build(python): vendor rpycdec #17
@@ -4,9 +4,14 @@ module(
|
||||
)
|
||||
|
||||
bazel_dep(name = "bazel_skylib", version = "1.8.2")
|
||||
bazel_dep(name = "rules_python", version = "1.6.3")
|
||||
bazel_dep(name = "rules_rust", version = "0.74.0")
|
||||
|
||||
python = use_extension("//bazel:python.bzl", "python")
|
||||
use_repo(python, "python")
|
||||
|
||||
rust = use_extension("//bazel:rust.bzl", "rust")
|
||||
use_repo(rust, "rust")
|
||||
|
||||
register_toolchains("@python//:python")
|
||||
register_toolchains("@rust//:rust")
|
||||
|
||||
638
MODULE.bazel.lock
generated
638
MODULE.bazel.lock
generated
@@ -6,35 +6,53 @@
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/source.json": "14892cc698e02ffedf4967546e6bedb7245015906888d3465fcf27c90a26da10",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.24.1/source.json": "cf725267cbacc5f028ef13bb77e7f2c2e0066923a4dab1025e4a0511b1ed258a",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.50.0/MODULE.bazel": "2083ef9c7a469f520890483ccf8e0189d6e71e2117e7752e15e6554433d5ae3e",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.50.0/source.json": "e0ee3debde2789ff56e4452e612d126925ba9ab64d4bde79c67f099d2902df9b",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
|
||||
"https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
|
||||
"https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
|
||||
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0/source.json": "2478949479000fdd7de9a3d0107ba2c85bb5f961c3ecb1aa448f52549ce310b5",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d",
|
||||
"https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83",
|
||||
"https://bcr.bazel.build/modules/package_metadata/0.0.3/source.json": "742075a428ad12a3fa18a69014c2f57f01af910c6d9d18646c990200853e641a",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
|
||||
@@ -45,10 +63,22 @@
|
||||
"https://bcr.bazel.build/modules/platforms/1.1.0/source.json": "fcf351c47596c939140ab0d333dfdd08ed1ea6ce33c2fe70c12493a301cf1344",
|
||||
"https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
|
||||
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
|
||||
"https://bcr.bazel.build/modules/protobuf/27.0/source.json": "1acf3d080c728d42f423fde5422fd0a1a24f44c15908124ce12363a253384193",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/source.json": "c16a6488fb279ef578da7098e605082d72ed85fc8d843eaae81e7d27d0f4625d",
|
||||
"https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
|
||||
"https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
|
||||
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
|
||||
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022",
|
||||
"https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
|
||||
"https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4",
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
|
||||
@@ -56,41 +86,83 @@
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.8/source.json": "85087982aca15f31307bd52698316b28faa31bd2c3095a41f456afec0131344c",
|
||||
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
|
||||
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
|
||||
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e",
|
||||
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
|
||||
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
|
||||
"https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.6.5/source.json": "a805b889531d1690e3c72a7a7e47a870d00323186a9904b36af83aa3d053ee8d",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.5.1/source.json": "db1a77d81b059e0f84985db67a22f3f579a529a86b7997605be3d214a0abe38e",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/source.json": "5abb45cc9beb27b77aec6a65a11855ef2b55d95dfdc358e9f312b78ae0ba32d5",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/source.json": "c2557066e0c0342223ba592510ad3d812d4963b9024831f7f66fd0584dd8c66c",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
|
||||
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
|
||||
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
|
||||
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/source.json": "d57902c052424dfda0e71646cb12668d39c4620ee0544294d9d941e7d12bc3a9",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.2/source.json": "17a2e195f56cb28d6bbf763e49973d13890487c6945311ed141e196fb660426d",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.22.1/source.json": "57226905e783bae7c37c2dd662be078728e48fa28ee4324a7eabcafb5a43d014",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.6.3/source.json": "f0be74977e5604a6526c8a416cda22985093ff7d5d380d41722d7e44015cc419",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.74.0/MODULE.bazel": "6e6fa04d7c8202f9cd63205ec9f96da39382a4ac971ee1d7a900fe8e1f68be17",
|
||||
"https://bcr.bazel.build/modules/rules_rust/0.74.0/source.json": "04eac31b4536eb23dd8e8e8ca7e079d3c1c4ac99934d38aa54bbae36917c7507",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.3/source.json": "cd53fe968dc8cd98197c052db3db6d82562960c87b61e7a90ee96f8e4e0dda97",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
|
||||
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d"
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
|
||||
},
|
||||
"selectedYankedVersions": {},
|
||||
"moduleExtensions": {
|
||||
"//bazel:python.bzl%python": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "CbGiMYJXjcea8dmoNxUWe/lWOjqRWfQPTXFHUUMheIY=",
|
||||
"usagesDigest": "bcY43VsTrhTJ8N4Dj5FtsgMhZxLyKKjHP+wWK4ntJQ4=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"python": {
|
||||
"bzlFile": "@@//bazel:python.bzl",
|
||||
"ruleClassName": "python_repository",
|
||||
"attributes": {}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": []
|
||||
}
|
||||
},
|
||||
"//bazel:rust.bzl%rust": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "4kUzQyjRjzchWIBZJKkNs8LuO4F+g/DwuwtTa0RTPcU=",
|
||||
@@ -108,6 +180,29 @@
|
||||
"recordedRepoMappingEntries": []
|
||||
}
|
||||
},
|
||||
"@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "KIX40nDfygEWbU+rq3nYpt3tVgTK/iO8PKh5VMBlN7M=",
|
||||
"usagesDigest": "pwHZ+26iLgQdwvdZeA5wnAjKnNI3y6XO2VbhOTeo5h8=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"compatibility_proxy": {
|
||||
"bzlFile": "@@rules_java~//java:rules_java_deps.bzl",
|
||||
"ruleClassName": "_compatibility_proxy_repo_rule",
|
||||
"attributes": {}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"rules_java~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_jvm_external~//:extensions.bzl%maven": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "twF5CTH+cMNcSDOS8HgBaxaIQ+FsdzZBKM0EbgJoYGU=",
|
||||
@@ -1123,47 +1218,100 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_jvm_external~//:non-module-deps.bzl%non_module_deps": {
|
||||
"@@rules_kotlin~//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "ZOivBbbZUakRexeLO/N26oX4Bcph6HHnqNmfxt7yoCc=",
|
||||
"usagesDigest": "SgmaWbjiY+8dgpUSia89kYJqNm3A67fr0hnvRaklrV4=",
|
||||
"bzlTransitiveDigest": "fus14IFJ/1LGWWGKPH/U18VnJCoMjfDt1ckahqCnM0A=",
|
||||
"usagesDigest": "aJF6fLy82rR95Ff5CZPAqxNoFgOMLMN5ImfBS0nhnkg=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"io_bazel_rules_kotlin": {
|
||||
"com_github_jetbrains_kotlin_git": {
|
||||
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
|
||||
"ruleClassName": "kotlin_compiler_git_repository",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip"
|
||||
],
|
||||
"sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88"
|
||||
}
|
||||
},
|
||||
"com_github_jetbrains_kotlin": {
|
||||
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
|
||||
"ruleClassName": "kotlin_capabilities_repository",
|
||||
"attributes": {
|
||||
"git_repository_name": "com_github_jetbrains_kotlin_git",
|
||||
"compiler_version": "1.9.23"
|
||||
}
|
||||
},
|
||||
"com_github_google_ksp": {
|
||||
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:ksp.bzl",
|
||||
"ruleClassName": "ksp_compiler_plugin_repository",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip"
|
||||
],
|
||||
"sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d",
|
||||
"strip_version": "1.9.23-1.0.20"
|
||||
}
|
||||
},
|
||||
"com_github_pinterest_ktlint": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_file",
|
||||
"attributes": {
|
||||
"sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985",
|
||||
"urls": [
|
||||
"https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint"
|
||||
],
|
||||
"executable": true
|
||||
}
|
||||
},
|
||||
"rules_android": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"sha256": "946747acdbeae799b085d12b240ec346f775ac65236dfcf18aa0cd7300f6de78",
|
||||
"sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
|
||||
"strip_prefix": "rules_android-0.1.1",
|
||||
"urls": [
|
||||
"https://github.com/bazelbuild/rules_kotlin/releases/download/v1.7.0-RC-2/rules_kotlin_release.tgz"
|
||||
"https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"rules_jvm_external~",
|
||||
"rules_kotlin~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_python~//python/extensions:python.bzl%python": {
|
||||
"@@rules_python~//python/uv:uv.bzl%uv": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "lbXqTyC4ahBb81TIrIp+2d3sWnlurVNqSeAaLJknLUs=",
|
||||
"usagesDigest": "1Y6kbygksx7wAtDStFoHnR90xr8Yeq00I91YcLMbxMI=",
|
||||
"bzlTransitiveDigest": "mxPY/VBQrSC9LvYeRrlxD+0IkDTQ4+36NGMnGWlN/Vw=",
|
||||
"usagesDigest": "qIeg3YEigU6oZD/nE0vQzliorEcL5dtbidAIP+ZhylM=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"pythons_hub": {
|
||||
"bzlFile": "@@rules_python~//python/extensions/private:interpreter_hub.bzl",
|
||||
"ruleClassName": "hub_repo",
|
||||
"uv": {
|
||||
"bzlFile": "@@rules_python~//python/uv/private:uv_toolchains_repo.bzl",
|
||||
"ruleClassName": "uv_toolchains_repo",
|
||||
"attributes": {
|
||||
"toolchains": []
|
||||
"toolchain_type": "'@@rules_python~//python/uv:uv_toolchain_type'",
|
||||
"toolchain_names": [
|
||||
"none"
|
||||
],
|
||||
"toolchain_implementations": {
|
||||
"none": "'@@rules_python~//python:none'"
|
||||
},
|
||||
"toolchain_compatible_with": {
|
||||
"none": [
|
||||
"@platforms//:incompatible"
|
||||
]
|
||||
},
|
||||
"toolchain_target_settings": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1175,446 +1323,8 @@
|
||||
],
|
||||
[
|
||||
"rules_python~",
|
||||
"rules_python",
|
||||
"rules_python~"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_python~//python/extensions/private:internal_deps.bzl%internal_deps": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "b6FMQSdoZ1QOssw14AW8bWDn2BvywI4FVkLbO2nTMsE=",
|
||||
"usagesDigest": "KPNj8wxzOk7dXY9StqZ91MCKEIJSEnAyV0Q/dGFP5sw=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"pypi__build": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/03/97/f58c723ff036a8d8b4d3115377c0a37ed05c1f68dd9a0d66dab5e82c5c1c/build-0.9.0-py3-none-any.whl",
|
||||
"sha256": "38a7a2b7a0bdc61a42a0a67509d88c71ecfc37b393baba770fae34e20929ff69",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__click": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/76/0a/b6c5f311e32aeb3b406e03c079ade51e905ea630fc19d1262a46249c1c86/click-8.0.1-py3-none-any.whl",
|
||||
"sha256": "fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__colorama": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",
|
||||
"sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__installer": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl",
|
||||
"sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__packaging": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/8f/7b/42582927d281d7cb035609cd3a543ffac89b74f3f4ee8e1c50914bcb57eb/packaging-22.0-py3-none-any.whl",
|
||||
"sha256": "957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pep517": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/ee/2f/ef63e64e9429111e73d3d6cbee80591672d16f2725e648ebc52096f3d323/pep517-0.13.0-py3-none-any.whl",
|
||||
"sha256": "4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pip": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/09/bd/2410905c76ee14c62baf69e3f4aa780226c1bbfc9485731ad018e35b0cb5/pip-22.3.1-py3-none-any.whl",
|
||||
"sha256": "908c78e6bc29b676ede1c4d57981d490cb892eb45cd8c214ab6298125119e077",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__pip_tools": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/5e/e8/f6d7d1847c7351048da870417724ace5c4506e816b38db02f4d7c675c189/pip_tools-6.12.1-py3-none-any.whl",
|
||||
"sha256": "f0c0c0ec57b58250afce458e2e6058b1f30a4263db895b7d72fd6311bf1dc6f7",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__setuptools": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/7c/5b/3d92b9f0f7ca1645cba48c080b54fe7d8b1033a4e5720091d1631c4266db/setuptools-60.10.0-py3-none-any.whl",
|
||||
"sha256": "782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__tomli": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl",
|
||||
"sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__wheel": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/bd/7c/d38a0b30ce22fc26ed7dbc087c6d00851fb3395e9d0dac40bec1f905030c/wheel-0.38.4-py3-none-any.whl",
|
||||
"sha256": "b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__importlib_metadata": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/d7/31/74dcb59a601b95fce3b0334e8fc9db758f78e43075f22aeb3677dfb19f4c/importlib_metadata-1.4.0-py2.py3-none-any.whl",
|
||||
"sha256": "bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__zipp": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/f4/50/cc72c5bcd48f6e98219fc4a88a5227e9e28b81637a99c49feba1d51f4d50/zipp-1.0.0-py2.py3-none-any.whl",
|
||||
"sha256": "8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__more_itertools": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"url": "https://files.pythonhosted.org/packages/bd/3f/c4b3dbd315e248f84c388bd4a72b131a29f123ecacc37ffb2b3834546e42/more_itertools-8.13.0-py3-none-any.whl",
|
||||
"sha256": "c5122bffc5f104d37c1626b8615b511f3427aa5389b94d61e5ef8236bfbc3ddb",
|
||||
"type": "zip",
|
||||
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp38_aarch64-apple-darwin": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/07/82/79fa21ceca9a9b091eb3c67e27eb648dade27b2c9e1eb23af47232a2a365/coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp38_aarch64-unknown-linux-gnu": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/40/3b/cd68cb278c4966df00158811ec1e357b9a7d132790c240fc65da57e10013/coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp38_x86_64-apple-darwin": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/05/63/a789b462075395d34f8152229dccf92b25ca73eac05b3f6cd75fa5017095/coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp38_x86_64-unknown-linux-gnu": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/bd/a0/e263b115808226fdb2658f1887808c06ac3f1b579ef5dda02309e0d54459/coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp39_aarch64-apple-darwin": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/63/e9/f23e8664ec4032d7802a1cf920853196bcbdce7b56408e3efe1b2da08f3c/coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp39_aarch64-unknown-linux-gnu": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/18/95/27f80dcd8273171b781a19d109aeaed7f13d78ef6d1e2f7134a5826fd1b4/coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp39_x86_64-apple-darwin": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/ea/52/c08080405329326a7ff16c0dfdb4feefaa8edd7446413df67386fe1bbfe0/coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp39_x86_64-unknown-linux-gnu": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/6b/f2/919f0fdc93d3991ca074894402074d847be8ac1e1d78e7e9e1c371b69a6f/coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp310_aarch64-apple-darwin": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/89/a2/cbf599e50bb4be416e0408c4cf523c354c51d7da39935461a9687e039481/coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp310_aarch64-unknown-linux-gnu": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/15/b0/3639d84ee8a900da0cf6450ab46e22517e4688b6cec0ba8ab6f8166103a2/coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp310_x86_64-apple-darwin": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/c4/8d/5ec7d08f4601d2d792563fe31db5e9322c306848fec1e65ec8885927f739/coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp310_x86_64-unknown-linux-gnu": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/3c/7d/d5211ea782b193ab8064b06dc0cc042cf1a4ca9c93a530071459172c550f/coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp311_aarch64-unknown-linux-gnu": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/36/f3/5cbd79cf4cd059c80b59104aca33b8d05af4ad5bf5b1547645ecee716378/coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp311_x86_64-apple-darwin": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/50/cf/455930004231fa87efe8be06d13512f34e070ddfee8b8bf5a050cdc47ab3/coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"pypi__coverage_cp311_x86_64-unknown-linux-gnu": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@rules_python~//python/private:coverage.patch"
|
||||
],
|
||||
"sha256": "a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91",
|
||||
"type": "zip",
|
||||
"urls": [
|
||||
"https://files.pythonhosted.org/packages/6a/63/8e82513b7e4a1b8d887b4e85c1c2b6c9b754a581b187c0b084f3330ac479/coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"rules_python~",
|
||||
"bazel_skylib",
|
||||
"bazel_skylib~"
|
||||
],
|
||||
[
|
||||
"rules_python~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
],
|
||||
[
|
||||
"rules_python~",
|
||||
"rules_python",
|
||||
"rules_python~"
|
||||
"platforms",
|
||||
"platforms"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
exports_files([
|
||||
"python.BUILD.template",
|
||||
"python.bzl",
|
||||
"rust.BUILD.template",
|
||||
"rust.bzl",
|
||||
"utils.bzl",
|
||||
|
||||
25
bazel/python.BUILD.template
Normal file
25
bazel/python.BUILD.template
Normal file
@@ -0,0 +1,25 @@
|
||||
load("@rules_python//python:py_runtime.bzl", "py_runtime")
|
||||
load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair")
|
||||
|
||||
py_runtime(
|
||||
name = "runtime",
|
||||
implementation_name = "cpython",
|
||||
interpreter_path = "{interpreter_path}",
|
||||
interpreter_version_info = {
|
||||
"major": "{major}",
|
||||
"minor": "{minor}",
|
||||
"micro": "{micro}",
|
||||
},
|
||||
python_version = "PY3",
|
||||
)
|
||||
|
||||
py_runtime_pair(
|
||||
name = "runtime_pair",
|
||||
py3_runtime = ":runtime",
|
||||
)
|
||||
|
||||
toolchain(
|
||||
name = "python",
|
||||
toolchain = ":runtime_pair",
|
||||
toolchain_type = "@rules_python//python:toolchain_type",
|
||||
)
|
||||
46
bazel/python.bzl
Normal file
46
bazel/python.bzl
Normal file
@@ -0,0 +1,46 @@
|
||||
load("//bazel:utils.bzl", "execute", "require_env")
|
||||
|
||||
|
||||
def _python_repository_impl(repository_ctx):
|
||||
root = repository_ctx.path(require_env(repository_ctx, "PYTHON_TOOLCHAIN_ROOT"))
|
||||
python3 = root.get_child("bin/python3")
|
||||
|
||||
version = execute(
|
||||
repository_ctx,
|
||||
[
|
||||
str(python3),
|
||||
"-c",
|
||||
"import sys; print('{}.{}.{}'.format(*sys.version_info[:3]))",
|
||||
],
|
||||
)
|
||||
parts = version.split(".")
|
||||
if len(parts) != 3:
|
||||
fail("python3 did not report a major.minor.micro version")
|
||||
|
||||
repository_ctx.file("WORKSPACE.bazel", "")
|
||||
repository_ctx.template(
|
||||
"BUILD.bazel",
|
||||
Label("//bazel:python.BUILD.template"),
|
||||
{
|
||||
"{interpreter_path}": str(python3),
|
||||
"{major}": parts[0],
|
||||
"{minor}": parts[1],
|
||||
"{micro}": parts[2],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
python_repository = repository_rule(
|
||||
implementation = _python_repository_impl,
|
||||
local = True,
|
||||
environ = ["PYTHON_TOOLCHAIN_ROOT"],
|
||||
)
|
||||
|
||||
|
||||
def _python_impl(module_ctx):
|
||||
python_repository(name = "python")
|
||||
|
||||
|
||||
python = module_extension(
|
||||
implementation = _python_impl,
|
||||
)
|
||||
@@ -12,6 +12,7 @@ in
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
modules.shell.bazel.enable = true;
|
||||
modules.shell.python.enable = true;
|
||||
modules.shell.rust.enable = true;
|
||||
};
|
||||
}
|
||||
|
||||
20
configs/nixos/modules/shell/python.nix
Normal file
20
configs/nixos/modules/shell/python.nix
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# Python tools for the primary user's shell environment.
|
||||
let
|
||||
cfg = config.modules.shell.python;
|
||||
user = config.user.name;
|
||||
in
|
||||
{
|
||||
options.modules.shell.python.enable = lib.mkEnableOption "Python tools for the primary user's shell environment";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
registry.env-var.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
|
||||
|
||||
home-manager.users.${user}.home.packages = [ pkgs.python3 ];
|
||||
};
|
||||
}
|
||||
16
third_party/python/BUILD.bazel
vendored
Normal file
16
third_party/python/BUILD.bazel
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
load("@rules_python//python:defs.bzl", "py_binary", "py_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
py_library(
|
||||
name = "rpycdec",
|
||||
srcs = glob(["rpycdec/src/**/*.py"]),
|
||||
imports = ["rpycdec/src"],
|
||||
)
|
||||
|
||||
py_binary(
|
||||
name = "rpycdec_bin",
|
||||
srcs = ["rpycdec_bin.py"],
|
||||
main = "rpycdec_bin.py",
|
||||
deps = [":rpycdec"],
|
||||
)
|
||||
41
third_party/python/README.md
vendored
Normal file
41
third_party/python/README.md
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# Vendored Python packages
|
||||
|
||||
This package exposes vendored Python dependencies through stable public Bazel targets.
|
||||
Bazel uses `PYTHON_TOOLCHAIN_ROOT` to find the monorepo `python3` runtime.
|
||||
|
||||
Project code depends on targets in `//third_party/python`.
|
||||
It does not depend on files inside vendored source directories directly.
|
||||
|
||||
Use this layout for a vendored package:
|
||||
|
||||
```text
|
||||
third_party/python/
|
||||
BUILD.bazel
|
||||
<name>/
|
||||
LICENSE
|
||||
pyproject.toml
|
||||
src/
|
||||
```
|
||||
|
||||
Use the upstream package name for library targets.
|
||||
For command-line tools, add a `_bin` suffix to the binary target.
|
||||
|
||||
Preserve upstream license files and package metadata.
|
||||
Keep repository Bazel files outside upstream source when possible.
|
||||
|
||||
Add optional dependencies as separate public targets.
|
||||
Do not bundle optional dependencies into the primary package target.
|
||||
|
||||
## `rpycdec`
|
||||
|
||||
Ariadne uses `rpycdec` for archive extraction and debug decompilation.
|
||||
Ariadne uses the game-bundled Ren'Py runtime for AST export.
|
||||
|
||||
The vendored source comes from `cnfatal/rpycdec` at commit `489e9c6b8a48ed7904d7039d7787c36a03c75fe3`.
|
||||
|
||||
The public targets are:
|
||||
|
||||
```text
|
||||
//third_party/python:rpycdec
|
||||
//third_party/python:rpycdec_bin
|
||||
```
|
||||
37
third_party/python/rpycdec/.github/workflows/python-publish.yml
vendored
Normal file
37
third_party/python/rpycdec/.github/workflows/python-publish.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# This workflow will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload Python Package
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
# on push tag
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v3
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
- name: Publish package
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
15
third_party/python/rpycdec/.gitignore
vendored
Normal file
15
third_party/python/rpycdec/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
*.swp
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.rpyc
|
||||
__pycache__
|
||||
/build
|
||||
/dist
|
||||
/.vscode
|
||||
/tmp
|
||||
/sdk
|
||||
/workspace
|
||||
/.cache
|
||||
/.venv
|
||||
/sdks
|
||||
.DS_Store
|
||||
2
third_party/python/rpycdec/.pylintrc
vendored
Normal file
2
third_party/python/rpycdec/.pylintrc
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
[MASTER]
|
||||
ignore-patterns=tests/*,docs/*,renpy/*
|
||||
10
third_party/python/rpycdec/DEVELOP.md
vendored
Normal file
10
third_party/python/rpycdec/DEVELOP.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Development Guide
|
||||
|
||||
## How decompilation works
|
||||
|
||||
All rpyc files are compiled from rpy files, renpy SDK read every file and parse it to AST object(renpy), and then use pickle to serialize the AST to rpyc file. So we use pickle to deserialize the rpyc file to AST, and then restore the rpy file from AST.
|
||||
|
||||
We created a fake "renpy" package(unlikely unrpyc) and it's ast objects to make the pickle can be deserialized correctly.
|
||||
Another reason to create a fake "renpy" package is that we can separate code generate logic (the `get_code(...)` function) to each AST object, it will be easier to future maintain and for multi-version renpy support.
|
||||
|
||||
The most difficult part is generating the rpy file from AST. Different renpy version has different AST structure also different grammar.
|
||||
21
third_party/python/rpycdec/LICENSE
vendored
Normal file
21
third_party/python/rpycdec/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [2023] [cnfatal@gmail.com]
|
||||
|
||||
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.
|
||||
21
third_party/python/rpycdec/Makefile
vendored
Normal file
21
third_party/python/rpycdec/Makefile
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
PYTHON ?= python
|
||||
RUFF ?= ruff
|
||||
|
||||
.PHONY: wheels release test lint format
|
||||
|
||||
wheels:
|
||||
$(PYTHON) -m build --wheel
|
||||
|
||||
release:
|
||||
twine upload dist/*
|
||||
|
||||
test:
|
||||
$(PYTHON) -m unittest discover -s tests
|
||||
|
||||
lint:
|
||||
$(RUFF) check .
|
||||
$(RUFF) format --check .
|
||||
|
||||
format:
|
||||
$(RUFF) check . --fix
|
||||
$(RUFF) format .
|
||||
111
third_party/python/rpycdec/README.md
vendored
Normal file
111
third_party/python/rpycdec/README.md
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
# rpycdec
|
||||
|
||||
A tool for decompiling Ren'py compiled script files (.rpyc and .rpymc).
|
||||
|
||||
## Features
|
||||
|
||||
- Decompile `.rpyc` and `.rpymc` files to readable Ren'Py script code
|
||||
- Extract RPA archives
|
||||
- Create RPA archives from existing files or directories
|
||||
- Extract and edit Ren'Py save files (`.save` → JSON → `.save`)
|
||||
- Extract Ren'Py games from Android APK files
|
||||
- Extract translations from compiled scripts to `tl/{language}/` directories
|
||||
- Support for multiple Ren'Py versions (7.x, 8.x)
|
||||
|
||||
## Installation
|
||||
|
||||
Install with pip:
|
||||
|
||||
```sh
|
||||
pip install rpycdec
|
||||
```
|
||||
|
||||
Or install from source:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/cnfatal/rpycdec.git
|
||||
cd rpycdec
|
||||
pip install .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line Interface
|
||||
|
||||
Decompile a single file:
|
||||
|
||||
```sh
|
||||
rpycdec decompile script.rpyc
|
||||
```
|
||||
|
||||
Decompile all files in a directory:
|
||||
|
||||
```sh
|
||||
rpycdec decompile /path/to/game/
|
||||
```
|
||||
|
||||
Extract RPA archive:
|
||||
|
||||
```sh
|
||||
rpycdec unrpa archive.rpa
|
||||
```
|
||||
|
||||
Extract files whose archive paths match a regular expression:
|
||||
|
||||
```sh
|
||||
rpycdec unrpa archive.rpa -e '\.rpyc?$'
|
||||
```
|
||||
|
||||
Extract files with one of several suffixes (case-insensitive):
|
||||
|
||||
```sh
|
||||
rpycdec unrpa archive.rpa -s .rpy .rpyc
|
||||
```
|
||||
|
||||
`--expression`/`-e` may be repeated, as may `--suffix`/`-s`. When both are
|
||||
used, a file is extracted if it matches any expression or any suffix.
|
||||
|
||||
Create RPA archive:
|
||||
|
||||
```sh
|
||||
rpycdec rpa /path/to/game/files -o archive.rpa
|
||||
```
|
||||
|
||||
Extract Ren'Py game from Android APK:
|
||||
|
||||
```sh
|
||||
rpycdec extract-game game.apk
|
||||
```
|
||||
|
||||
Extract translations:
|
||||
|
||||
```sh
|
||||
rpycdec extract-translate /path/to/game/ -l Chinese
|
||||
```
|
||||
|
||||
## Security Warning
|
||||
|
||||
This tool processes `.rpyc`, `.rpymc`, `.rpa`, and `.save` files which use Python's `pickle` format internally. rpycdec uses restricted unpicklers with whitelist-based class loading to mitigate arbitrary code execution risks, but **no pickle safeguard is perfect**. Only process files from sources you trust.
|
||||
|
||||
Set `RPYCDEC_NO_WARNING=1` to suppress the CLI security warning.
|
||||
|
||||
See also: [Python pickle security warning](https://docs.python.org/3/library/pickle.html#module-pickle)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Q: Pickle error `ModuleNotFoundError: No module named '...'`**
|
||||
|
||||
A: This means our fake `renpy`/`store` packages don't cover the class your file needs. Please [open an issue](https://github.com/cnfatal/rpycdec/issues) with the Ren'Py version and the file that failed.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please [open an issue](https://github.com/cnfatal/rpycdec/issues) before submitting major changes so we can discuss the approach.
|
||||
|
||||
## Community & Support
|
||||
|
||||
- [GitHub Issues](https://github.com/cnfatal/rpycdec/issues) — Bug reports and feature requests
|
||||
- [Telegram Group](https://t.me/rpycdec) — Community discussion and support
|
||||
|
||||
## Alternative
|
||||
|
||||
- [unrpyc](https://github.com/CensoredUsername/unrpyc) - The well-established and widely-used Ren'Py script decompiler
|
||||
55
third_party/python/rpycdec/pyproject.toml
vendored
Normal file
55
third_party/python/rpycdec/pyproject.toml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
[build-system]
|
||||
requires = ["hatchling", "hatch-vcs"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "rpycdec"
|
||||
authors = [{ name = "cnfatal", email = "cnfatal@gmail.com" }]
|
||||
description = "A tool to decrypt .rpyc files"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Utilities",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
release = ["twine"]
|
||||
sign = ["ecdsa"]
|
||||
dev = ["ruff>=0.16.4,<0.17"]
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/cnfatal/rpycdec"
|
||||
"Bug Tracker" = "https://github.com/cnfatal/rpycdec/issues"
|
||||
|
||||
[project.scripts]
|
||||
rpycdec = "rpycdec:main"
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
exclude = ["/.github", "/docs"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/rpycdec", "src/renpy", "src/store"]
|
||||
|
||||
[tool.hatch.version]
|
||||
source = "vcs"
|
||||
|
||||
[tool.ruff]
|
||||
target-version = "py310"
|
||||
line-length = 88
|
||||
extend-exclude = ["src/renpy", "src/store"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E4", "E7", "E9", "F", "I", "UP035"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"src/rpycdec/__init__.py" = ["F401"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
line-ending = "auto"
|
||||
1
third_party/python/rpycdec/src/renpy/__init__.py
vendored
Normal file
1
third_party/python/rpycdec/src/renpy/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# Ren'Py AST and support modules for rpycdec
|
||||
1350
third_party/python/rpycdec/src/renpy/ast.py
vendored
Normal file
1350
third_party/python/rpycdec/src/renpy/ast.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
32
third_party/python/rpycdec/src/renpy/astsupport.py
vendored
Normal file
32
third_party/python/rpycdec/src/renpy/astsupport.py
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
class PyExpr(str):
|
||||
|
||||
expr: str
|
||||
filename: str
|
||||
linenumber: int
|
||||
py: int
|
||||
hashcode: str
|
||||
column: int
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
expr: str,
|
||||
filename: str = '',
|
||||
linenumber: int = 0,
|
||||
py: int = 3,
|
||||
hashcode: str = '',
|
||||
column: int = 0,
|
||||
):
|
||||
self = super().__new__(cls, expr)
|
||||
self.expr = expr
|
||||
self.filename = filename
|
||||
self.linenumber = linenumber
|
||||
self.py = py
|
||||
self.hashcode = hashcode
|
||||
self.column = column
|
||||
return self
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
return self.expr
|
||||
|
||||
def __str__(self):
|
||||
return self.expr
|
||||
319
third_party/python/rpycdec/src/renpy/atl.py
vendored
Normal file
319
third_party/python/rpycdec/src/renpy/atl.py
vendored
Normal file
@@ -0,0 +1,319 @@
|
||||
from . import util, ast, astsupport
|
||||
from .object import Object
|
||||
|
||||
|
||||
class Context(object):
|
||||
pass
|
||||
|
||||
|
||||
class ATLTransformBase(Object):
|
||||
pass
|
||||
|
||||
|
||||
class RawStatement(object):
|
||||
pass
|
||||
|
||||
|
||||
class Statement(Object):
|
||||
pass
|
||||
|
||||
|
||||
class RawBlock(RawStatement):
|
||||
statements = None
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#block-statement
|
||||
|
||||
atl_block_stmt ::= "block" ":"
|
||||
atl_block
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
if (
|
||||
not self.animation
|
||||
and len(self.statements) == 1
|
||||
and isinstance(self.statements[0], RawBlock)
|
||||
):
|
||||
return util.get_code(self.statements[0], **kwargs)
|
||||
|
||||
rv = []
|
||||
if self.animation:
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#animation-statement
|
||||
|
||||
atl_animation ::= "animation"
|
||||
|
||||
image eileen vhappy moving:
|
||||
animation
|
||||
"eileen vhappy"
|
||||
xalign 0.0
|
||||
linear 5.0 xalign 1.0
|
||||
repeat
|
||||
"""
|
||||
rv.append("animation")
|
||||
|
||||
statements = self.statements
|
||||
if statements:
|
||||
for statement in statements:
|
||||
if statement is None:
|
||||
rv.append("pass")
|
||||
else:
|
||||
rv.append(util.get_code(statement, **kwargs))
|
||||
else:
|
||||
rv.append("pass")
|
||||
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class Block(Statement):
|
||||
pass
|
||||
|
||||
|
||||
class RawMultipurpose(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#interpolation-statement
|
||||
|
||||
atl_interp ::= ((warper simple_expression) | ("warp" simple_expression simple_expression)) (atl_interp_target+ | (":"
|
||||
atl_interp_target+ ))
|
||||
atl_interp_target ::= (atl_property+ ("knot" simple_expression)* )
|
||||
| atl_transform_expression
|
||||
| "clockwise"
|
||||
| "counterclockwise"
|
||||
| ("circles" simple_expression)
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
start = ""
|
||||
warp_fun, duration, wrap = self.warp_function, self.duration, self.warper
|
||||
if warp_fun and wrap is None:
|
||||
# warp {warp_function:expr} {duration:expr}
|
||||
start = f"warp {util.get_code(warp_fun, **kwargs)} {util.get_code(duration, **kwargs)}"
|
||||
elif not warp_fun and wrap:
|
||||
# {wrap} {duration:expr}
|
||||
start = f"{wrap} {util.get_code(duration, **kwargs)}"
|
||||
elif not warp_fun and not wrap and duration == "0":
|
||||
pass
|
||||
|
||||
rv = []
|
||||
|
||||
revolution = self.revolution
|
||||
if revolution:
|
||||
rv.append(revolution)
|
||||
|
||||
circles = self.circles
|
||||
if circles and circles != "0":
|
||||
# circles {expression}
|
||||
rv.append(f"circles {util.get_code(circles, **kwargs)}")
|
||||
|
||||
for prop, expr in self.properties:
|
||||
rv.append(f"{prop} {util.get_code(expr, **kwargs)}")
|
||||
|
||||
for prop, exprs in self.splines:
|
||||
last, exprs = exprs[-1], exprs[:-1]
|
||||
knots = " ".join(f"knot {util.get_code(expr, **kwargs)}" for expr in exprs)
|
||||
rv.append(f"{prop} {util.get_code(last, **kwargs)} {knots}")
|
||||
|
||||
for expr, with_expr in self.expressions:
|
||||
if with_expr is None:
|
||||
rv.append(str(expr))
|
||||
continue
|
||||
if isinstance(with_expr, ast.PyExpr):
|
||||
valstr = f"with {with_expr}"
|
||||
elif isinstance(with_expr, astsupport.PyExpr):
|
||||
valstr = f"with {with_expr}"
|
||||
else:
|
||||
valstr = str(with_expr)
|
||||
rv.append(f"{expr} {valstr}")
|
||||
|
||||
if not start:
|
||||
return " ".join(rv)
|
||||
if not rv:
|
||||
# if no properties, return just the warp
|
||||
return start
|
||||
if len(rv) < 3:
|
||||
# if only few properties, return them in one line
|
||||
return start + " " + " ".join(rv)
|
||||
return start + ":\n" + util.indent(" ".join(rv))
|
||||
|
||||
|
||||
class RawContainsExpr(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#inline-contains-statement
|
||||
|
||||
atl_contains ::= "contains" simple_expression
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
"""
|
||||
Returns the code for a contains expression.
|
||||
"""
|
||||
expression = util.attr(self, "expression")
|
||||
if expression:
|
||||
return f"contains {util.get_code(expression, **kwargs)}"
|
||||
return "contains"
|
||||
|
||||
|
||||
# This allows us to have multiple ATL transforms as children.
|
||||
class RawChild(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#contains-block-statement
|
||||
|
||||
atl_counts ::= "contains" ":"
|
||||
atl_block
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
children = util.attr(self, "children")
|
||||
if not children:
|
||||
return "contains:\n pass"
|
||||
rv = ["contains:"]
|
||||
rv.append(util.indent(util.get_code(children, **kwargs)))
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
# This changes the child of this statement, optionally with a transition.
|
||||
class Child(Statement):
|
||||
pass
|
||||
|
||||
|
||||
# This causes interpolation to occur.
|
||||
class Interpolation(Statement):
|
||||
pass
|
||||
|
||||
|
||||
class RawRepeat(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#repeat-statement
|
||||
|
||||
atl_repeat ::= "repeat" (simple_expression)?
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
if self.repeats:
|
||||
return f"repeat {util.get_code(self.repeats, **kwargs)}"
|
||||
return "repeat"
|
||||
|
||||
|
||||
class Repeat(Statement):
|
||||
pass
|
||||
|
||||
|
||||
class RawParallel(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#parallel-statement
|
||||
|
||||
atl_parallel ::= ("parallel" ":"
|
||||
atl_block)+
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
rv = ["parallel:"]
|
||||
blocks = util.attr(self, "blocks")
|
||||
if not blocks:
|
||||
rv.append(util.indent("pass"))
|
||||
else:
|
||||
rv.append(util.indent(util.get_code(blocks, **kwargs)))
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class Parallel(Statement):
|
||||
pass
|
||||
|
||||
|
||||
class RawChoice(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#choice-statement
|
||||
|
||||
atl_choice ::= ("choice" (simple_expression)? ":"
|
||||
atl_block)+
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
rv = []
|
||||
for text, stmt in self.choices:
|
||||
rv.append(f"choice {text}:")
|
||||
rv.append(util.indent(util.get_code(stmt, **kwargs)))
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class Choice(Statement):
|
||||
pass
|
||||
|
||||
|
||||
class RawTime(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/atl.html#time-statement
|
||||
|
||||
atl_time ::= "time" (simple_expression)?
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
time = util.attr(self, "time")
|
||||
if time:
|
||||
return f"time {util.get_code(time, **kwargs)}"
|
||||
return "time"
|
||||
|
||||
|
||||
class Time(Statement):
|
||||
pass
|
||||
|
||||
|
||||
class RawOn(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#on-statement
|
||||
|
||||
atl_on ::= "on" name [ "," name ] * ":"
|
||||
atl_block
|
||||
|
||||
on show:
|
||||
alpha 0.0
|
||||
linear .5 alpha 1.0
|
||||
on hide:
|
||||
linear .5 alpha 0.0
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
rv = []
|
||||
for text, stmt in self.handlers.items():
|
||||
rv.append(f"on {text}:")
|
||||
rv.append(util.indent(util.get_code(stmt, **kwargs)))
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class On(Statement):
|
||||
pass
|
||||
|
||||
|
||||
class RawEvent(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#event-statement
|
||||
|
||||
atl_event ::= "event" name
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
name = util.attr(self, "name")
|
||||
if name:
|
||||
return f"event {name}"
|
||||
return "event"
|
||||
|
||||
|
||||
class Event(Statement):
|
||||
pass
|
||||
|
||||
|
||||
class RawFunction(RawStatement):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/transforms.html#function-statement
|
||||
|
||||
atl_function ::= "function" simple_expression
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
if self.expr:
|
||||
return "function " + str(self.expr)
|
||||
else:
|
||||
return "function"
|
||||
|
||||
|
||||
class Function(Statement):
|
||||
pass
|
||||
15
third_party/python/rpycdec/src/renpy/config.py
vendored
Normal file
15
third_party/python/rpycdec/src/renpy/config.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
from typing import Optional
|
||||
|
||||
# Do we use old text substitutions?
|
||||
old_substitutions = True
|
||||
|
||||
# An alternate path to use when uneliding. (Mostly used by the launcher to enable
|
||||
# the style inspector.)
|
||||
alternate_unelide_path = None
|
||||
|
||||
# Various directories.
|
||||
gamedir = ""
|
||||
basedir = ""
|
||||
renpy_base = ""
|
||||
commondir = None # type: Optional[str]
|
||||
logdir = None # type: Optional[str] # Where log and error files go.
|
||||
71
third_party/python/rpycdec/src/renpy/display/behavior.py
vendored
Normal file
71
third_party/python/rpycdec/src/renpy/display/behavior.py
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
from . import layout, core
|
||||
from ..text import text
|
||||
from .. import object as renpyobject, python
|
||||
|
||||
|
||||
class Keymap(layout.Null):
|
||||
pass
|
||||
|
||||
|
||||
class RollForward(layout.Null):
|
||||
pass
|
||||
|
||||
|
||||
class PauseBehavior(layout.Null):
|
||||
pass
|
||||
|
||||
|
||||
class DismissBehavior(layout.Null):
|
||||
pass
|
||||
|
||||
|
||||
class SoundStopBehavior(layout.Null):
|
||||
pass
|
||||
|
||||
|
||||
class SayBehavior(layout.Null):
|
||||
pass
|
||||
|
||||
|
||||
class Button(layout.Window):
|
||||
pass
|
||||
|
||||
|
||||
class ImageButton(Button):
|
||||
pass
|
||||
|
||||
|
||||
class HoveredProxy(object):
|
||||
pass
|
||||
|
||||
|
||||
class Input(text.Text): # @UndefinedVariable
|
||||
pass
|
||||
|
||||
|
||||
class Adjustment(renpyobject.Object):
|
||||
pass
|
||||
|
||||
|
||||
class Bar(core.Displayable):
|
||||
pass
|
||||
|
||||
|
||||
class Conditional(layout.Container):
|
||||
pass
|
||||
|
||||
|
||||
class TimerState(python.RevertableObject):
|
||||
pass
|
||||
|
||||
|
||||
class Timer(layout.Null):
|
||||
pass
|
||||
|
||||
|
||||
class MouseArea(core.Displayable):
|
||||
pass
|
||||
|
||||
|
||||
class OnEvent(core.Displayable):
|
||||
pass
|
||||
33
third_party/python/rpycdec/src/renpy/display/core.py
vendored
Normal file
33
third_party/python/rpycdec/src/renpy/display/core.py
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
from .. import object as renpyobject
|
||||
|
||||
|
||||
class IgnoreEvent(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class EndInteraction(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class absolute(float):
|
||||
pass
|
||||
|
||||
|
||||
class DisplayableArguments(renpyobject.Object):
|
||||
pass
|
||||
|
||||
|
||||
class Displayable(renpyobject.Object):
|
||||
pass
|
||||
|
||||
|
||||
class SceneLists(renpyobject.Object):
|
||||
pass
|
||||
|
||||
|
||||
class MouseMove(object):
|
||||
pass
|
||||
|
||||
|
||||
class Interface(object):
|
||||
pass
|
||||
10
third_party/python/rpycdec/src/renpy/display/dragdrop.py
vendored
Normal file
10
third_party/python/rpycdec/src/renpy/display/dragdrop.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
from . import core, layout
|
||||
from .. import python
|
||||
|
||||
|
||||
class Drag(core.Displayable, python.RevertableObject):
|
||||
pass
|
||||
|
||||
|
||||
class DragGroup(layout.MultiBox):
|
||||
pass
|
||||
84
third_party/python/rpycdec/src/renpy/display/layout.py
vendored
Normal file
84
third_party/python/rpycdec/src/renpy/display/layout.py
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
from ..display import core, layout
|
||||
from .. import object as renpyobject
|
||||
|
||||
|
||||
class Null(core.Displayable):
|
||||
pass
|
||||
|
||||
|
||||
class Container(core.Displayable):
|
||||
pass
|
||||
|
||||
|
||||
class Position(layout.Container):
|
||||
pass
|
||||
|
||||
|
||||
class Grid(Container):
|
||||
pass
|
||||
|
||||
|
||||
class IgnoreLayers(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MultiBox(layout.Container):
|
||||
pass
|
||||
|
||||
|
||||
class SizeGroup(renpyobject.Object):
|
||||
pass
|
||||
|
||||
|
||||
class Window(Container):
|
||||
pass
|
||||
|
||||
|
||||
class DynamicDisplayable(core.Displayable):
|
||||
pass
|
||||
|
||||
|
||||
def ConditionSwitch(*args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class IgnoresEvents(Container):
|
||||
pass
|
||||
|
||||
|
||||
def Crop(rect, child, **properties):
|
||||
pass
|
||||
|
||||
|
||||
LiveCrop = Crop
|
||||
|
||||
|
||||
class Side(Container):
|
||||
pass
|
||||
|
||||
|
||||
class Alpha(core.Displayable):
|
||||
pass
|
||||
|
||||
|
||||
class AdjustTimes(Container):
|
||||
pass
|
||||
|
||||
|
||||
class Tile(Container):
|
||||
pass
|
||||
|
||||
|
||||
LiveTile = Tile
|
||||
|
||||
|
||||
class Flatten(Container):
|
||||
pass
|
||||
|
||||
|
||||
class AlphaMask(Container):
|
||||
pass
|
||||
|
||||
|
||||
class NearRect(Container):
|
||||
pass
|
||||
2
third_party/python/rpycdec/src/renpy/display/transform.py
vendored
Normal file
2
third_party/python/rpycdec/src/renpy/display/transform.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
class Transform:
|
||||
pass
|
||||
50
third_party/python/rpycdec/src/renpy/lexer.py
vendored
Normal file
50
third_party/python/rpycdec/src/renpy/lexer.py
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
import os
|
||||
from typing import NamedTuple
|
||||
|
||||
import renpy
|
||||
|
||||
|
||||
class SubParse(object):
|
||||
"""
|
||||
This represents the information about a subparse that can be provided to
|
||||
a creator-defined statement.
|
||||
"""
|
||||
|
||||
def __init__(self, block):
|
||||
self.block = block
|
||||
|
||||
def __repr__(self):
|
||||
if not self.block:
|
||||
return "<SubParse empty>"
|
||||
else:
|
||||
return "<SubParse {}:{}>".format(
|
||||
self.block[0].filename, self.block[0].linenumber
|
||||
)
|
||||
|
||||
|
||||
def unelide_filename(fn: str) -> str:
|
||||
fn = os.path.normpath(fn)
|
||||
|
||||
if renpy.config.alternate_unelide_path is not None:
|
||||
fn0 = os.path.join(renpy.config.alternate_unelide_path, fn)
|
||||
if os.path.exists(fn0):
|
||||
return fn0
|
||||
|
||||
fn1 = os.path.join(renpy.config.basedir, fn)
|
||||
if os.path.exists(fn1):
|
||||
return fn1
|
||||
|
||||
fn2 = os.path.join(renpy.config.renpy_base, fn)
|
||||
if os.path.exists(fn2):
|
||||
return fn2
|
||||
|
||||
return fn
|
||||
|
||||
|
||||
class GroupedLine(NamedTuple):
|
||||
# The filename the line is from.
|
||||
filename: str
|
||||
number: int
|
||||
indent: int
|
||||
text: str
|
||||
block: list["GroupedLine"]
|
||||
33
third_party/python/rpycdec/src/renpy/object.py
vendored
Normal file
33
third_party/python/rpycdec/src/renpy/object.py
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
class Object(object):
|
||||
__version__ = 0
|
||||
|
||||
nosave = []
|
||||
|
||||
def __getstate__(self):
|
||||
rv = vars(self).copy()
|
||||
|
||||
for f in self.nosave:
|
||||
if f in rv:
|
||||
del rv[f]
|
||||
|
||||
rv["__version__"] = self.__version__
|
||||
|
||||
return rv
|
||||
|
||||
# None, to prevent this from being called when unnecessary.
|
||||
after_setstate = None
|
||||
|
||||
def __setstate__(self, new_dict):
|
||||
version = new_dict.pop("__version__", 0)
|
||||
|
||||
self.__dict__.update(new_dict)
|
||||
|
||||
if version != self.__version__:
|
||||
self.after_upgrade(version) # E1101
|
||||
|
||||
if self.after_setstate:
|
||||
self.after_setstate() # E1102
|
||||
|
||||
|
||||
class Sentinel(object):
|
||||
pass
|
||||
63
third_party/python/rpycdec/src/renpy/parameter.py
vendored
Normal file
63
third_party/python/rpycdec/src/renpy/parameter.py
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
from .. import util
|
||||
|
||||
|
||||
class Signature:
|
||||
def get_code(self, **kwargs) -> str:
|
||||
rv = []
|
||||
parameters = util.attr(self, "parameters")
|
||||
if isinstance(parameters, dict):
|
||||
for name, value in parameters.items():
|
||||
rv.append(util.get_code(value, **kwargs))
|
||||
else:
|
||||
for value in parameters:
|
||||
rv.append(str(value))
|
||||
if not rv:
|
||||
return ""
|
||||
return f"({', '.join(rv)})"
|
||||
|
||||
|
||||
class Parameter:
|
||||
|
||||
(
|
||||
POSITIONAL_ONLY,
|
||||
POSITIONAL_OR_KEYWORD,
|
||||
VAR_POSITIONAL,
|
||||
KEYWORD_ONLY,
|
||||
VAR_KEYWORD,
|
||||
) = range(5)
|
||||
|
||||
def _variable_or_default(self, name, default):
|
||||
return f"{name}={default}" if default is not None else f"{name}"
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
name = util.attr(self, "name")
|
||||
kind = util.attr(self, "kind")
|
||||
default = util.attr(self, "default")
|
||||
if kind == self.POSITIONAL_ONLY:
|
||||
return self._variable_or_default(name, default)
|
||||
elif kind == self.POSITIONAL_OR_KEYWORD:
|
||||
return self._variable_or_default(name, default)
|
||||
elif kind == self.VAR_POSITIONAL:
|
||||
return f"*{name}"
|
||||
elif kind == self.KEYWORD_ONLY:
|
||||
return self._variable_or_default(name, default)
|
||||
elif kind == self.VAR_KEYWORD:
|
||||
return f"**{name}"
|
||||
else:
|
||||
raise ValueError(f"Unknown parameter kind: {kind}")
|
||||
|
||||
|
||||
class ArgumentInfo:
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
rv = []
|
||||
for i, (keyword, expression) in enumerate(self.arguments):
|
||||
if i in self.starred_indexes:
|
||||
rv.append("*" + expression)
|
||||
elif i in self.doublestarred_indexes:
|
||||
rv.append("**" + expression)
|
||||
elif keyword is not None:
|
||||
rv.append("{}={}".format(keyword, expression))
|
||||
else:
|
||||
rv.append(str(expression))
|
||||
return "(" + ", ".join(rv) + ")"
|
||||
25
third_party/python/rpycdec/src/renpy/pyanalysis.py
vendored
Normal file
25
third_party/python/rpycdec/src/renpy/pyanalysis.py
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import ast
|
||||
|
||||
# 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 Control(object):
|
||||
pass
|
||||
|
||||
|
||||
class Analysis(object):
|
||||
pass
|
||||
|
||||
|
||||
class PyAnalysis(ast.NodeVisitor):
|
||||
pass
|
||||
|
||||
|
||||
class CompilerCache(object):
|
||||
pass
|
||||
|
||||
|
||||
ccache = CompilerCache()
|
||||
82
third_party/python/rpycdec/src/renpy/python.py
vendored
Normal file
82
third_party/python/rpycdec/src/renpy/python.py
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
import ast
|
||||
import random
|
||||
from .object import Object
|
||||
|
||||
|
||||
class StoreDeleted(object):
|
||||
pass
|
||||
|
||||
|
||||
class StoreModule(object):
|
||||
pass
|
||||
|
||||
|
||||
class StoreDict(dict):
|
||||
pass
|
||||
|
||||
|
||||
class StoreBackup:
|
||||
pass
|
||||
|
||||
|
||||
class NoRollback(object):
|
||||
pass
|
||||
|
||||
|
||||
class WrapNode(ast.NodeTransformer):
|
||||
pass
|
||||
|
||||
|
||||
class CompressedList(object):
|
||||
pass
|
||||
|
||||
|
||||
class RevertableList(list):
|
||||
pass
|
||||
|
||||
|
||||
class RevertableDict(dict):
|
||||
pass
|
||||
|
||||
|
||||
class RevertableSet(set):
|
||||
def __setstate__(self, state):
|
||||
if isinstance(state, tuple):
|
||||
self.update(state[0].keys())
|
||||
else:
|
||||
self.update(state)
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
__reduce_ex__ = object.__reduce_ex__
|
||||
|
||||
|
||||
class RevertableObject(object):
|
||||
pass
|
||||
|
||||
|
||||
class RollbackRandom(random.Random):
|
||||
pass
|
||||
|
||||
|
||||
class DetRandom(random.Random):
|
||||
pass
|
||||
|
||||
|
||||
class Rollback(Object):
|
||||
pass
|
||||
|
||||
|
||||
class RollbackLog(Object):
|
||||
pass
|
||||
|
||||
|
||||
class StoreProxy(object):
|
||||
pass
|
||||
|
||||
|
||||
def py_compile(source, mode, filename="<none>", lineno=1, ast_node=False, cache=True):
|
||||
pass
|
||||
|
||||
|
||||
def py_eval_bytecode(bytecode, globals=None, locals=None): # @ReservedAssignment
|
||||
pass
|
||||
83
third_party/python/rpycdec/src/renpy/revertable.py
vendored
Normal file
83
third_party/python/rpycdec/src/renpy/revertable.py
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
import ast
|
||||
import random
|
||||
from .object import Object
|
||||
|
||||
|
||||
class StoreDeleted(object):
|
||||
pass
|
||||
|
||||
|
||||
class StoreModule(object):
|
||||
pass
|
||||
|
||||
|
||||
class StoreDict(dict):
|
||||
pass
|
||||
|
||||
|
||||
class StoreBackup:
|
||||
pass
|
||||
|
||||
|
||||
class NoRollback(object):
|
||||
pass
|
||||
|
||||
|
||||
class WrapNode(ast.NodeTransformer):
|
||||
pass
|
||||
|
||||
|
||||
class CompressedList(object):
|
||||
pass
|
||||
|
||||
|
||||
class RevertableList(list):
|
||||
pass
|
||||
|
||||
|
||||
class RevertableDict(dict):
|
||||
pass
|
||||
|
||||
|
||||
class RevertableSet(set):
|
||||
def __setstate__(self, state):
|
||||
if isinstance(state, tuple):
|
||||
self.update(state[0].keys())
|
||||
else:
|
||||
self.update(state)
|
||||
|
||||
# Required to ensure that getstate and setstate are called.
|
||||
__reduce__ = object.__reduce__
|
||||
__reduce_ex__ = object.__reduce_ex__
|
||||
|
||||
|
||||
class RevertableObject(object):
|
||||
pass
|
||||
|
||||
|
||||
class RollbackRandom(random.Random):
|
||||
pass
|
||||
|
||||
|
||||
class DetRandom(random.Random):
|
||||
pass
|
||||
|
||||
|
||||
class Rollback(Object):
|
||||
pass
|
||||
|
||||
|
||||
class RollbackLog(Object):
|
||||
pass
|
||||
|
||||
|
||||
class StoreProxy(object):
|
||||
pass
|
||||
|
||||
|
||||
def py_compile(source, mode, filename="<none>", lineno=1, ast_node=False, cache=True):
|
||||
pass
|
||||
|
||||
|
||||
def py_eval_bytecode(bytecode, globals=None, locals=None): # @ReservedAssignment
|
||||
pass
|
||||
12
third_party/python/rpycdec/src/renpy/script.py
vendored
Normal file
12
third_party/python/rpycdec/src/renpy/script.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
def collapse_stmts(stmts):
|
||||
"""
|
||||
Returns a flat list containing every statement in the tree
|
||||
stmts.
|
||||
"""
|
||||
|
||||
rv = []
|
||||
|
||||
for i in stmts:
|
||||
i.get_children(rv.append)
|
||||
|
||||
return rv
|
||||
323
third_party/python/rpycdec/src/renpy/sl2/slast.py
vendored
Normal file
323
third_party/python/rpycdec/src/renpy/sl2/slast.py
vendored
Normal file
@@ -0,0 +1,323 @@
|
||||
import logging
|
||||
from .. import ast, astsupport, ui, util
|
||||
|
||||
|
||||
class SLContext(ui.Addable):
|
||||
pass
|
||||
|
||||
|
||||
class SLNode(object):
|
||||
pass
|
||||
|
||||
|
||||
class SLBlock(SLNode):
|
||||
"""
|
||||
foo bar
|
||||
baz 1
|
||||
|
||||
expression
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
rv = []
|
||||
if self.keyword:
|
||||
rv.append(util.get_code_properties(self.keyword, newline=True))
|
||||
if self.children:
|
||||
rv.append(util.get_code(self.children, **kwargs))
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class SLCache(object):
|
||||
pass
|
||||
|
||||
|
||||
class SLDisplayable(SLBlock):
|
||||
"""
|
||||
`displayable`
|
||||
A function that, when called with the positional and keyword
|
||||
arguments, causes the displayable to be displayed.
|
||||
|
||||
`scope`
|
||||
If true, the scope is supplied as an argument to the displayable.
|
||||
|
||||
`child_or_fixed`
|
||||
If true and the number of children of this displayable is not one,
|
||||
the children are added to a Fixed, and the Fixed is added to the
|
||||
displayable.
|
||||
|
||||
`style`
|
||||
The base name of the main style.
|
||||
|
||||
`pass_context`
|
||||
If given, the context is passed in as the first positonal argument
|
||||
of the displayable.
|
||||
|
||||
`imagemap`
|
||||
True if this is an imagemap, and should be handled as one.
|
||||
|
||||
`hotspot`
|
||||
True if this is a hotspot that depends on the imagemap it was
|
||||
first displayed with.
|
||||
|
||||
`replaces`
|
||||
True if the object this displayable replaces should be
|
||||
passed to it.
|
||||
|
||||
`default_keywords`
|
||||
The default keyword arguments to supply to the displayable.
|
||||
|
||||
`variable`
|
||||
A variable that the main displayable is assigned to.
|
||||
|
||||
https://www.renpy.org/doc/html/screens.html#add-statement
|
||||
https://www.renpy.org/doc/html/screens.html#use
|
||||
https://www.renpy.org/doc/html/screens.html#has
|
||||
https://www.renpy.org/doc/html/screens.html#imagebutton
|
||||
"""
|
||||
|
||||
def get_name(self) -> str:
|
||||
# higher version use style instead of name
|
||||
name = getattr(self, "name", None)
|
||||
displayable = getattr(self, "displayable", None)
|
||||
style = getattr(self, "style", None)
|
||||
start = ""
|
||||
if name:
|
||||
start = name
|
||||
elif displayable:
|
||||
# displayable function named sl2xxx like sl2vbar , sl2viewport
|
||||
displable_name = displayable.__name__.lower()
|
||||
if displable_name.startswith("sl2"):
|
||||
start = displable_name.replace("sl2", "")
|
||||
elif displable_name == "onevent":
|
||||
start = "on"
|
||||
else:
|
||||
start = displable_name.replace("_", "")
|
||||
if style:
|
||||
match start:
|
||||
case "multibox":
|
||||
start = style
|
||||
case "window":
|
||||
start = style
|
||||
if not start:
|
||||
logging.warning(
|
||||
"warning: displayable not found, use textbutton instead, you may need fix it manually"
|
||||
)
|
||||
start = "textbutton"
|
||||
return start
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
# higher version use style instead of name
|
||||
start = self.get_name()
|
||||
if not start:
|
||||
raise Exception("displayable not found")
|
||||
|
||||
variable = util.attr(self, "variable")
|
||||
if variable:
|
||||
start += f" as {variable}"
|
||||
|
||||
tag = util.attr(self, "tag")
|
||||
if tag:
|
||||
start += f" tag {tag}"
|
||||
|
||||
positional = util.attr(self, "positional")
|
||||
if positional:
|
||||
if isinstance(positional, list):
|
||||
start += " " + " ".join(util.get_code(p, **kwargs) for p in positional)
|
||||
else:
|
||||
start += " " + util.get_code(positional, **kwargs)
|
||||
# keywords
|
||||
keyword = util.attr(self, "keyword")
|
||||
children = util.attr(self, "children")
|
||||
if not children:
|
||||
if keyword:
|
||||
start += f" {util.get_code_properties(keyword)}"
|
||||
return start
|
||||
# children
|
||||
rv = [start + ":"]
|
||||
if keyword:
|
||||
rv.append(util.indent(util.get_code_properties(keyword, newline=True)))
|
||||
rv.append(util.indent(util.get_code(children, **kwargs)))
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class SLIf(SLNode):
|
||||
def get_code(self, **kwargs) -> str:
|
||||
rv = []
|
||||
for index, (cond, body) in enumerate(self.entries):
|
||||
body_code = util.get_code(body, **kwargs)
|
||||
if not body_code:
|
||||
body_code = "pass"
|
||||
body_code = util.indent(body_code)
|
||||
if index == 0:
|
||||
rv.append(f"if {cond}:\n{body_code}")
|
||||
continue
|
||||
if cond and cond != "True":
|
||||
rv.append(f"elif {cond}:\n{body_code}")
|
||||
else:
|
||||
rv.append(f"else:\n{body_code}")
|
||||
break
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class SLShowIf(SLNode):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/screens.html#showif-statement
|
||||
|
||||
showif n == 3:
|
||||
text "Three" size 100 at cd_transform
|
||||
elif n == 2:
|
||||
text "Two" size 100 at cd_transform
|
||||
elif n == 1:
|
||||
text "One" size 100 at cd_transform
|
||||
else:
|
||||
text "Liftoff!" size 100 at cd_transform
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
rv = []
|
||||
for index, (cond, body) in enumerate(self.entries):
|
||||
body_code = util.get_code(body, **kwargs)
|
||||
if not body_code:
|
||||
body_code = "pass"
|
||||
body_code = util.indent(body_code)
|
||||
if index == 0:
|
||||
rv.append(f"showif {cond}:\n{body_code}")
|
||||
continue
|
||||
if cond and cond != "True":
|
||||
rv.append(f"elif {cond}:\n{body_code}")
|
||||
else:
|
||||
rv.append(f"else:\n{body_code}")
|
||||
break
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class SLFor(SLBlock):
|
||||
pass
|
||||
|
||||
"""
|
||||
screen five_buttons():
|
||||
vbox:
|
||||
for i, numeral index numeral in enumerate(numerals):
|
||||
textbutton numeral action Return(i + 1)
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
rv = [f"for {self.variable} in {self.expression}:"]
|
||||
children = util.attr(self, "children")
|
||||
if not children:
|
||||
rv.append(util.indent("pass"))
|
||||
else:
|
||||
rv.append(util.indent(util.get_code(children, **kwargs)))
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class SLPython(SLNode):
|
||||
def get_code(self, **kwargs) -> str:
|
||||
inner_code = util.get_code(self.code, **kwargs)
|
||||
if not inner_code:
|
||||
return ""
|
||||
if len(inner_code.splitlines()) == 1:
|
||||
return f"$ {inner_code}"
|
||||
return f"python:\n{util.indent(inner_code)}"
|
||||
|
||||
|
||||
class SLPass(SLNode):
|
||||
pass
|
||||
|
||||
|
||||
class SLDefault(SLNode):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/python.html#default-statement
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
return f"default {self.variable} = {self.expression}"
|
||||
|
||||
|
||||
class SLUse(SLNode):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/screens.html#use
|
||||
|
||||
use [expression EXPR pass|WORD] ARGUMENTS [id EXPR][:]
|
||||
BLOCK
|
||||
"""
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
start = "use"
|
||||
target = util.attr(self, "target")
|
||||
if target:
|
||||
if isinstance(target, ast.PyExpr):
|
||||
start += f" expression {util.get_code(target, **kwargs)} pass "
|
||||
elif isinstance(target, astsupport.PyExpr):
|
||||
start += f" expression {util.get_code(target, **kwargs)} pass "
|
||||
else:
|
||||
start += f" {target}"
|
||||
if self.args:
|
||||
start += f"{util.get_code(self.args, **kwargs)}"
|
||||
if self.block:
|
||||
return util.label_code(start, self.block, **kwargs)
|
||||
if self.ast:
|
||||
return util.label_code(start, self.ast, **kwargs)
|
||||
return start
|
||||
|
||||
|
||||
# https://www.renpy.org/doc/html/screens.html#use-and-transclude
|
||||
"""
|
||||
screen movable_frame(pos):
|
||||
drag:
|
||||
pos pos
|
||||
|
||||
frame:
|
||||
background Frame("movable_frame.png", 10, 10)
|
||||
top_padding 20
|
||||
|
||||
transclude
|
||||
"""
|
||||
|
||||
|
||||
class SLTransclude(SLNode):
|
||||
def get_code(self, **kwargs) -> str:
|
||||
return "transclude"
|
||||
|
||||
|
||||
class SLScreen(SLBlock):
|
||||
"""
|
||||
https://www.renpy.org/doc/html/screens.html#screen-statement
|
||||
"""
|
||||
|
||||
parameters: ast.ParameterInfo
|
||||
keyword: list
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
start = "screen"
|
||||
if self.name:
|
||||
start += f" {self.name}"
|
||||
if self.parameters:
|
||||
start += f"{util.get_code(self.parameters, **kwargs)}"
|
||||
|
||||
properties = self.keyword.copy()
|
||||
tag = util.attr(self, "tag")
|
||||
if tag:
|
||||
properties.append(("tag", tag))
|
||||
|
||||
rv = [start + ":"]
|
||||
# keyword
|
||||
if properties:
|
||||
rv.append(util.indent(util.get_code_properties(properties, newline=True)))
|
||||
rv.append("") # add a newline after keyword
|
||||
# children
|
||||
children = util.attr(self, "children")
|
||||
if children:
|
||||
rv.append(util.indent(util.get_code(children, **kwargs)))
|
||||
if len(rv) == 1:
|
||||
rv.append("pass")
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
class ScreenCache(object):
|
||||
pass
|
||||
|
||||
|
||||
class SLContinue(SLNode):
|
||||
def get_code(self, **kwargs) -> str:
|
||||
return "continue"
|
||||
25
third_party/python/rpycdec/src/renpy/sl2/sldisplayables.py
vendored
Normal file
25
third_party/python/rpycdec/src/renpy/sl2/sldisplayables.py
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
from ..display import layout
|
||||
|
||||
|
||||
class ShowIf(layout.Container):
|
||||
pass
|
||||
|
||||
|
||||
def sl2vbar(context=None, **properties):
|
||||
pass
|
||||
|
||||
|
||||
def sl2viewport(context=None, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def sl2vpgrid(context=None, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def sl2add(d, replaces=None, scope=None, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def sl2bar(context=None, **properties):
|
||||
pass
|
||||
0
third_party/python/rpycdec/src/renpy/test/__init__.py
vendored
Normal file
0
third_party/python/rpycdec/src/renpy/test/__init__.py
vendored
Normal file
2
third_party/python/rpycdec/src/renpy/text/extras.py
vendored
Normal file
2
third_party/python/rpycdec/src/renpy/text/extras.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
class ParameterizedText(object):
|
||||
pass
|
||||
26
third_party/python/rpycdec/src/renpy/text/font.py
vendored
Normal file
26
third_party/python/rpycdec/src/renpy/text/font.py
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
class ImageFont(object):
|
||||
pass
|
||||
|
||||
|
||||
class FontGroup(object):
|
||||
pass
|
||||
|
||||
|
||||
class ScaledImageFont(ImageFont):
|
||||
pass
|
||||
|
||||
|
||||
class BMFont(ImageFont):
|
||||
pass
|
||||
|
||||
|
||||
class MudgeFont(ImageFont):
|
||||
pass
|
||||
|
||||
|
||||
class SFont(ImageFont):
|
||||
pass
|
||||
|
||||
|
||||
class ImageFont(object):
|
||||
pass
|
||||
33
third_party/python/rpycdec/src/renpy/text/text.py
vendored
Normal file
33
third_party/python/rpycdec/src/renpy/text/text.py
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
from ..display import core
|
||||
|
||||
|
||||
class Blit(object):
|
||||
pass
|
||||
|
||||
|
||||
class DrawInfo(object):
|
||||
pass
|
||||
|
||||
|
||||
class TextSegment(object):
|
||||
pass
|
||||
|
||||
|
||||
class SpaceSegment(object):
|
||||
pass
|
||||
|
||||
|
||||
class DisplayableSegment(object):
|
||||
pass
|
||||
|
||||
|
||||
class FlagSegment(object):
|
||||
pass
|
||||
|
||||
|
||||
class Layout(object):
|
||||
pass
|
||||
|
||||
|
||||
class Text(core.Displayable):
|
||||
pass
|
||||
28
third_party/python/rpycdec/src/renpy/translation/__init__.py
vendored
Normal file
28
third_party/python/rpycdec/src/renpy/translation/__init__.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import re
|
||||
|
||||
|
||||
def encode_say_string(s):
|
||||
"""
|
||||
Encodes a string in the format used by Ren'Py say statements.
|
||||
"""
|
||||
|
||||
s = s.replace("\\", "\\\\")
|
||||
s = s.replace("\n", "\\n")
|
||||
s = s.replace('"', '\\"')
|
||||
s = re.sub(r"(?<= ) ", "\\ ", s)
|
||||
|
||||
return '"' + s + '"'
|
||||
|
||||
|
||||
def quote_unicode(s):
|
||||
s = s.replace("\\", "\\\\")
|
||||
s = s.replace('"', '\\"')
|
||||
s = s.replace("\a", "\\a")
|
||||
s = s.replace("\b", "\\b")
|
||||
s = s.replace("\f", "\\f")
|
||||
s = s.replace("\n", "\\n")
|
||||
s = s.replace("\r", "\\r")
|
||||
s = s.replace("\t", "\\t")
|
||||
s = s.replace("\v", "\\v")
|
||||
|
||||
return s
|
||||
27
third_party/python/rpycdec/src/renpy/translation/generation.py
vendored
Normal file
27
third_party/python/rpycdec/src/renpy/translation/generation.py
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import os
|
||||
|
||||
import renpy
|
||||
|
||||
|
||||
def shorten_filename(filename):
|
||||
"""
|
||||
Shortens a file name. Returns the shortened filename, and a flag that says
|
||||
if the filename is in the common directory.
|
||||
"""
|
||||
|
||||
commondir = os.path.normpath(renpy.config.commondir) # type: ignore
|
||||
gamedir = os.path.normpath(renpy.config.gamedir)
|
||||
|
||||
if filename.startswith(commondir):
|
||||
fn = os.path.relpath(filename, commondir)
|
||||
common = True
|
||||
|
||||
elif filename.startswith(gamedir):
|
||||
fn = os.path.relpath(filename, gamedir)
|
||||
common = False
|
||||
|
||||
else:
|
||||
fn = os.path.basename(filename)
|
||||
common = False
|
||||
|
||||
return fn, common
|
||||
9
third_party/python/rpycdec/src/renpy/translation/scanstrings.py
vendored
Normal file
9
third_party/python/rpycdec/src/renpy/translation/scanstrings.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
STRING_RE = r"""(?x)
|
||||
\b(?:_|__|___|_p)
|
||||
\s*(\((?:[\s\\\n]*[uU]?(?:
|
||||
\"\"\"(?:\\.|\\\n|\"{1,2}|[^\\"])*?\"\"\"
|
||||
|'''(?:\\.|\\\n|\'{1,2}|[^\\'])*?'''
|
||||
|"(?:\\.|\\\n|[^\\"])*"
|
||||
|'(?:\\.|\\\n|[^\\'])*'
|
||||
))+\s*\))
|
||||
"""
|
||||
96
third_party/python/rpycdec/src/renpy/ui.py
vendored
Normal file
96
third_party/python/rpycdec/src/renpy/ui.py
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
from .object import Object
|
||||
|
||||
|
||||
class Action(Object):
|
||||
pass
|
||||
|
||||
|
||||
class BarValue(Object):
|
||||
pass
|
||||
|
||||
|
||||
class Addable(object):
|
||||
pass
|
||||
|
||||
|
||||
class Layer(Addable):
|
||||
pass
|
||||
|
||||
|
||||
class Many(Addable):
|
||||
pass
|
||||
|
||||
|
||||
class One(Addable):
|
||||
pass
|
||||
|
||||
|
||||
class Detached(Addable):
|
||||
pass
|
||||
|
||||
|
||||
class ChildOrFixed(Addable):
|
||||
pass
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
pass
|
||||
|
||||
|
||||
def _hotspot(spot, style="hotspot", **properties):
|
||||
pass
|
||||
|
||||
|
||||
def _imagebutton(
|
||||
idle_image=None,
|
||||
hover_image=None,
|
||||
insensitive_image=None,
|
||||
activate_image=None,
|
||||
selected_idle_image=None,
|
||||
selected_hover_image=None,
|
||||
selected_insensitive_image=None,
|
||||
selected_activate_image=None,
|
||||
idle=None,
|
||||
hover=None,
|
||||
insensitive=None,
|
||||
selected_idle=None,
|
||||
selected_hover=None,
|
||||
selected_insensitive=None,
|
||||
image_style=None,
|
||||
auto=None,
|
||||
**properties,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def _imagemap(
|
||||
ground,
|
||||
selected,
|
||||
hotspots,
|
||||
unselected=None,
|
||||
overlays=False,
|
||||
style="imagemap",
|
||||
mouse="imagemap",
|
||||
with_none=None,
|
||||
**properties,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def _textbutton(
|
||||
label,
|
||||
clicked=None,
|
||||
style=None,
|
||||
text_style=None,
|
||||
substitute=True,
|
||||
scope=None,
|
||||
**kwargs,
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
def _key(key, action=None, activate_sound=None):
|
||||
pass
|
||||
|
||||
|
||||
def _label(label, style=None, text_style=None, substitute=True, scope=None, **kwargs):
|
||||
pass
|
||||
250
third_party/python/rpycdec/src/renpy/util.py
vendored
Normal file
250
third_party/python/rpycdec/src/renpy/util.py
vendored
Normal file
@@ -0,0 +1,250 @@
|
||||
from . import ast
|
||||
from collections import deque
|
||||
|
||||
IDENT_CHAR = " "
|
||||
|
||||
|
||||
def indent(code: str, level: int = 1) -> str:
|
||||
"""
|
||||
Indent each line of code by a specified level.
|
||||
|
||||
Args:
|
||||
code: The string to indent
|
||||
level: Number of indentation levels to apply (default: 1)
|
||||
|
||||
Returns:
|
||||
The indented string
|
||||
"""
|
||||
if not code:
|
||||
return ""
|
||||
indent_str = IDENT_CHAR * level
|
||||
# add indentation to each non-empty line
|
||||
# keep empty lines as they are
|
||||
rv = [indent_str + line if line.strip() else line for line in code.split("\n")]
|
||||
return "\n".join(rv)
|
||||
|
||||
|
||||
def get_code_properties(props: tuple | dict, newline: bool = False, **kwargs) -> str:
|
||||
"""
|
||||
:param keyword: tuple | dict
|
||||
:param newline: bool
|
||||
:return: str
|
||||
|
||||
>>> get_code_properties((("a", 1), ("b", 2)))
|
||||
a 1 b 2
|
||||
>>> get_code_properties((("a", 1), (None, b)), newline=True)
|
||||
a 1
|
||||
b
|
||||
>>> get_code_properties({"a": 1, "b": None})
|
||||
a 1
|
||||
b None
|
||||
"""
|
||||
list = []
|
||||
if isinstance(props, dict):
|
||||
props = props.items()
|
||||
for prop in props:
|
||||
if isinstance(prop, tuple) and len(prop) == 2:
|
||||
key, value = prop[0], prop[1]
|
||||
valstr = get_code(value, **kwargs)
|
||||
if not valstr:
|
||||
list.append(f"{key}")
|
||||
else:
|
||||
list.append(f"{key} {valstr}")
|
||||
continue
|
||||
else:
|
||||
prop_str = " ".join([str(x) for x in prop])
|
||||
if not prop_str:
|
||||
continue
|
||||
list.append(prop_str)
|
||||
return ("\n" if newline else " ").join(list)
|
||||
|
||||
|
||||
def get_code(node, **kwargs) -> str:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
node : ast.Node
|
||||
kwargs : dict
|
||||
indent : int
|
||||
space indent level
|
||||
modifier : Callable[[ast.Node], ast.Node]
|
||||
modify node before get code
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
generated code
|
||||
|
||||
Raises
|
||||
------
|
||||
NotImplementedError
|
||||
if node type is not implemented or some attributes unable to handle.
|
||||
|
||||
"""
|
||||
|
||||
if isinstance(node, str):
|
||||
return node
|
||||
if isinstance(node, list):
|
||||
rv = []
|
||||
items = deque(node)
|
||||
while items:
|
||||
item = items.popleft()
|
||||
|
||||
# [Say|UserStatement?, Menu]
|
||||
if isinstance(item, ast.Say) or isinstance(item, ast.UserStatement):
|
||||
next = items[0] if items else None
|
||||
if isinstance(next, ast.Menu) and attr(next, "statement_start") == item:
|
||||
menu = items.popleft()
|
||||
call_kwargs = kwargs.copy()
|
||||
call_kwargs["menu_say"] = item
|
||||
rv.append(get_code(menu, **call_kwargs))
|
||||
continue
|
||||
|
||||
# [Label, Say|UserStatement?, Menu]
|
||||
if isinstance(item, ast.Label):
|
||||
next = items[0] if items else None
|
||||
# [Label, Say|UserStatement, Menu]
|
||||
if isinstance(next, ast.Say) or isinstance(next, ast.UserStatement):
|
||||
next_next = items[1] if len(items) > 1 else None
|
||||
if (
|
||||
isinstance(next_next, ast.Menu)
|
||||
and attr(next_next, "statement_start") == item
|
||||
):
|
||||
say = items.popleft()
|
||||
menu = items.popleft()
|
||||
call_kwargs = kwargs.copy()
|
||||
call_kwargs["menu_label"] = item
|
||||
call_kwargs["menu_say"] = say
|
||||
rv.append(get_code(menu, **call_kwargs))
|
||||
continue
|
||||
# [Label, Menu]
|
||||
if isinstance(next, ast.Menu) and attr(next, "statement_start") == item:
|
||||
menu = items.popleft()
|
||||
call_kwargs = kwargs.copy()
|
||||
call_kwargs["menu_label"] = item
|
||||
rv.append(get_code(menu, **call_kwargs))
|
||||
continue
|
||||
|
||||
if isinstance(item, ast.With):
|
||||
# some node quoted by two ast.With if has with suffix expr, the sdk snippet:
|
||||
#
|
||||
# renpy.parser.parse_with(node):
|
||||
# if not "with":
|
||||
# return node
|
||||
# expr = simple_expression
|
||||
# return [ast.With(loc, "None", paired=expr), node, ast.With(loc, expr)]
|
||||
#
|
||||
# - ast.Scene
|
||||
# - ast.Show
|
||||
# - ast.Hide
|
||||
expr, paired = attr(item, "expr"), attr(item, "paired")
|
||||
if (not expr or expr == "None") and paired:
|
||||
next_next = items[1] if len(items) > 1 else None
|
||||
if (
|
||||
isinstance(next_next, ast.With)
|
||||
and attr(next_next, "expr") == paired
|
||||
):
|
||||
node = items.popleft()
|
||||
close_with = items.popleft()
|
||||
call_kwargs = kwargs.copy()
|
||||
call_kwargs.update({"with_expr": attr(close_with, "expr")})
|
||||
rv.append(get_code(node, **call_kwargs))
|
||||
continue
|
||||
|
||||
if isinstance(item, ast.Call):
|
||||
# call = [Call, Label?, Pass]
|
||||
from_label = None
|
||||
# try to pop next Label|Pass
|
||||
next = items.popleft() if items else None
|
||||
if isinstance(next, ast.Label):
|
||||
from_label = next
|
||||
# pop Pass
|
||||
items.popleft() if items else None
|
||||
|
||||
if from_label is not None:
|
||||
call_kwargs = kwargs.copy()
|
||||
call_kwargs["from_label"] = from_label
|
||||
rv.append(get_code(item, **call_kwargs))
|
||||
continue
|
||||
|
||||
if isinstance(item, ast.Return):
|
||||
if (
|
||||
# it cant tell out an anto appended Return or an empty one
|
||||
# currently we must not ignore
|
||||
False
|
||||
and len(node) > 1
|
||||
and not items
|
||||
and not attr(item, "expression")
|
||||
# omit the return after While may cause unexpected behavior
|
||||
and not any(isinstance(n, ast.While) for n in node)
|
||||
):
|
||||
# ignore the return
|
||||
continue
|
||||
|
||||
# if all left items are returns
|
||||
if all(
|
||||
isinstance(n, ast.Return) and not attr(n, "expression")
|
||||
for n in items
|
||||
):
|
||||
# ignore all returns
|
||||
rv.append(get_code(item, **kwargs))
|
||||
break
|
||||
|
||||
# if isinstance(item, ast.Init):
|
||||
# if item.priority == 500:
|
||||
# if len(item.block) == 1 and isinstance(item.block[0], ast.Image):
|
||||
# rv.append(get_code(item.block[0]))
|
||||
# continue
|
||||
|
||||
rv.append(get_code(item, **kwargs))
|
||||
return "\n".join(rv)
|
||||
|
||||
# modify node before get code
|
||||
modifier = kwargs.get("modifier")
|
||||
if modifier:
|
||||
modifier(node, **kwargs)
|
||||
return node.get_code(**kwargs)
|
||||
|
||||
|
||||
def get_block_code(node, **kwargs) -> str:
|
||||
"""
|
||||
https://www.renpy.org/doc/html/layeredimage.html#layeredimage
|
||||
"""
|
||||
if isinstance(node, list):
|
||||
return "\n".join(map(lambda x: get_block_code(x, **kwargs), node))
|
||||
lines = []
|
||||
if isinstance(node, tuple):
|
||||
if len(node) == 5:
|
||||
_, _, _, code, block = node
|
||||
elif len(node) == 4:
|
||||
_, _, code, block = node
|
||||
else:
|
||||
raise NotImplementedError
|
||||
lines.append(code)
|
||||
lines.append(indent(get_block_code(block, **kwargs)))
|
||||
else:
|
||||
raise NotImplementedError
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def attr(item, key: str) -> str:
|
||||
if isinstance(item, dict):
|
||||
return item.get(key, None)
|
||||
if hasattr(item, key):
|
||||
return getattr(item, key)
|
||||
return None
|
||||
|
||||
|
||||
def label_code(label: str, child, **kwargs) -> str:
|
||||
"""
|
||||
return a code block with a label
|
||||
example:
|
||||
>>> label_getcode("label foo", None)
|
||||
"label foo"
|
||||
>>> label_getcode("label foo", Expression("bar"))
|
||||
"label foo:
|
||||
bar"
|
||||
"""
|
||||
if not child:
|
||||
return label
|
||||
return f"{label}:\n{indent(get_code(child, **kwargs))}"
|
||||
5
third_party/python/rpycdec/src/rpycdec/__init__.py
vendored
Normal file
5
third_party/python/rpycdec/src/rpycdec/__init__.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from .cli import main
|
||||
from .decompile import decompile, decompile_file
|
||||
from .rpa import create_rpa, extract_rpa
|
||||
from .save import dump_save_info, extract_save, generate_new_key, restore_save
|
||||
from .translate import extract_translations
|
||||
3
third_party/python/rpycdec/src/rpycdec/__main__.py
vendored
Normal file
3
third_party/python/rpycdec/src/rpycdec/__main__.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
from .cli import main
|
||||
|
||||
main()
|
||||
193
third_party/python/rpycdec/src/rpycdec/apk.py
vendored
Normal file
193
third_party/python/rpycdec/src/rpycdec/apk.py
vendored
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
APK extraction module for Ren'Py games.
|
||||
|
||||
This module provides functionality to extract Ren'Py game resources from Android APK files.
|
||||
The extraction is done in a streaming fashion to minimize disk I/O and temporary file usage.
|
||||
"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
import tarfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from rpycdec.utils import safe_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PrependedStream(io.RawIOBase):
|
||||
"""Wrapper to prepend already-read bytes back to stream."""
|
||||
|
||||
def __init__(self, prepend_data, stream):
|
||||
self.prepend_data = prepend_data
|
||||
self.stream = stream
|
||||
self.prepend_pos = 0
|
||||
|
||||
def readable(self):
|
||||
return True
|
||||
|
||||
def read(self, size=-1):
|
||||
if size == -1:
|
||||
result = self.prepend_data[self.prepend_pos :] + self.stream.read()
|
||||
self.prepend_pos = len(self.prepend_data)
|
||||
return result
|
||||
|
||||
result = b""
|
||||
# First read from prepended data
|
||||
if self.prepend_pos < len(self.prepend_data):
|
||||
take = min(size, len(self.prepend_data) - self.prepend_pos)
|
||||
result = self.prepend_data[self.prepend_pos : self.prepend_pos + take]
|
||||
self.prepend_pos += take
|
||||
size -= take
|
||||
|
||||
# Then read from underlying stream
|
||||
if size > 0:
|
||||
result += self.stream.read(size)
|
||||
|
||||
return result
|
||||
|
||||
def readinto(self, b):
|
||||
data = self.read(len(b))
|
||||
b[: len(data)] = data
|
||||
return len(data)
|
||||
|
||||
|
||||
def extract_apk(
|
||||
apk_path: str,
|
||||
output_dir: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Extract Ren'Py game resources from an Android APK file.
|
||||
|
||||
This function directly extracts files from the APK (ZIP) archive,
|
||||
renaming them on-the-fly to remove 'x-' prefixes and organizing
|
||||
them into a standard Ren'Py structure. The private.mp3 archive
|
||||
is extracted in-memory without writing to disk first.
|
||||
|
||||
Args:
|
||||
apk_path: Path to the APK file
|
||||
output_dir: Output directory (default: APK filename without extension)
|
||||
|
||||
Returns:
|
||||
True if extraction was successful, False otherwise
|
||||
"""
|
||||
apk_file = Path(apk_path)
|
||||
|
||||
if not apk_file.exists():
|
||||
logger.error(f"APK file not found: {apk_path}")
|
||||
return False
|
||||
|
||||
if not zipfile.is_zipfile(apk_path):
|
||||
logger.error(f"File is not a valid APK/ZIP file: {apk_path}")
|
||||
return False
|
||||
|
||||
# Determine output directory
|
||||
if output_dir is None:
|
||||
output_path = apk_file.parent / apk_file.stem
|
||||
else:
|
||||
output_path = Path(output_dir)
|
||||
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(f"Extracting APK: {apk_path}")
|
||||
|
||||
with zipfile.ZipFile(apk_path, "r") as apk:
|
||||
# Get all relevant files (assets and lib)
|
||||
target_files = [f for f in apk.namelist() if f.startswith(("assets/", "lib/"))]
|
||||
|
||||
if not target_files:
|
||||
logger.warning("No assets or lib directory found in APK")
|
||||
return False
|
||||
|
||||
private_mp3_path = None
|
||||
extracted_count = 0
|
||||
|
||||
for file_path in target_files:
|
||||
# Skip directories
|
||||
if file_path.endswith("/"):
|
||||
continue
|
||||
|
||||
if file_path.startswith("assets/"):
|
||||
# Remove 'assets/' prefix
|
||||
relative_path = file_path[7:] # len('assets/') = 7
|
||||
|
||||
# Handle private.mp3 specially - remember path for later streaming extraction
|
||||
if relative_path == "private.mp3":
|
||||
logger.info("Found private.mp3, will extract via streaming...")
|
||||
private_mp3_path = file_path
|
||||
continue
|
||||
|
||||
# Process path: remove x- prefix and apply mappings
|
||||
path_parts = relative_path.split("/")
|
||||
|
||||
# Process all path parts to remove x- prefix
|
||||
for i in range(len(path_parts)):
|
||||
# Remove x- prefix from all parts
|
||||
if path_parts[i].startswith("x-"):
|
||||
path_parts[i] = path_parts[i][2:] # Remove 'x-' prefix
|
||||
|
||||
# Reconstruct path
|
||||
dest_path = output_path / "/".join(path_parts)
|
||||
|
||||
elif file_path.startswith("lib/"):
|
||||
# User request: extract lib to output/lib/android/
|
||||
rel_lib_path = file_path[4:] # len('lib/') = 4
|
||||
dest_path = output_path / "lib" / "android" / rel_lib_path
|
||||
|
||||
else:
|
||||
continue
|
||||
|
||||
# Create parent directories
|
||||
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Extract file directly
|
||||
with apk.open(file_path) as src, open(dest_path, "wb") as dst:
|
||||
dst.write(src.read())
|
||||
|
||||
extracted_count += 1
|
||||
if extracted_count % 100 == 0:
|
||||
logger.info(f"Extracted {extracted_count} files...")
|
||||
|
||||
logger.info(f"Extracted {extracted_count} files")
|
||||
|
||||
# Extract private.mp3 via streaming if found
|
||||
if private_mp3_path:
|
||||
logger.info("Extracting files from private.mp3 archive...")
|
||||
# Extract directly to output root (same level as game/)
|
||||
private_output = output_path
|
||||
private_output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with apk.open(private_mp3_path) as private_stream:
|
||||
# Read magic bytes to detect gzip format
|
||||
magic = private_stream.read(2)
|
||||
|
||||
if not magic.startswith(b"\x1f\x8b"):
|
||||
logger.warning(
|
||||
"private.mp3 is not gzip format, skipping extraction"
|
||||
)
|
||||
else:
|
||||
# Create buffered stream with magic bytes prepended
|
||||
wrapped_stream = io.BufferedReader(
|
||||
PrependedStream(magic, private_stream)
|
||||
)
|
||||
|
||||
with tarfile.open(fileobj=wrapped_stream, mode="r:gz") as tar:
|
||||
# Path traversal protection
|
||||
for member in tar.getmembers():
|
||||
try:
|
||||
safe_path(str(private_output), member.name)
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
"Skipping path traversal attempt in tar: %s",
|
||||
member.name,
|
||||
)
|
||||
continue
|
||||
tar.extract(member, path=private_output)
|
||||
logger.info(
|
||||
f"Extracted {len(tar.getmembers())} files from private.mp3"
|
||||
)
|
||||
|
||||
logger.info(f"Successfully extracted game to: {output_path}")
|
||||
return True
|
||||
317
third_party/python/rpycdec/src/rpycdec/cli.py
vendored
Normal file
317
third_party/python/rpycdec/src/rpycdec/cli.py
vendored
Normal file
@@ -0,0 +1,317 @@
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from rpycdec.apk import extract_apk
|
||||
from rpycdec.decompile import decompile
|
||||
from rpycdec.rpa import create_rpa, extract_rpa
|
||||
from rpycdec.save import dump_save_info, extract_save, generate_new_key, restore_save
|
||||
from rpycdec.translate import extract_translations
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def regular_expression(value: str) -> str:
|
||||
"""Validate a command-line regular expression."""
|
||||
try:
|
||||
re.compile(value)
|
||||
except re.error as exc:
|
||||
raise argparse.ArgumentTypeError(f"invalid regular expression: {exc}") from exc
|
||||
return value
|
||||
|
||||
|
||||
def non_empty_suffix(value: str) -> str:
|
||||
"""Validate a command-line file suffix."""
|
||||
if not value:
|
||||
raise argparse.ArgumentTypeError("suffix must not be empty")
|
||||
return value
|
||||
|
||||
|
||||
def decompile_files(srcs: list[str], **kwargs):
|
||||
"""
|
||||
decompile rpyc file or directory.
|
||||
"""
|
||||
for src in srcs:
|
||||
decompile(
|
||||
src,
|
||||
output_path=kwargs.get("output"),
|
||||
dis=kwargs.get("disassemble", False),
|
||||
)
|
||||
|
||||
|
||||
def extract_rpa_files(srcs: list[str], **kwargs):
|
||||
"""
|
||||
extract rpa archive.
|
||||
"""
|
||||
for src in srcs:
|
||||
with open(src, "rb") as f:
|
||||
output_path = kwargs.get("output") or os.path.dirname(src)
|
||||
extract_rpa(
|
||||
f,
|
||||
output_dir=output_path,
|
||||
expressions=kwargs.get("expressions"),
|
||||
suffixes=kwargs.get("suffixes"),
|
||||
)
|
||||
|
||||
|
||||
def create_rpa_archive(srcs: list[str], output: str, **kwargs):
|
||||
"""
|
||||
create rpa archive.
|
||||
"""
|
||||
create_rpa(
|
||||
output,
|
||||
srcs,
|
||||
base_dir=kwargs.get("base"),
|
||||
)
|
||||
|
||||
|
||||
def run_extract_translations(
|
||||
srcs: list[str], language: str, output: str | None = None, **kwargs
|
||||
):
|
||||
"""
|
||||
extract translations from rpy files.
|
||||
"""
|
||||
for src in srcs:
|
||||
# Default output to current directory, extract_translations will create tl/{language}/
|
||||
dest = output or "."
|
||||
extract_translations(
|
||||
game_dir=src,
|
||||
output_dir=dest,
|
||||
language=language,
|
||||
include_dialogues=kwargs.get("include_dialogues", True),
|
||||
include_strings=kwargs.get("include_strings", True),
|
||||
empty_translation=kwargs.get("empty_translation", False),
|
||||
)
|
||||
|
||||
|
||||
def extract_game_from_apk(apk_path: str, **kwargs):
|
||||
"""
|
||||
Extract Ren'Py game from Android APK file.
|
||||
"""
|
||||
extract_apk(
|
||||
apk_path=apk_path,
|
||||
output_dir=kwargs.get("output"),
|
||||
)
|
||||
|
||||
|
||||
SECURITY_WARNING = (
|
||||
"\033[33mSecurity Warning: This tool uses a restricted pickle unpickler with "
|
||||
"whitelist-based class loading to mitigate arbitrary code execution risks. "
|
||||
"However, no safeguard is perfect. Only process files from trusted sources.\033[0m"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
command line tool entry.
|
||||
"""
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
argparser = argparse.ArgumentParser()
|
||||
argparser.add_argument(
|
||||
"--verbose", "-v", action="store_true", help="verbose output"
|
||||
)
|
||||
subparsers = argparser.add_subparsers(
|
||||
title="subcommands", dest="command", help="subcommand help"
|
||||
)
|
||||
|
||||
decompile_parser = subparsers.add_parser("decompile", help="decompile rpyc file")
|
||||
decompile_parser.add_argument("src", nargs="+", help="rpyc file or directory")
|
||||
decompile_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="output path",
|
||||
)
|
||||
decompile_parser.add_argument(
|
||||
"--disassemble", "-d", action="store_true", help="disassemble rpyc file"
|
||||
)
|
||||
decompile_parser.set_defaults(
|
||||
func=lambda args: decompile_files(args.src, **vars(args))
|
||||
)
|
||||
|
||||
unrpa_parser = subparsers.add_parser("unrpa", help="extract rpa archive")
|
||||
unrpa_parser.add_argument("file", nargs=1, help="rpa archive")
|
||||
unrpa_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="output path",
|
||||
)
|
||||
unrpa_parser.add_argument(
|
||||
"--expression",
|
||||
"-e",
|
||||
dest="expressions",
|
||||
action="append",
|
||||
type=regular_expression,
|
||||
metavar="REGEX",
|
||||
help=(
|
||||
"extract archive paths matching this regular expression; may be repeated"
|
||||
),
|
||||
)
|
||||
unrpa_parser.add_argument(
|
||||
"--suffix",
|
||||
"-s",
|
||||
dest="suffixes",
|
||||
action="extend",
|
||||
nargs="+",
|
||||
type=non_empty_suffix,
|
||||
metavar="SUFFIX",
|
||||
help=(
|
||||
"extract archive paths ending with any of these suffixes "
|
||||
"(case-insensitive); may be repeated"
|
||||
),
|
||||
)
|
||||
unrpa_parser.set_defaults(
|
||||
func=lambda args: extract_rpa_files(args.file, **vars(args))
|
||||
)
|
||||
|
||||
rpa_parser = subparsers.add_parser("rpa", help="create rpa archive")
|
||||
rpa_parser.add_argument("src", nargs="+", help="file or directory to add")
|
||||
rpa_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
required=True,
|
||||
help="output .rpa archive path",
|
||||
)
|
||||
rpa_parser.add_argument(
|
||||
"--base",
|
||||
help=(
|
||||
"base directory for archive names "
|
||||
"(default: source directory for a single directory, otherwise common parent)"
|
||||
),
|
||||
)
|
||||
rpa_parser.set_defaults(
|
||||
func=lambda args: create_rpa_archive(
|
||||
args.src,
|
||||
args.output,
|
||||
base=args.base,
|
||||
)
|
||||
)
|
||||
|
||||
extract_game_parser = subparsers.add_parser(
|
||||
"extract-game", help="extract Ren'Py game from Android APK file"
|
||||
)
|
||||
extract_game_parser.add_argument("apk", help="Android APK file path")
|
||||
extract_game_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="output directory (default: APK filename without extension)",
|
||||
)
|
||||
extract_game_parser.set_defaults(
|
||||
func=lambda args: extract_game_from_apk(args.apk, **vars(args))
|
||||
)
|
||||
|
||||
extract_translate_parser = subparsers.add_parser(
|
||||
"extract-translate",
|
||||
help="""extract translations from rpy files and save to tl/<language> directory.""",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"src", nargs="+", help="source game directory"
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
help="output directory (default: current directory, creates tl/<language>/)",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--language",
|
||||
"-l",
|
||||
required=True,
|
||||
help="target language code (e.g. chinese, japanese)",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--no-dialogues",
|
||||
action="store_true",
|
||||
help="skip dialogue translations",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--no-strings",
|
||||
action="store_true",
|
||||
help="skip string translations",
|
||||
)
|
||||
extract_translate_parser.add_argument(
|
||||
"--empty",
|
||||
action="store_true",
|
||||
help="generate empty translations (default: copy original)",
|
||||
)
|
||||
extract_translate_parser.set_defaults(
|
||||
func=lambda args: run_extract_translations(
|
||||
args.src,
|
||||
args.language,
|
||||
output=args.output,
|
||||
include_dialogues=not args.no_dialogues,
|
||||
include_strings=not args.no_strings,
|
||||
empty_translation=args.empty,
|
||||
)
|
||||
)
|
||||
|
||||
save_parser = subparsers.add_parser("save", help="save operations")
|
||||
save_subparsers = save_parser.add_subparsers(
|
||||
title="save subcommands", dest="save_command", help="save subcommand help"
|
||||
)
|
||||
save_extract_parser = save_subparsers.add_parser(
|
||||
"extract", help="extract save data to JSON files for editing"
|
||||
)
|
||||
save_extract_parser.add_argument("path", nargs=1, help="save-file path")
|
||||
save_extract_parser.add_argument("dest", nargs="?", help="extract to directory")
|
||||
save_extract_parser.add_argument(
|
||||
"--disassemble", "-d", action="store_true", help="print pickle disassembly"
|
||||
)
|
||||
save_extract_parser.add_argument(
|
||||
"--verbose", "-V", action="store_true", help="verbose class loading output"
|
||||
)
|
||||
save_extract_parser.set_defaults(
|
||||
func=lambda args: extract_save(
|
||||
args.path[0],
|
||||
args.dest or "",
|
||||
dissemble=args.disassemble,
|
||||
verbose=getattr(args, "verbose", False),
|
||||
)
|
||||
)
|
||||
|
||||
save_restore_parser = save_subparsers.add_parser(
|
||||
"restore", help="restore save data from extracted JSON directory"
|
||||
)
|
||||
save_restore_parser.add_argument("path", nargs=1, help="extracted directory")
|
||||
save_restore_parser.add_argument("dest", nargs="?", help="restore to save-file")
|
||||
save_restore_parser.add_argument(
|
||||
"--key", "-k", help="path to security_keys.txt for re-signing"
|
||||
)
|
||||
save_restore_parser.set_defaults(
|
||||
func=lambda args: restore_save(
|
||||
args.path[0], args.dest or "", key_file=args.key or ""
|
||||
)
|
||||
)
|
||||
|
||||
save_info_parser = save_subparsers.add_parser(
|
||||
"info", help="show save file information"
|
||||
)
|
||||
save_info_parser.add_argument("path", nargs=1, help="save-file path")
|
||||
save_info_parser.set_defaults(func=lambda args: dump_save_info(args.path[0]))
|
||||
|
||||
save_genkey_parser = save_subparsers.add_parser(
|
||||
"genkey", help="generate a new signing key (requires ecdsa library)"
|
||||
)
|
||||
save_genkey_parser.add_argument(
|
||||
"output",
|
||||
nargs="?",
|
||||
default="security_keys.txt",
|
||||
help="output key file path (default: security_keys.txt)",
|
||||
)
|
||||
save_genkey_parser.set_defaults(func=lambda args: generate_new_key(args.output))
|
||||
|
||||
args = argparser.parse_args()
|
||||
if args.verbose:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
if not args.command:
|
||||
argparser.print_help()
|
||||
return
|
||||
|
||||
# Show security warning for commands that process pickle data
|
||||
if args.command in ("decompile", "save", "unrpa") and not os.environ.get(
|
||||
"RPYCDEC_NO_WARNING"
|
||||
):
|
||||
print(SECURITY_WARNING, file=sys.stderr)
|
||||
|
||||
args.func(args)
|
||||
58
third_party/python/rpycdec/src/rpycdec/decompile.py
vendored
Normal file
58
third_party/python/rpycdec/src/rpycdec/decompile.py
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Module for decompiling Ren'Py compiled files (.rpyc) back to source code (.rpy)."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from renpy import util
|
||||
from rpycdec import stmts, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def decompile_file(input_file, output_path=None, **kwargs):
|
||||
"""
|
||||
decompile rpyc file into rpy file and write to output.
|
||||
"""
|
||||
if not output_path:
|
||||
name, _ = os.path.splitext(input_file)
|
||||
output_path = f"{name}.rpy"
|
||||
|
||||
output_dir = os.path.dirname(output_path)
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
stmt = stmts.load_file(input_file, **kwargs)
|
||||
code = util.get_code(stmt)
|
||||
except Exception as e:
|
||||
logger.error("decode file %s failed: %s", input_file, e)
|
||||
raise
|
||||
utils.write_file(output_path, code)
|
||||
logger.info("decompile %s -> %s", input_file, output_path)
|
||||
|
||||
|
||||
def decompile(input_path, output_path=None, **kwargs):
|
||||
"""
|
||||
decompile rpyc file or directory into rpy
|
||||
|
||||
Parameters
|
||||
----------
|
||||
input_path : str
|
||||
path to rpyc file or directory contains rpyc files
|
||||
output_path : str, optional
|
||||
output path, by default it's same path of input_path.
|
||||
"""
|
||||
if not os.path.isdir(input_path):
|
||||
if output_path:
|
||||
name, _ = os.path.splitext(os.path.basename(input_path))
|
||||
output_path = os.path.join(output_path, f"{name}.rpy")
|
||||
decompile_file(input_path, output_path, **kwargs)
|
||||
return
|
||||
if not output_path:
|
||||
output_path = input_path
|
||||
for filename in utils.match_files(input_path, r".*\.rpym?c$"):
|
||||
decompile_file(
|
||||
os.path.join(input_path, filename),
|
||||
os.path.join(output_path, filename.removesuffix("c")),
|
||||
**kwargs,
|
||||
)
|
||||
249
third_party/python/rpycdec/src/rpycdec/rpa.py
vendored
Normal file
249
third_party/python/rpycdec/src/rpycdec/rpa.py
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import zlib
|
||||
from collections.abc import Iterable
|
||||
from io import BufferedIOBase
|
||||
from pathlib import Path
|
||||
|
||||
from rpycdec.safe_pickle import rpa_loads
|
||||
from rpycdec.utils import safe_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RPA3_KEY = 0x42424242
|
||||
RPA3_PADDING = b"Made with Ren'Py."
|
||||
RPA3_HEADER_PLACEHOLDER = b"RPA-3.0 XXXXXXXXXXXXXXXX XXXXXXXX\n"
|
||||
|
||||
|
||||
def read_until(data: BufferedIOBase, delimiter: int = 0x00) -> bytes:
|
||||
"""Read bytes from stream until delimiter is found.
|
||||
|
||||
Raises ValueError on unexpected EOF.
|
||||
"""
|
||||
content = bytearray()
|
||||
while True:
|
||||
c = data.read(1)
|
||||
if not c:
|
||||
raise ValueError("Unexpected EOF while reading stream")
|
||||
if c[0] == delimiter:
|
||||
break
|
||||
content += c
|
||||
return bytes(content)
|
||||
|
||||
|
||||
def start_to_bytes(left: list | None) -> bytes:
|
||||
if not left:
|
||||
return b""
|
||||
if isinstance(left[0], bytes):
|
||||
return left[0]
|
||||
return left[0].encode("latin-1")
|
||||
|
||||
|
||||
def normalize_archive_name(name: str) -> str:
|
||||
return name.replace(os.sep, "/")
|
||||
|
||||
|
||||
def collect_rpa_files(
|
||||
sources: list[str],
|
||||
base_dir: str | None = None,
|
||||
output_path: str | None = None,
|
||||
) -> list[tuple[str, str]]:
|
||||
"""Collect files to add to an archive.
|
||||
|
||||
Returns ``(archive_name, filesystem_path)`` tuples. Directories contribute
|
||||
all nested files, with deterministic sorting.
|
||||
"""
|
||||
if not sources:
|
||||
raise ValueError("No source files were provided.")
|
||||
|
||||
paths = [Path(src) for src in sources]
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(path)
|
||||
if not path.is_file() and not path.is_dir():
|
||||
raise ValueError(f"Unsupported source path: {path}")
|
||||
|
||||
if base_dir is not None:
|
||||
base = Path(base_dir).resolve()
|
||||
elif len(paths) == 1 and paths[0].is_dir():
|
||||
base = paths[0].resolve()
|
||||
else:
|
||||
parents = [
|
||||
path.resolve().parent if path.is_file() else path.resolve()
|
||||
for path in paths
|
||||
]
|
||||
base = Path(os.path.commonpath([str(parent) for parent in parents]))
|
||||
|
||||
output = Path(output_path).resolve() if output_path else None
|
||||
entries: list[tuple[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
for path in sorted(paths, key=lambda p: str(p)):
|
||||
resolved = path.resolve()
|
||||
candidates = [resolved]
|
||||
if resolved.is_dir():
|
||||
candidates = sorted(
|
||||
(p for p in resolved.rglob("*") if p.is_file()),
|
||||
key=lambda p: str(p),
|
||||
)
|
||||
|
||||
for file_path in candidates:
|
||||
if output is not None and file_path.resolve() == output:
|
||||
logger.info("skipping output archive: %s", file_path)
|
||||
continue
|
||||
|
||||
try:
|
||||
archive_name = normalize_archive_name(
|
||||
str(file_path.resolve().relative_to(base))
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"{file_path} is not under base directory {base}"
|
||||
) from exc
|
||||
|
||||
if archive_name in ("", "."):
|
||||
archive_name = file_path.name
|
||||
|
||||
if archive_name in seen:
|
||||
raise ValueError(f"Duplicate archive path: {archive_name}")
|
||||
|
||||
seen.add(archive_name)
|
||||
entries.append((archive_name, str(file_path)))
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def create_rpa(
|
||||
output_path: str,
|
||||
sources: list[str],
|
||||
base_dir: str | None = None,
|
||||
key: int = RPA3_KEY,
|
||||
):
|
||||
"""Create a Ren'Py RPA-3.0 archive from files or directories."""
|
||||
entries = collect_rpa_files(sources, base_dir=base_dir, output_path=output_path)
|
||||
output_dir = os.path.dirname(output_path)
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
index: dict[str, list[tuple[int, int, bytes]]] = {}
|
||||
|
||||
with open(output_path, "wb") as archive:
|
||||
archive.write(RPA3_HEADER_PLACEHOLDER)
|
||||
|
||||
for archive_name, file_path in entries:
|
||||
archive.write(RPA3_PADDING)
|
||||
offset = archive.tell()
|
||||
size = os.path.getsize(file_path)
|
||||
|
||||
with open(file_path, "rb") as source:
|
||||
while True:
|
||||
chunk = source.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
archive.write(chunk)
|
||||
|
||||
index[archive_name] = [(offset ^ key, size ^ key, b"")]
|
||||
logger.info("adding: %s", archive_name)
|
||||
|
||||
index_offset = archive.tell()
|
||||
archive.write(zlib.compress(pickle.dumps(index, pickle.HIGHEST_PROTOCOL)))
|
||||
|
||||
archive.seek(0)
|
||||
archive.write(b"RPA-3.0 %016x %08x\n" % (index_offset, key))
|
||||
|
||||
|
||||
def _compile_file_filters(
|
||||
expressions: str | Iterable[str] | None,
|
||||
suffixes: str | Iterable[str] | None,
|
||||
) -> tuple[list[re.Pattern[str]], tuple[str, ...]]:
|
||||
expression_values = (expressions,) if isinstance(expressions, str) else expressions
|
||||
suffix_values = (suffixes,) if isinstance(suffixes, str) else suffixes
|
||||
patterns = [re.compile(expression) for expression in expression_values or ()]
|
||||
normalized_suffixes = tuple(suffix.casefold() for suffix in suffix_values or ())
|
||||
if any(not suffix for suffix in normalized_suffixes):
|
||||
raise ValueError("RPA file suffixes must not be empty.")
|
||||
return patterns, normalized_suffixes
|
||||
|
||||
|
||||
def _matches_file_filters(
|
||||
filename: str,
|
||||
patterns: list[re.Pattern[str]],
|
||||
suffixes: tuple[str, ...],
|
||||
) -> bool:
|
||||
if not patterns and not suffixes:
|
||||
return True
|
||||
|
||||
# RPA paths conventionally use forward slashes. Normalize legacy archives
|
||||
# so expressions behave consistently on every host platform.
|
||||
archive_path = filename.replace("\\", "/")
|
||||
return any(pattern.search(archive_path) for pattern in patterns) or (
|
||||
bool(suffixes) and archive_path.casefold().endswith(suffixes)
|
||||
)
|
||||
|
||||
|
||||
def extract_rpa(
|
||||
r: BufferedIOBase,
|
||||
output_dir: str | None = None,
|
||||
expressions: str | Iterable[str] | None = None,
|
||||
suffixes: str | Iterable[str] | None = None,
|
||||
):
|
||||
"""Extract files from an RPA archive.
|
||||
|
||||
When *expressions* or *suffixes* are provided, a file is extracted when
|
||||
its normalized archive path matches any regular expression or suffix.
|
||||
Regular expressions use :func:`re.search`; suffix matching ignores case.
|
||||
"""
|
||||
output_dir = output_dir or "."
|
||||
patterns, normalized_suffixes = _compile_file_filters(expressions, suffixes)
|
||||
magic = read_until(r, 0x20)
|
||||
if magic != b"RPA-3.0":
|
||||
raise ValueError("Not a Ren'Py RPA-3.0 archive.")
|
||||
index_offset = int(read_until(r, 0x20), 16)
|
||||
key = int(read_until(r, 0x0A).decode(), 16)
|
||||
|
||||
# read index
|
||||
r.seek(index_offset)
|
||||
index = rpa_loads(zlib.decompress(r.read()))
|
||||
|
||||
for k, v in index.items():
|
||||
index[k] = [
|
||||
(offset ^ key, dlen ^ key, start_to_bytes(left))
|
||||
for offset, dlen, *left in v
|
||||
]
|
||||
|
||||
for filename, entries in index.items():
|
||||
# Handle bytes filenames from Python 2 era archives
|
||||
if isinstance(filename, bytes):
|
||||
filename = filename.decode("utf-8", errors="surrogateescape")
|
||||
|
||||
if not _matches_file_filters(filename, patterns, normalized_suffixes):
|
||||
logger.debug("skipping filtered file: %s", filename)
|
||||
continue
|
||||
|
||||
# Path traversal protection. Resolve the destination before reading
|
||||
# the payload so rejected entries do not consume unnecessary memory.
|
||||
try:
|
||||
dest = safe_path(output_dir, filename)
|
||||
except ValueError:
|
||||
logger.warning("Skipping path traversal attempt: %s", filename)
|
||||
continue
|
||||
|
||||
data = bytearray()
|
||||
for offset, dlen, start in entries:
|
||||
r.seek(offset)
|
||||
block = r.read(dlen)
|
||||
if start:
|
||||
if block.startswith(start):
|
||||
block = block[len(start) :]
|
||||
else:
|
||||
logger.warning(
|
||||
"%s does not start with expected prefix %s", filename, start
|
||||
)
|
||||
data += block
|
||||
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
with open(dest, "wb") as f:
|
||||
logger.info("extracting: %s", dest)
|
||||
f.write(data)
|
||||
257
third_party/python/rpycdec/src/rpycdec/safe_pickle.py
vendored
Normal file
257
third_party/python/rpycdec/src/rpycdec/safe_pickle.py
vendored
Normal file
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
Safe pickle unpickling utilities.
|
||||
|
||||
WARNING: Python's pickle format can execute arbitrary code during
|
||||
deserialization. This module provides restricted unpicklers that only
|
||||
allow known-safe classes to be loaded, preventing malicious pickle
|
||||
files from executing harmful code.
|
||||
|
||||
See: https://docs.python.org/3/library/pickle.html#module-pickle
|
||||
"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
import pickle
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Whitelist definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Standard library modules that are safe to import during unpickling.
|
||||
SAFE_MODULES: set[str] = {
|
||||
"collections",
|
||||
"collections.abc",
|
||||
}
|
||||
|
||||
# Builtin names that are safe to unpickle (data types only).
|
||||
# Dangerous names like eval, exec, __import__, getattr, etc. are excluded.
|
||||
SAFE_BUILTINS: set[str] = {
|
||||
"set",
|
||||
"frozenset",
|
||||
"list",
|
||||
"dict",
|
||||
"tuple",
|
||||
"bytes",
|
||||
"bytearray",
|
||||
"complex",
|
||||
"True",
|
||||
"False",
|
||||
"None",
|
||||
"object",
|
||||
"int",
|
||||
"float",
|
||||
"str",
|
||||
"bool",
|
||||
"slice",
|
||||
"range",
|
||||
"type",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DummyClass — safe placeholder for unknown classes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DummyClass(object):
|
||||
"""
|
||||
Safe placeholder class for unpickling unknown classes.
|
||||
|
||||
Handles all pickle reconstruction patterns:
|
||||
- NEWOBJ/REDUCE with arguments (stored as _new_args/_new_kwargs)
|
||||
- BUILD with dict state, (state, slotstate) tuples, or opaque state
|
||||
- List-like append operations (APPENDS opcode)
|
||||
"""
|
||||
|
||||
_state: Any
|
||||
_new_args: tuple[Any, ...] | None
|
||||
_new_kwargs: dict[str, Any] | None
|
||||
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> "DummyClass":
|
||||
self = object.__new__(cls)
|
||||
self._state = None
|
||||
self._new_args = args if args else None
|
||||
self._new_kwargs = kwargs if kwargs else None
|
||||
return self
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
# __new__ already handled storage; avoid overwriting
|
||||
pass
|
||||
|
||||
def append(self, value: Any) -> None:
|
||||
if self._state is None:
|
||||
self._state = []
|
||||
self._state.append(value)
|
||||
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self.__dict__[key]
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, DummyClass):
|
||||
return NotImplemented
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
self.__dict__[key] = value
|
||||
|
||||
def __getstate__(self) -> Any:
|
||||
if self._state is not None:
|
||||
return self._state
|
||||
# Exclude internal fields from state
|
||||
return {k: v for k, v in self.__dict__.items() if not k.startswith("_new_")}
|
||||
|
||||
def __setstate__(self, state: Any) -> None:
|
||||
# Handle (state, slotstate) tuple pattern from __slots__ classes
|
||||
slotstate = None
|
||||
if (
|
||||
isinstance(state, tuple)
|
||||
and len(state) == 2
|
||||
and (state[0] is None or isinstance(state[0], dict))
|
||||
and (state[1] is None or isinstance(state[1], dict))
|
||||
):
|
||||
state, slotstate = state
|
||||
|
||||
if isinstance(state, dict):
|
||||
self.__dict__.update(state)
|
||||
elif state is not None:
|
||||
self._state = state
|
||||
|
||||
if slotstate:
|
||||
self.__dict__.update(slotstate)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
cls = type(self)
|
||||
module = getattr(cls, "__module__", "?")
|
||||
name = getattr(cls, "__name__", "?")
|
||||
if self._state is not None:
|
||||
return f"<{module}.{name}: state={self._state!r}>"
|
||||
attrs = {
|
||||
k: v
|
||||
for k, v in self.__dict__.items()
|
||||
if not k.startswith("_new_") and k != "_state"
|
||||
}
|
||||
return f"<{module}.{name}: {attrs}>"
|
||||
|
||||
def get_code(self, **kwargs) -> str:
|
||||
name = getattr(type(self), "__name__", "<?>")
|
||||
logger.warning("Unrecognized node %s in decompile output", name)
|
||||
return f"# <unrecognized: {name}>"
|
||||
|
||||
|
||||
def make_dummy_class(module: str, name: str) -> type:
|
||||
"""Create a named DummyClass subclass for a given module.name."""
|
||||
return type(name, (DummyClass,), {"__module__": module})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SafeUnpickler — base restricted unpickler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SafeUnpickler(pickle.Unpickler):
|
||||
"""Base restricted unpickler with whitelist-based class loading.
|
||||
|
||||
Security model:
|
||||
- ``renpy.*`` / ``store.*``: allowed (our fake packages)
|
||||
- ``builtins`` / ``__builtin__``: only safe data types
|
||||
- ``SAFE_MODULES``: standard library modules known to be safe
|
||||
- Everything else: handled by ``_on_unknown_class()``
|
||||
|
||||
Subclasses can override ``_on_unknown_class()`` to customise behaviour
|
||||
for classes outside the whitelist (raise, create placeholder, etc.).
|
||||
"""
|
||||
|
||||
def find_class(self, module: str, name: str) -> Any:
|
||||
# store.* modules -> DummyClass placeholder
|
||||
if module.startswith("store"):
|
||||
return make_dummy_class(module, name)
|
||||
|
||||
# Our fake renpy package is safe to import
|
||||
if module.startswith("renpy"):
|
||||
try:
|
||||
return super().find_class(module, name)
|
||||
except (ImportError, AttributeError):
|
||||
return self._on_unknown_class(module, name)
|
||||
|
||||
# Whitelisted standard library modules
|
||||
if module in SAFE_MODULES:
|
||||
return super().find_class(module, name)
|
||||
|
||||
# Safe builtins only (excludes eval, exec, __import__, etc.)
|
||||
if module in ("builtins", "__builtin__") and name in SAFE_BUILTINS:
|
||||
return super().find_class(module, name)
|
||||
|
||||
return self._on_unknown_class(module, name)
|
||||
|
||||
def _on_unknown_class(self, module: str, name: str) -> Any:
|
||||
"""Handle a class reference outside the whitelist.
|
||||
|
||||
Default: log a warning and return a DummyClass placeholder.
|
||||
Subclasses may override to raise or return different placeholders.
|
||||
"""
|
||||
logger.warning(
|
||||
f"Unknown class encountered: {module}.{name}. "
|
||||
f"Substituting with DummyClass placeholder. "
|
||||
f"If this is a legitimate Ren'Py class, please report an issue."
|
||||
)
|
||||
return make_dummy_class(module, name)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RestrictedUnpickler — only allows primitive/builtin data types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Primitive builtin types that are always safe.
|
||||
_PRIMITIVE_BUILTINS = {
|
||||
"dict",
|
||||
"list",
|
||||
"tuple",
|
||||
"set",
|
||||
"frozenset",
|
||||
"bytes",
|
||||
"bytearray",
|
||||
"str",
|
||||
"int",
|
||||
"float",
|
||||
"bool",
|
||||
"complex",
|
||||
"True",
|
||||
"False",
|
||||
"None",
|
||||
}
|
||||
|
||||
|
||||
class RestrictedUnpickler(pickle.Unpickler):
|
||||
"""Strictly restricted unpickler that only allows primitive data types.
|
||||
|
||||
Suitable for data that should only contain basic Python types
|
||||
(dict, list, tuple, int, str, bytes, etc.) with no custom classes.
|
||||
|
||||
Used by RPA index parsing and any other context where only
|
||||
primitive data structures are expected.
|
||||
"""
|
||||
|
||||
def find_class(self, module: str, name: str) -> Any:
|
||||
if module in ("builtins", "__builtin__") and name in _PRIMITIVE_BUILTINS:
|
||||
return super().find_class(module, name)
|
||||
|
||||
raise pickle.UnpicklingError(
|
||||
f"Refusing to unpickle non-primitive class: {module}.{name}"
|
||||
)
|
||||
|
||||
|
||||
def safe_loads(data: bytes, **kwargs: Any) -> Any:
|
||||
"""Safely unpickle data using SafeUnpickler.
|
||||
|
||||
Unknown classes are substituted with DummyClass placeholders.
|
||||
"""
|
||||
return SafeUnpickler(io.BytesIO(data), **kwargs).load()
|
||||
|
||||
|
||||
def rpa_loads(data: bytes) -> Any:
|
||||
"""Safely unpickle RPA index data (very restricted, primitives only)."""
|
||||
return RestrictedUnpickler(io.BytesIO(data), encoding="bytes").load()
|
||||
697
third_party/python/rpycdec/src/rpycdec/save.py
vendored
Normal file
697
third_party/python/rpycdec/src/rpycdec/save.py
vendored
Normal file
@@ -0,0 +1,697 @@
|
||||
"""
|
||||
Ren'Py Save File Parser and Writer
|
||||
|
||||
Ren'Py save files are ZIP archives containing:
|
||||
- screenshot.png: Save screenshot
|
||||
- extra_info: UTF-8 encoded save name/description
|
||||
- json: JSON metadata (version, timestamp, etc.)
|
||||
- renpy_version: Ren'Py version string
|
||||
- log: Pickle serialized game state (roots, log)
|
||||
- signatures: Digital signature for integrity verification
|
||||
|
||||
This module provides functionality to:
|
||||
1. Extract save files to JSON for manual editing
|
||||
2. Restore edited JSON back to valid save files
|
||||
"""
|
||||
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import pickletools
|
||||
import zipfile
|
||||
from typing import Any
|
||||
|
||||
from rpycdec.safe_pickle import (
|
||||
SAFE_MODULES,
|
||||
SafeUnpickler,
|
||||
)
|
||||
|
||||
|
||||
class DynamicObject:
|
||||
"""
|
||||
A dynamic object that can represent any Ren'Py class during unpickling.
|
||||
Stores the original class name and module for JSON round-trip serialization.
|
||||
|
||||
Unlike ``DummyClass`` (safe_pickle.py) which spreads state into ``__dict__``
|
||||
for attribute access during decompilation, ``DynamicObject`` keeps state
|
||||
as-is in ``_state`` so it can be faithfully serialized to / from JSON.
|
||||
|
||||
Handles all pickle reconstruction patterns:
|
||||
- NEWOBJ/REDUCE with arguments
|
||||
- BUILD with state (dict or opaque)
|
||||
- APPENDS (list-like append)
|
||||
- SETITEMS (dict-like __setitem__)
|
||||
"""
|
||||
|
||||
# Class-level defaults — overridden by type() in _on_unknown_class.
|
||||
# Using class attrs ensures they survive even when pickle skips __init__
|
||||
# (e.g. the NEWOBJ opcode calls __new__ only).
|
||||
_class_name: str = ""
|
||||
_module_name: str = ""
|
||||
_state: Any
|
||||
_items: list[Any] | None
|
||||
_new_args: tuple[Any, ...] | None
|
||||
|
||||
def __new__(cls, *args: Any, **kwargs: Any) -> "DynamicObject":
|
||||
self = object.__new__(cls)
|
||||
self._state = None
|
||||
self._items = None
|
||||
self._new_args = args if args else None
|
||||
return self
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
# __new__ already handled initialization; nothing to do.
|
||||
pass
|
||||
|
||||
# -- pickle BUILD --
|
||||
|
||||
def __setstate__(self, state: Any) -> None:
|
||||
self._state = state
|
||||
|
||||
def __getstate__(self) -> Any:
|
||||
return self._state
|
||||
|
||||
# -- pickle APPENDS --
|
||||
|
||||
def append(self, item: Any) -> None:
|
||||
if self._items is None:
|
||||
self._items = []
|
||||
self._items.append(item)
|
||||
|
||||
# -- pickle SETITEMS --
|
||||
|
||||
def __setitem__(self, key: Any, value: Any) -> None:
|
||||
if self._state is None:
|
||||
self._state = {}
|
||||
if isinstance(self._state, dict):
|
||||
self._state[key] = value
|
||||
|
||||
# -- re-pickling / repr --
|
||||
|
||||
def __reduce__(self):
|
||||
"""Support re-pickling by returning the original class info."""
|
||||
return (
|
||||
_reconstruct_object,
|
||||
(self._module_name, self._class_name, self._state),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self._module_name}.{self._class_name}: {self._state}>"
|
||||
|
||||
# -- JSON round-trip helpers --
|
||||
|
||||
def to_json_dict(self) -> dict:
|
||||
"""Convert to a JSON-serializable dictionary."""
|
||||
result: dict[str, Any] = {
|
||||
"__class__": self._class_name,
|
||||
"__module__": self._module_name,
|
||||
"__state__": _to_json_serializable(self._state),
|
||||
}
|
||||
if self._items:
|
||||
result["__items__"] = _to_json_serializable(self._items)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_json_dict(cls, data: dict) -> "DynamicObject":
|
||||
"""Create a DynamicObject from a JSON dictionary."""
|
||||
obj = cls()
|
||||
obj._class_name = data.get("__class__", "")
|
||||
obj._module_name = data.get("__module__", "")
|
||||
obj._state = _from_json_serializable(data.get("__state__"))
|
||||
items = data.get("__items__")
|
||||
if items:
|
||||
obj._items = _from_json_serializable(items)
|
||||
return obj
|
||||
|
||||
|
||||
# Modules allowed for reconstruction (renpy/store fake packages + safe stdlib)
|
||||
_RECONSTRUCT_SAFE_PREFIXES = ("renpy.", "store.")
|
||||
|
||||
|
||||
def _reconstruct_object(module_name: str, class_name: str, state: Any) -> Any:
|
||||
"""Reconstruct an object from its module, class name, and state.
|
||||
|
||||
Only allows importing from renpy.*/store.* (our fake packages) and
|
||||
whitelisted standard library modules. All other modules fall back
|
||||
to DynamicObject.
|
||||
"""
|
||||
# Only allow our fake packages and whitelisted modules
|
||||
is_safe = (
|
||||
any(module_name.startswith(p) for p in _RECONSTRUCT_SAFE_PREFIXES)
|
||||
or module_name in ("renpy", "store")
|
||||
or module_name in SAFE_MODULES
|
||||
)
|
||||
if is_safe:
|
||||
try:
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
cls = getattr(module, class_name)
|
||||
obj = object.__new__(cls)
|
||||
if state is not None:
|
||||
if hasattr(obj, "__setstate__"):
|
||||
obj.__setstate__(state)
|
||||
elif isinstance(state, dict):
|
||||
obj.__dict__.update(state)
|
||||
return obj
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
# Fall back to DynamicObject
|
||||
obj = DynamicObject()
|
||||
obj._class_name = class_name
|
||||
obj._module_name = module_name
|
||||
obj._state = state
|
||||
return obj
|
||||
|
||||
|
||||
class SaveUnpickler(SafeUnpickler):
|
||||
"""Safe unpickler for Ren'Py save files.
|
||||
|
||||
Extends SafeUnpickler to replace unknown classes with DynamicObject
|
||||
instances (instead of raising) so save data can be serialized to JSON.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file,
|
||||
*,
|
||||
fix_imports: bool = False,
|
||||
encoding: str = "ASCII",
|
||||
errors: str = "strict",
|
||||
verbose: bool = False,
|
||||
):
|
||||
super().__init__(
|
||||
file, fix_imports=fix_imports, encoding=encoding, errors=errors
|
||||
)
|
||||
self.verbose = verbose
|
||||
|
||||
def _on_unknown_class(self, module: str, name: str) -> type:
|
||||
"""Return a DynamicObject subclass instead of raising."""
|
||||
if self.verbose:
|
||||
print(f"Creating dynamic class: {module}.{name}")
|
||||
return type(
|
||||
name,
|
||||
(DynamicObject,),
|
||||
{
|
||||
"__module__": module,
|
||||
"_class_name": name,
|
||||
"_module_name": module,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SavePickler(pickle.Pickler):
|
||||
"""
|
||||
Custom pickler that handles DynamicObject instances by restoring
|
||||
their original class information.
|
||||
"""
|
||||
|
||||
def reducer_override(self, obj):
|
||||
if isinstance(obj, DynamicObject):
|
||||
# Return a reduction that will reconstruct the original class
|
||||
return (
|
||||
_reconstruct_object,
|
||||
(obj._module_name, obj._class_name, obj._state),
|
||||
)
|
||||
# Fall back to default behavior
|
||||
return NotImplemented
|
||||
|
||||
|
||||
def _to_json_serializable(obj: Any) -> Any:
|
||||
"""Convert a Python object to a JSON-serializable structure."""
|
||||
if obj is None or isinstance(obj, (bool, int, float, str)):
|
||||
return obj
|
||||
|
||||
if isinstance(obj, bytes):
|
||||
return {"__bytes__": base64.b64encode(obj).decode("ascii")}
|
||||
|
||||
if isinstance(obj, tuple):
|
||||
return {"__tuple__": [_to_json_serializable(item) for item in obj]}
|
||||
|
||||
if isinstance(obj, list):
|
||||
return [_to_json_serializable(item) for item in obj]
|
||||
|
||||
if isinstance(obj, dict):
|
||||
result = {}
|
||||
for key, value in obj.items():
|
||||
# JSON keys must be strings
|
||||
if isinstance(key, str):
|
||||
json_key = key
|
||||
elif isinstance(key, (int, float, bool)):
|
||||
json_key = f"__key_{type(key).__name__}__:{key}"
|
||||
elif isinstance(key, tuple):
|
||||
json_key = f"__key_tuple__:{json.dumps(_to_json_serializable(key))}"
|
||||
else:
|
||||
json_key = f"__key_repr__:{repr(key)}"
|
||||
result[json_key] = _to_json_serializable(value)
|
||||
return result
|
||||
|
||||
if isinstance(obj, set):
|
||||
return {"__set__": [_to_json_serializable(item) for item in obj]}
|
||||
|
||||
if isinstance(obj, frozenset):
|
||||
return {"__frozenset__": [_to_json_serializable(item) for item in obj]}
|
||||
|
||||
if isinstance(obj, DynamicObject):
|
||||
return obj.to_json_dict()
|
||||
|
||||
# For other objects, try to extract state
|
||||
if hasattr(obj, "__dict__"):
|
||||
return {
|
||||
"__class__": obj.__class__.__name__,
|
||||
"__module__": obj.__class__.__module__,
|
||||
"__state__": _to_json_serializable(obj.__dict__),
|
||||
}
|
||||
|
||||
if hasattr(obj, "__getstate__"):
|
||||
return {
|
||||
"__class__": obj.__class__.__name__,
|
||||
"__module__": obj.__class__.__module__,
|
||||
"__state__": _to_json_serializable(obj.__getstate__()),
|
||||
}
|
||||
|
||||
# Fallback: convert to string representation
|
||||
return {"__repr__": repr(obj), "__type__": type(obj).__name__}
|
||||
|
||||
|
||||
def _from_json_serializable(obj: Any) -> Any:
|
||||
"""Convert a JSON structure back to Python objects."""
|
||||
if obj is None or isinstance(obj, (bool, int, float, str)):
|
||||
return obj
|
||||
|
||||
if isinstance(obj, list):
|
||||
return [_from_json_serializable(item) for item in obj]
|
||||
|
||||
if isinstance(obj, dict):
|
||||
# Check for special types
|
||||
if "__bytes__" in obj:
|
||||
return base64.b64decode(obj["__bytes__"])
|
||||
|
||||
if "__tuple__" in obj:
|
||||
return tuple(_from_json_serializable(item) for item in obj["__tuple__"])
|
||||
|
||||
if "__set__" in obj:
|
||||
return set(_from_json_serializable(item) for item in obj["__set__"])
|
||||
|
||||
if "__frozenset__" in obj:
|
||||
return frozenset(
|
||||
_from_json_serializable(item) for item in obj["__frozenset__"]
|
||||
)
|
||||
|
||||
if "__class__" in obj and "__module__" in obj:
|
||||
return DynamicObject.from_json_dict(obj)
|
||||
|
||||
if "__repr__" in obj:
|
||||
# Cannot reconstruct, return as-is with warning marker
|
||||
return {"__unrecoverable__": obj["__repr__"]}
|
||||
|
||||
# Regular dict with possibly encoded keys
|
||||
result = {}
|
||||
for key, value in obj.items():
|
||||
if key.startswith("__key_int__:"):
|
||||
actual_key = int(key.split(":", 1)[1])
|
||||
elif key.startswith("__key_float__:"):
|
||||
actual_key = float(key.split(":", 1)[1])
|
||||
elif key.startswith("__key_bool__:"):
|
||||
actual_key = key.split(":", 1)[1] == "True"
|
||||
elif key.startswith("__key_tuple__:"):
|
||||
actual_key = tuple(
|
||||
_from_json_serializable(json.loads(key.split(":", 1)[1]))
|
||||
)
|
||||
elif key.startswith("__key_repr__:"):
|
||||
# Cannot fully reconstruct, use string
|
||||
actual_key = key.split(":", 1)[1]
|
||||
else:
|
||||
actual_key = key
|
||||
result[actual_key] = _from_json_serializable(value)
|
||||
return result
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def load_save_zip(file_path: str) -> dict:
|
||||
"""
|
||||
Load all components from a Ren'Py save file.
|
||||
|
||||
Returns a dict with keys:
|
||||
- screenshot: bytes or None
|
||||
- extra_info: str
|
||||
- json: dict (parsed JSON metadata)
|
||||
- renpy_version: str
|
||||
- log: bytes (raw pickle data)
|
||||
- signatures: str
|
||||
"""
|
||||
with zipfile.ZipFile(file_path, "r") as zf:
|
||||
result = {
|
||||
"screenshot": None,
|
||||
"extra_info": "",
|
||||
"json": {},
|
||||
"renpy_version": "",
|
||||
"log": b"",
|
||||
"signatures": "",
|
||||
}
|
||||
|
||||
namelist = zf.namelist()
|
||||
|
||||
if "screenshot.png" in namelist:
|
||||
result["screenshot"] = zf.read("screenshot.png")
|
||||
|
||||
if "extra_info" in namelist:
|
||||
result["extra_info"] = zf.read("extra_info").decode("utf-8")
|
||||
|
||||
if "json" in namelist:
|
||||
result["json"] = json.loads(zf.read("json").decode("utf-8"))
|
||||
|
||||
if "renpy_version" in namelist:
|
||||
result["renpy_version"] = zf.read("renpy_version").decode("utf-8")
|
||||
|
||||
if "log" in namelist:
|
||||
result["log"] = zf.read("log")
|
||||
|
||||
if "signatures" in namelist:
|
||||
result["signatures"] = zf.read("signatures").decode("utf-8")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def parse_save_log(log_data: bytes, verbose: bool = False) -> tuple[Any, Any]:
|
||||
"""
|
||||
Parse the pickle log data from a save file.
|
||||
|
||||
Returns (roots, log) tuple.
|
||||
"""
|
||||
unpickler = SaveUnpickler(
|
||||
io.BytesIO(log_data),
|
||||
verbose=verbose,
|
||||
encoding="utf-8",
|
||||
errors="surrogateescape",
|
||||
)
|
||||
return unpickler.load()
|
||||
|
||||
|
||||
def serialize_save_log(roots: Any, log: Any) -> bytes:
|
||||
"""
|
||||
Serialize roots and log back to pickle format.
|
||||
"""
|
||||
buffer = io.BytesIO()
|
||||
pickler = SavePickler(buffer, protocol=2)
|
||||
pickler.dump((roots, log))
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def extract_save(
|
||||
file_path: str,
|
||||
extracted_dir: str = "",
|
||||
dissemble: bool = False,
|
||||
verbose: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Extract a Ren'Py save file to a directory with JSON files for editing.
|
||||
|
||||
Creates:
|
||||
- metadata.json: Contains extra_info, json metadata, renpy_version, signatures
|
||||
- roots.json: The game state roots (variables, etc.)
|
||||
- log.json: The game log/history
|
||||
- screenshot.png: The save screenshot (if present)
|
||||
|
||||
Args:
|
||||
file_path: Path to the .save file
|
||||
extracted_dir: Output directory (default: {file_path}.extracted)
|
||||
dissemble: If True, print pickle disassembly to stdout
|
||||
verbose: If True, print verbose class loading info
|
||||
"""
|
||||
if not extracted_dir:
|
||||
extracted_dir = file_path + ".extracted"
|
||||
|
||||
print(f"Extracting save file: {file_path}")
|
||||
print(f"Output directory: {extracted_dir}")
|
||||
|
||||
# Load the save file
|
||||
save_data = load_save_zip(file_path)
|
||||
|
||||
if dissemble:
|
||||
print("\n=== Pickle Disassembly ===")
|
||||
pickletools.dis(save_data["log"])
|
||||
print("=== End Disassembly ===\n")
|
||||
|
||||
# Parse the log data
|
||||
roots, log = parse_save_log(save_data["log"], verbose=verbose)
|
||||
|
||||
# Create output directory
|
||||
os.makedirs(extracted_dir, exist_ok=True)
|
||||
|
||||
# Save metadata
|
||||
metadata = {
|
||||
"extra_info": save_data["extra_info"],
|
||||
"json": save_data["json"],
|
||||
"renpy_version": save_data["renpy_version"],
|
||||
"signatures": save_data["signatures"],
|
||||
}
|
||||
metadata_path = os.path.join(extracted_dir, "metadata.json")
|
||||
with open(metadata_path, "w", encoding="utf-8") as f:
|
||||
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
||||
print(" Saved: metadata.json")
|
||||
|
||||
# Save roots
|
||||
roots_path = os.path.join(extracted_dir, "roots.json")
|
||||
with open(roots_path, "w", encoding="utf-8") as f:
|
||||
json.dump(_to_json_serializable(roots), f, indent=2, ensure_ascii=False)
|
||||
print(" Saved: roots.json")
|
||||
|
||||
# Save log
|
||||
log_path = os.path.join(extracted_dir, "log.json")
|
||||
with open(log_path, "w", encoding="utf-8") as f:
|
||||
json.dump(_to_json_serializable(log), f, indent=2, ensure_ascii=False)
|
||||
print(" Saved: log.json")
|
||||
|
||||
# Save screenshot if present
|
||||
if save_data["screenshot"]:
|
||||
screenshot_path = os.path.join(extracted_dir, "screenshot.png")
|
||||
with open(screenshot_path, "wb") as f:
|
||||
f.write(save_data["screenshot"])
|
||||
print(" Saved: screenshot.png")
|
||||
|
||||
print("\nExtraction complete!")
|
||||
print("Edit roots.json or log.json, then use 'restore' to create a new save file.")
|
||||
|
||||
|
||||
def restore_save(
|
||||
extracted_dir: str,
|
||||
output_file: str = "",
|
||||
key_file: str = "",
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Restore a Ren'Py save file from extracted JSON files.
|
||||
|
||||
Reads:
|
||||
- metadata.json: Contains extra_info, json metadata, renpy_version, signatures
|
||||
- roots.json: The game state roots
|
||||
- log.json: The game log/history
|
||||
- screenshot.png: The save screenshot (optional)
|
||||
|
||||
Args:
|
||||
extracted_dir: Directory containing extracted JSON files
|
||||
output_file: Output save file path (default: {extracted_dir}.save)
|
||||
key_file: Path to security_keys.txt for re-signing (optional)
|
||||
"""
|
||||
if not output_file:
|
||||
output_file = (
|
||||
extracted_dir.rstrip("/").removesuffix(".extracted") + ".restored.save"
|
||||
)
|
||||
|
||||
print(f"Restoring save file from: {extracted_dir}")
|
||||
print(f"Output file: {output_file}")
|
||||
|
||||
# Load metadata
|
||||
metadata_path = os.path.join(extracted_dir, "metadata.json")
|
||||
with open(metadata_path, "r", encoding="utf-8") as f:
|
||||
metadata = json.load(f)
|
||||
print(" Loaded: metadata.json")
|
||||
|
||||
# Load roots
|
||||
roots_path = os.path.join(extracted_dir, "roots.json")
|
||||
with open(roots_path, "r", encoding="utf-8") as f:
|
||||
roots_json = json.load(f)
|
||||
roots = _from_json_serializable(roots_json)
|
||||
print(" Loaded: roots.json")
|
||||
|
||||
# Load log
|
||||
log_path = os.path.join(extracted_dir, "log.json")
|
||||
with open(log_path, "r", encoding="utf-8") as f:
|
||||
log_json = json.load(f)
|
||||
log = _from_json_serializable(log_json)
|
||||
print(" Loaded: log.json")
|
||||
|
||||
# Load screenshot if present
|
||||
screenshot = None
|
||||
screenshot_path = os.path.join(extracted_dir, "screenshot.png")
|
||||
if os.path.exists(screenshot_path):
|
||||
with open(screenshot_path, "rb") as f:
|
||||
screenshot = f.read()
|
||||
print(" Loaded: screenshot.png")
|
||||
|
||||
# Serialize the game state
|
||||
log_data = serialize_save_log(roots, log)
|
||||
|
||||
# Handle signatures
|
||||
if key_file:
|
||||
print(f" Re-signing with key: {key_file}")
|
||||
signatures = sign_data(log_data, key_file)
|
||||
print(" Generated new signature")
|
||||
else:
|
||||
# Keep original signatures (will be invalid for modified saves)
|
||||
signatures = metadata.get("signatures", "")
|
||||
|
||||
# Create the save file
|
||||
output_file_tmp = output_file + ".tmp"
|
||||
with zipfile.ZipFile(output_file_tmp, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
# Screenshot
|
||||
if screenshot:
|
||||
zf.writestr("screenshot.png", screenshot)
|
||||
|
||||
# Extra info
|
||||
zf.writestr("extra_info", metadata.get("extra_info", "").encode("utf-8"))
|
||||
|
||||
# JSON metadata
|
||||
zf.writestr("json", json.dumps(metadata.get("json", {})).encode("utf-8"))
|
||||
|
||||
# Ren'Py version
|
||||
zf.writestr("renpy_version", metadata.get("renpy_version", "").encode("utf-8"))
|
||||
|
||||
# Log data
|
||||
zf.writestr("log", log_data)
|
||||
|
||||
# Signatures
|
||||
zf.writestr("signatures", signatures.encode("utf-8"))
|
||||
|
||||
# Atomic rename
|
||||
if os.path.exists(output_file):
|
||||
os.unlink(output_file)
|
||||
os.rename(output_file_tmp, output_file)
|
||||
|
||||
print("\nRestore complete!")
|
||||
print(f"Save file created: {output_file}")
|
||||
|
||||
if not key_file:
|
||||
print(
|
||||
"\nNote: No signing key provided. Original signatures preserved but invalid."
|
||||
)
|
||||
print("Use --key to provide security_keys.txt for valid signatures.")
|
||||
print("Some games with strict signature checking may reject modified saves.")
|
||||
|
||||
|
||||
def sign_data(log_data: bytes, key_file: str) -> str:
|
||||
"""
|
||||
Sign log data using ECDSA keys from a Ren'Py security_keys.txt file.
|
||||
|
||||
The key file is typically located at:
|
||||
<game_save_dir>/tokens/security_keys.txt
|
||||
|
||||
Args:
|
||||
log_data: The pickle-serialized game state (log) data
|
||||
key_file: Path to security_keys.txt containing the signing key
|
||||
|
||||
Returns:
|
||||
Signature string in Ren'Py format
|
||||
"""
|
||||
try:
|
||||
import ecdsa
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"ecdsa library required for signing. Install with: pip install ecdsa"
|
||||
)
|
||||
|
||||
def encode_line(kind: str, key: bytes, sig: bytes = b"") -> str:
|
||||
"""Encode a signature line in Ren'Py format."""
|
||||
key_b64 = base64.b64encode(key).decode("ascii")
|
||||
if sig:
|
||||
sig_b64 = base64.b64encode(sig).decode("ascii")
|
||||
return f"{kind} {key_b64} {sig_b64}\n"
|
||||
return f"{kind} {key_b64}\n"
|
||||
|
||||
def decode_line(line: str) -> tuple[str, bytes, bytes]:
|
||||
"""Decode a signature line from Ren'Py format."""
|
||||
parts = line.strip().split()
|
||||
if len(parts) < 2:
|
||||
return "", b"", b""
|
||||
kind = parts[0]
|
||||
key = base64.b64decode(parts[1])
|
||||
sig = base64.b64decode(parts[2]) if len(parts) > 2 else b""
|
||||
return kind, key, sig
|
||||
|
||||
signatures = ""
|
||||
|
||||
with open(key_file, "r") as f:
|
||||
for line in f:
|
||||
kind, key_der, _ = decode_line(line)
|
||||
if kind == "signing-key":
|
||||
try:
|
||||
sk = ecdsa.SigningKey.from_der(key_der)
|
||||
if sk is not None and sk.verifying_key is not None:
|
||||
sig = sk.sign(log_data)
|
||||
signatures += encode_line(
|
||||
"signature", sk.verifying_key.to_der(), sig
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to sign with key: {e}")
|
||||
|
||||
return signatures
|
||||
|
||||
|
||||
def generate_new_key(key_file: str) -> str:
|
||||
"""
|
||||
Generate a new ECDSA signing key and save to file.
|
||||
|
||||
This creates a new security_keys.txt that can be used to sign saves.
|
||||
Note: Saves signed with this key will trigger a trust dialog in the game.
|
||||
|
||||
Args:
|
||||
key_file: Path to save the new security_keys.txt
|
||||
|
||||
Returns:
|
||||
Path to the created key file
|
||||
"""
|
||||
try:
|
||||
import ecdsa
|
||||
except ImportError:
|
||||
raise ImportError("ecdsa library required. Install with: pip install ecdsa")
|
||||
|
||||
sk = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p)
|
||||
vk = sk.verifying_key
|
||||
|
||||
if vk is not None:
|
||||
sk_b64 = base64.b64encode(sk.to_der()).decode("ascii")
|
||||
vk_b64 = base64.b64encode(vk.to_der()).decode("ascii")
|
||||
line = f"signing-key {sk_b64} {vk_b64}\n"
|
||||
|
||||
os.makedirs(os.path.dirname(key_file) or ".", exist_ok=True)
|
||||
with open(key_file, "w") as f:
|
||||
f.write(line)
|
||||
|
||||
print(f"Generated new signing key: {key_file}")
|
||||
return key_file
|
||||
|
||||
raise RuntimeError("Failed to generate signing key")
|
||||
|
||||
|
||||
def dump_save_info(file_path: str):
|
||||
"""
|
||||
Print summary information about a save file.
|
||||
"""
|
||||
save_data = load_save_zip(file_path)
|
||||
|
||||
print(f"Save file: {file_path}")
|
||||
print("\n=== Metadata ===")
|
||||
print(f"Extra info: {save_data['extra_info']}")
|
||||
print(f"Ren'Py version: {save_data['renpy_version']}")
|
||||
print("\n=== JSON Metadata ===")
|
||||
for key, value in save_data["json"].items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
print("\n=== Contents ===")
|
||||
print(f"Screenshot: {'Yes' if save_data['screenshot'] else 'No'}")
|
||||
print(f"Log data size: {len(save_data['log'])} bytes")
|
||||
print(f"Has signatures: {'Yes' if save_data['signatures'] else 'No'}")
|
||||
87
third_party/python/rpycdec/src/rpycdec/stmts.py
vendored
Normal file
87
third_party/python/rpycdec/src/rpycdec/stmts.py
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
import io
|
||||
import logging
|
||||
import pickletools
|
||||
import struct
|
||||
import zlib
|
||||
from os import path
|
||||
|
||||
from renpy.ast import Node
|
||||
from rpycdec.safe_pickle import SafeUnpickler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# A string at the start of each rpycv2 file.
|
||||
RPYC2_HEADER = b"RENPY RPC2"
|
||||
|
||||
|
||||
def read_rpyc_data(file: io.BufferedReader, slot):
|
||||
"""
|
||||
Reads the binary data from `slot` in a .rpyc (v1 or v2) file. Returns
|
||||
the data if the slot exists, or None if the slot does not exist.
|
||||
"""
|
||||
header_data = file.read(1024)
|
||||
# Legacy path.
|
||||
if header_data[: len(RPYC2_HEADER)] != RPYC2_HEADER:
|
||||
if slot != 1:
|
||||
return None
|
||||
file.seek(0)
|
||||
data = file.read()
|
||||
return zlib.decompress(data)
|
||||
# RPYC2 path.
|
||||
pos = len(RPYC2_HEADER)
|
||||
while True:
|
||||
header_slot, start, length = struct.unpack("III", header_data[pos : pos + 12])
|
||||
if slot == header_slot:
|
||||
break
|
||||
if header_slot == 0:
|
||||
return None
|
||||
pos += 12
|
||||
file.seek(start)
|
||||
data = file.read(length)
|
||||
return zlib.decompress(data)
|
||||
|
||||
|
||||
def load(
|
||||
data: io.BufferedReader, slots: list[int] | None = None, **kwargs
|
||||
) -> list[Node] | None:
|
||||
"""Load Ren'Py AST from a .rpyc file.
|
||||
|
||||
Tries each slot in order. Slot 1 is the original script,
|
||||
slot 2 is the pre-translated version.
|
||||
"""
|
||||
if slots is None:
|
||||
slots = [1, 2]
|
||||
# 1 is statements before translation, 2 is after translation.
|
||||
for slot in slots:
|
||||
try:
|
||||
bindata = read_rpyc_data(data, slot)
|
||||
if not bindata:
|
||||
continue
|
||||
|
||||
if kwargs.get("dis", False):
|
||||
logger.info("Disassembling rpyc file...")
|
||||
pickletools.dis(bindata)
|
||||
|
||||
unpickler = SafeUnpickler(
|
||||
io.BytesIO(bindata), encoding="utf-8", errors="surrogateescape"
|
||||
)
|
||||
metadata, stmts = unpickler.load()
|
||||
return stmts
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read slot {slot}: {e}")
|
||||
data.seek(0)
|
||||
continue
|
||||
raise ValueError("Unsupported file format or invalid file")
|
||||
|
||||
|
||||
def load_file(filename, **kwargs) -> list[Node] | None:
|
||||
"""Load Ren'Py AST from a .rpyc/.rpymc file."""
|
||||
ext = path.splitext(filename)[1]
|
||||
if ext in [".rpy", ".rpym"]:
|
||||
raise NotImplementedError(
|
||||
"Parsing .rpy files is not supported. Use renpy.parser.parse() from Ren'Py's SDK."
|
||||
)
|
||||
# slot 2 is for pre-translated scripts, slot 1 is for normal scripts
|
||||
slots = [2, 1] if kwargs.get("pre_translated", False) else [1, 2]
|
||||
with open(filename, "rb") as file:
|
||||
return load(file, slots=slots, **kwargs)
|
||||
473
third_party/python/rpycdec/src/rpycdec/translate.py
vendored
Normal file
473
third_party/python/rpycdec/src/rpycdec/translate.py
vendored
Normal file
@@ -0,0 +1,473 @@
|
||||
import collections
|
||||
import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Callable, Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import renpy.ast
|
||||
from rpycdec import stmts, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Translation Extraction Data Structures
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class DialogueTranslation:
|
||||
"""Dialogue translation item - generates translate {identifier}: format"""
|
||||
|
||||
identifier: str # Translation identifier e.g. "start_a1b2c3d4"
|
||||
filename: str # Source file
|
||||
linenumber: int # Line number
|
||||
who: Optional[str] # Speaker
|
||||
what: str # Dialogue content
|
||||
code: str # Original code (for comments)
|
||||
|
||||
|
||||
@dataclass
|
||||
class StringTranslation:
|
||||
"""String translation item - generates translate strings: format"""
|
||||
|
||||
filename: str
|
||||
linenumber: int
|
||||
text: str # Text to translate
|
||||
source: str = "string" # Source: "menu", "string", "screen", etc.
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslationExtractor:
|
||||
"""Extract translation content from AST"""
|
||||
|
||||
label: Optional[str] = None
|
||||
identifiers: set[str] = field(default_factory=set)
|
||||
dialogues: list[DialogueTranslation] = field(default_factory=list)
|
||||
strings: list[StringTranslation] = field(default_factory=list)
|
||||
_seen_strings: set[str] = field(default_factory=set)
|
||||
|
||||
def extract_file(self, filename: str, nodes: list[renpy.ast.Node]) -> None:
|
||||
"""Extract translations from a single file"""
|
||||
for node in self._walk_nodes(nodes):
|
||||
self._process_node(node, filename)
|
||||
|
||||
def _walk_nodes(self, nodes: list[renpy.ast.Node]) -> Iterator[renpy.ast.Node]:
|
||||
"""Recursively walk all nodes"""
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
for node in nodes:
|
||||
yield node
|
||||
|
||||
# Handle different node types - each node type should only be processed once
|
||||
if isinstance(node, renpy.ast.If):
|
||||
# If node has entries with blocks
|
||||
for _, block in node.entries:
|
||||
if block:
|
||||
yield from self._walk_nodes(block)
|
||||
|
||||
elif isinstance(node, renpy.ast.Menu):
|
||||
# Menu node has items with blocks
|
||||
for _, _, block in node.items:
|
||||
if block:
|
||||
yield from self._walk_nodes(block)
|
||||
|
||||
elif isinstance(node, renpy.ast.UserStatement):
|
||||
# UserStatement may have code_block and subparses
|
||||
if hasattr(node, "code_block") and node.code_block:
|
||||
yield from self._walk_nodes(node.code_block)
|
||||
if hasattr(node, "subparses") and node.subparses:
|
||||
for subparse in node.subparses:
|
||||
if hasattr(subparse, "block") and subparse.block:
|
||||
yield from self._walk_nodes(subparse.block)
|
||||
|
||||
elif hasattr(node, "block") and node.block:
|
||||
# Generic block handling for Label, While, Init, Translate, etc.
|
||||
yield from self._walk_nodes(node.block)
|
||||
|
||||
def _process_node(self, node: renpy.ast.Node, filename: str) -> None:
|
||||
"""Process a single node"""
|
||||
# Track label
|
||||
if isinstance(node, renpy.ast.Label):
|
||||
if not getattr(node, "hide", False):
|
||||
name = (
|
||||
node.get_name()
|
||||
if hasattr(node, "get_name")
|
||||
else getattr(node, "name", None)
|
||||
)
|
||||
if name and isinstance(name, str) and not name.startswith("_"):
|
||||
self.label = name
|
||||
|
||||
# Say node -> dialogue translation
|
||||
if isinstance(node, renpy.ast.Say):
|
||||
self._extract_say(node, filename)
|
||||
|
||||
# TranslateSay node - already translated, skip
|
||||
elif isinstance(node, renpy.ast.TranslateSay):
|
||||
pass # Already a translation node, no need to extract
|
||||
|
||||
# Menu node -> string translation
|
||||
elif isinstance(node, renpy.ast.Menu):
|
||||
self._extract_menu(node, filename)
|
||||
|
||||
# UserStatement node -> string translation
|
||||
elif isinstance(node, renpy.ast.UserStatement):
|
||||
self._extract_user_statement(node, filename)
|
||||
|
||||
def _extract_say(self, node: renpy.ast.Say, filename: str) -> None:
|
||||
"""Extract Say node"""
|
||||
what = getattr(node, "what", None)
|
||||
if not what:
|
||||
return
|
||||
|
||||
identifier = self._generate_identifier(node)
|
||||
who = getattr(node, "who", None)
|
||||
|
||||
self.dialogues.append(
|
||||
DialogueTranslation(
|
||||
identifier=identifier,
|
||||
filename=filename,
|
||||
linenumber=node.linenumber,
|
||||
who=who,
|
||||
what=what,
|
||||
code=node.get_code(),
|
||||
)
|
||||
)
|
||||
|
||||
def _extract_menu(self, node: renpy.ast.Menu, filename: str) -> None:
|
||||
"""Extract Menu node options"""
|
||||
items = getattr(node, "items", [])
|
||||
for caption, condition, block in items:
|
||||
if caption and caption not in self._seen_strings:
|
||||
self._seen_strings.add(caption)
|
||||
# Calculate correct line number
|
||||
if block:
|
||||
line = block[0].linenumber - 1
|
||||
else:
|
||||
line = node.linenumber
|
||||
|
||||
self.strings.append(
|
||||
StringTranslation(
|
||||
filename=filename,
|
||||
linenumber=line,
|
||||
text=caption,
|
||||
source="menu",
|
||||
)
|
||||
)
|
||||
|
||||
def _extract_user_statement(
|
||||
self, node: renpy.ast.UserStatement, filename: str
|
||||
) -> None:
|
||||
"""Extract strings from UserStatement node"""
|
||||
try:
|
||||
translation_strings = node.get_translation_strings()
|
||||
for item in translation_strings:
|
||||
if isinstance(item, tuple):
|
||||
line, text = item
|
||||
else:
|
||||
line, text = node.linenumber, item
|
||||
|
||||
if text and text not in self._seen_strings:
|
||||
self._seen_strings.add(text)
|
||||
self.strings.append(
|
||||
StringTranslation(
|
||||
filename=filename,
|
||||
linenumber=line,
|
||||
text=text,
|
||||
source="user_statement",
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass # Some UserStatements may not implement get_translation_strings
|
||||
|
||||
def _generate_identifier(self, node: renpy.ast.Node) -> str:
|
||||
"""Generate translation identifier (Ren'Py compatible)"""
|
||||
# Check for explicit identifier
|
||||
explicit_id = getattr(node, "identifier", None)
|
||||
if explicit_id and getattr(node, "explicit_identifier", False):
|
||||
if explicit_id not in self.identifiers:
|
||||
self.identifiers.add(explicit_id)
|
||||
return explicit_id
|
||||
|
||||
# Calculate MD5
|
||||
code = node.get_code()
|
||||
md5 = hashlib.md5()
|
||||
md5.update((code + "\r\n").encode("utf-8"))
|
||||
digest = md5.hexdigest()[:8]
|
||||
|
||||
identifier = unique_identifier(self.label, digest, self.identifiers)
|
||||
self.identifiers.add(identifier)
|
||||
return identifier
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def unique_identifier(
|
||||
label: str | None, digest: str, existing_identifiers: set | None = None
|
||||
) -> str:
|
||||
"""Generate a unique translation identifier"""
|
||||
if existing_identifiers is None:
|
||||
existing_identifiers = set()
|
||||
if label is None:
|
||||
base = digest
|
||||
else:
|
||||
base = label.replace(".", "_") + "_" + digest
|
||||
i = 0
|
||||
suffix = ""
|
||||
while True:
|
||||
identifier = base + suffix
|
||||
if identifier not in existing_identifiers:
|
||||
break
|
||||
i += 1
|
||||
suffix = "_{0}".format(i)
|
||||
return identifier
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Translation File Generation
|
||||
# ============================================================================
|
||||
|
||||
|
||||
def quote_unicode(s: str) -> str:
|
||||
"""Escape special characters in string"""
|
||||
s = s.replace("\\", "\\\\")
|
||||
s = s.replace('"', '\\"')
|
||||
s = s.replace("\a", "\\a")
|
||||
s = s.replace("\b", "\\b")
|
||||
s = s.replace("\f", "\\f")
|
||||
s = s.replace("\n", "\\n")
|
||||
s = s.replace("\r", "\\r")
|
||||
s = s.replace("\t", "\\t")
|
||||
s = s.replace("\v", "\\v")
|
||||
return s
|
||||
|
||||
|
||||
def elide_filename(filename: str, game_dir: str = "") -> str:
|
||||
"""Simplify filename for comments"""
|
||||
if game_dir and filename.startswith(game_dir):
|
||||
filename = filename[len(game_dir) :].lstrip(os.sep)
|
||||
# Convert .rpyc to .rpy for display
|
||||
if filename.endswith(".rpyc"):
|
||||
filename = filename[:-1]
|
||||
elif filename.endswith(".rpymc"):
|
||||
filename = filename[:-1]
|
||||
return filename
|
||||
|
||||
|
||||
def get_translation_filename(source_filename: str) -> str:
|
||||
"""Generate translation filename from source filename, preserving directory structure"""
|
||||
# Remove .rpyc/.rpymc compiled suffix to get the source .rpy/.rpym extension
|
||||
if source_filename.endswith(".rpyc"):
|
||||
return source_filename[:-1]
|
||||
elif source_filename.endswith(".rpymc"):
|
||||
return source_filename[:-1]
|
||||
return source_filename
|
||||
|
||||
|
||||
def write_dialogue_translations(
|
||||
dialogues: list[DialogueTranslation],
|
||||
output_dir: str,
|
||||
language: str,
|
||||
game_dir: str = "",
|
||||
empty_translation: bool = True,
|
||||
filter_func: Optional[Callable[[str], str]] = None,
|
||||
) -> dict[str, int]:
|
||||
"""
|
||||
Generate dialogue translation files.
|
||||
|
||||
Returns:
|
||||
dict: {filename: count} number of translations written per file
|
||||
"""
|
||||
# Group by source file
|
||||
by_file: dict[str, list[DialogueTranslation]] = collections.defaultdict(list)
|
||||
for d in dialogues:
|
||||
tl_filename = get_translation_filename(d.filename)
|
||||
by_file[tl_filename].append(d)
|
||||
|
||||
result = {}
|
||||
|
||||
for tl_filename, items in by_file.items():
|
||||
tl_path = os.path.join(output_dir, tl_filename)
|
||||
os.makedirs(
|
||||
os.path.dirname(tl_path) if os.path.dirname(tl_path) else output_dir,
|
||||
exist_ok=True,
|
||||
)
|
||||
|
||||
with open(tl_path, "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
f"# TODO: Translation updated at {datetime.datetime.now().isoformat()}\n\n"
|
||||
)
|
||||
|
||||
for item in items:
|
||||
# Write source file location comment
|
||||
elided = elide_filename(item.filename, game_dir)
|
||||
f.write(f"# {elided}:{item.linenumber}\n")
|
||||
|
||||
# Write translate block
|
||||
f.write(f"translate {language} {item.identifier}:\n\n")
|
||||
|
||||
# Write original code as comment
|
||||
f.write(f" # {item.code}\n")
|
||||
|
||||
# Write translation content
|
||||
if empty_translation:
|
||||
new_what = ""
|
||||
elif filter_func:
|
||||
new_what = filter_func(item.what)
|
||||
else:
|
||||
new_what = item.what
|
||||
|
||||
# Generate translated code
|
||||
if item.who:
|
||||
f.write(f' {item.who} "{quote_unicode(new_what)}"\n')
|
||||
else:
|
||||
f.write(f' "{quote_unicode(new_what)}"\n')
|
||||
|
||||
f.write("\n")
|
||||
|
||||
result[tl_filename] = len(items)
|
||||
logger.info(f"Wrote {len(items)} dialogue translations to {tl_path}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def write_string_translations(
|
||||
strings: list[StringTranslation],
|
||||
output_dir: str,
|
||||
language: str,
|
||||
game_dir: str = "",
|
||||
empty_translation: bool = True,
|
||||
filter_func: Optional[Callable[[str], str]] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Generate string translation file.
|
||||
|
||||
Returns:
|
||||
int: number of translations written
|
||||
"""
|
||||
if not strings:
|
||||
return 0
|
||||
|
||||
tl_path = os.path.join(output_dir, "strings.rpy")
|
||||
|
||||
with open(tl_path, "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
f"# TODO: Translation updated at {datetime.datetime.now().isoformat()}\n\n"
|
||||
)
|
||||
f.write(f"translate {language} strings:\n\n")
|
||||
|
||||
for item in strings:
|
||||
# Write source file location comment
|
||||
elided = elide_filename(item.filename, game_dir)
|
||||
f.write(f" # {elided}:{item.linenumber}\n")
|
||||
|
||||
# Write old/new
|
||||
old_text = quote_unicode(item.text)
|
||||
if empty_translation:
|
||||
new_text = ""
|
||||
elif filter_func:
|
||||
new_text = quote_unicode(filter_func(item.text))
|
||||
else:
|
||||
new_text = old_text
|
||||
|
||||
f.write(f' old "{old_text}"\n')
|
||||
f.write(f' new "{new_text}"\n\n')
|
||||
|
||||
logger.info(f"Wrote {len(strings)} string translations to {tl_path}")
|
||||
return len(strings)
|
||||
|
||||
|
||||
def extract_translations(
|
||||
game_dir: str,
|
||||
output_dir: str,
|
||||
language: str,
|
||||
*,
|
||||
include_dialogues: bool = True,
|
||||
include_strings: bool = True,
|
||||
empty_translation: bool = True,
|
||||
filter_func: Optional[Callable[[str], str]] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Extract translation templates from .rpyc files.
|
||||
|
||||
Args:
|
||||
game_dir: Game directory (containing .rpyc files)
|
||||
output_dir: Output directory (will create tl/{language}/ structure)
|
||||
language: Target language code e.g. "chinese", "japanese"
|
||||
include_dialogues: Whether to extract dialogue translations
|
||||
include_strings: Whether to extract string translations
|
||||
empty_translation: True=new is empty, False=new copies old
|
||||
filter_func: Optional text filter/transform function
|
||||
|
||||
Returns:
|
||||
dict: Extraction statistics
|
||||
"""
|
||||
# Create output directory tl/{language}/
|
||||
tl_output_dir = os.path.join(output_dir, "tl", language)
|
||||
os.makedirs(tl_output_dir, exist_ok=True)
|
||||
|
||||
# Find all .rpyc/.rpymc files
|
||||
matches = utils.match_files(game_dir, r".*\.rpym?c$")
|
||||
logger.info(f"Found {len(matches)} rpyc files in {game_dir}")
|
||||
|
||||
# Create extractor
|
||||
extractor = TranslationExtractor()
|
||||
|
||||
# Process all files
|
||||
for filename in matches:
|
||||
filepath = os.path.join(game_dir, filename)
|
||||
logger.info(f"Processing {filename}")
|
||||
|
||||
try:
|
||||
loaded_stmts = stmts.load_file(filepath)
|
||||
if loaded_stmts:
|
||||
extractor.extract_file(filename, loaded_stmts)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process {filename}: {e}")
|
||||
continue
|
||||
|
||||
# Statistics
|
||||
stats = {
|
||||
"files_processed": len(matches),
|
||||
"dialogues_found": len(extractor.dialogues),
|
||||
"strings_found": len(extractor.strings),
|
||||
"dialogues_written": 0,
|
||||
"strings_written": 0,
|
||||
}
|
||||
|
||||
# Generate translation files
|
||||
if include_dialogues and extractor.dialogues:
|
||||
dialogue_stats = write_dialogue_translations(
|
||||
extractor.dialogues,
|
||||
tl_output_dir,
|
||||
language,
|
||||
game_dir=game_dir,
|
||||
empty_translation=empty_translation,
|
||||
filter_func=filter_func,
|
||||
)
|
||||
stats["dialogues_written"] = sum(dialogue_stats.values())
|
||||
stats["dialogue_files"] = dialogue_stats
|
||||
|
||||
if include_strings and extractor.strings:
|
||||
stats["strings_written"] = write_string_translations(
|
||||
extractor.strings,
|
||||
tl_output_dir,
|
||||
language,
|
||||
game_dir=game_dir,
|
||||
empty_translation=empty_translation,
|
||||
filter_func=filter_func,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Extraction complete: {stats['dialogues_written']} dialogues, "
|
||||
f"{stats['strings_written']} strings written to {tl_output_dir}"
|
||||
)
|
||||
|
||||
return stats
|
||||
56
third_party/python/rpycdec/src/rpycdec/utils.py
vendored
Normal file
56
third_party/python/rpycdec/src/rpycdec/utils.py
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def safe_path(base_dir: str, filename: str) -> str:
|
||||
"""Resolve *filename* under *base_dir* and guard against path traversal.
|
||||
|
||||
Returns the resolved absolute path. Raises ``ValueError`` if the
|
||||
resulting path escapes *base_dir*.
|
||||
"""
|
||||
root = os.path.realpath(base_dir)
|
||||
dest = os.path.realpath(os.path.join(base_dir, filename))
|
||||
# dest must be inside root (or equal to root itself)
|
||||
if dest != root and not dest.startswith(root + os.sep):
|
||||
raise ValueError(f"Path traversal detected: {filename!r}")
|
||||
return dest
|
||||
|
||||
|
||||
def write_file(filename: str, data: str):
|
||||
"""
|
||||
write data to file
|
||||
"""
|
||||
dir = os.path.dirname(filename)
|
||||
if dir and not os.path.exists(dir):
|
||||
os.makedirs(dir)
|
||||
with open(filename, "w", encoding="utf-8") as file:
|
||||
file.write(data)
|
||||
|
||||
|
||||
def match_files(base_dir: str, pattern: str) -> list[str]:
|
||||
"""
|
||||
match files in dir with regex pattern
|
||||
|
||||
Parameters
|
||||
----------
|
||||
base_dir : str
|
||||
directory to find in
|
||||
pattern : str
|
||||
regex pattern
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
matched filenames relative to base_dir
|
||||
"""
|
||||
|
||||
if pattern == "":
|
||||
pattern = ".*"
|
||||
results = []
|
||||
matched = re.compile(pattern)
|
||||
for root, _, files in os.walk(base_dir):
|
||||
for filename in files:
|
||||
filename = os.path.relpath(os.path.join(root, filename), base_dir)
|
||||
if matched.match(filename):
|
||||
results.append(filename)
|
||||
return results
|
||||
1
third_party/python/rpycdec/src/store/__init__.py
vendored
Normal file
1
third_party/python/rpycdec/src/store/__init__.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Hover = ''
|
||||
22
third_party/python/rpycdec/src/store/layeredimage.py
vendored
Normal file
22
third_party/python/rpycdec/src/store/layeredimage.py
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
class RawLayeredImage:
|
||||
pass
|
||||
|
||||
|
||||
class RawAlways:
|
||||
pass
|
||||
|
||||
|
||||
class RawAttributeGroup:
|
||||
pass
|
||||
|
||||
|
||||
class RawAttribute:
|
||||
pass
|
||||
|
||||
|
||||
class RawConditionGroup:
|
||||
pass
|
||||
|
||||
|
||||
class RawCondition:
|
||||
pass
|
||||
8
third_party/python/rpycdec/tests/.gitignore
vendored
Normal file
8
third_party/python/rpycdec/tests/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
*.swp
|
||||
*.pyc
|
||||
*.rpyc
|
||||
__pycache__
|
||||
saves
|
||||
cache
|
||||
*_decompiled
|
||||
log.txt
|
||||
109
third_party/python/rpycdec/tests/test_ast.py
vendored
Normal file
109
third_party/python/rpycdec/tests/test_ast.py
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Tests for renpy.ast node code generation"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
import renpy.ast as ast
|
||||
|
||||
|
||||
def make_pycode(code_str):
|
||||
"""Create a PyCode object with the given source string."""
|
||||
pycode = ast.PyCode()
|
||||
pycode.state = (None, code_str, None, None, None)
|
||||
return pycode
|
||||
|
||||
|
||||
class TestPythonGetCode(unittest.TestCase):
|
||||
"""Tests for Python.get_code() keyword order and correctness."""
|
||||
|
||||
def _make_node(self, code_str, store=None, hide=None):
|
||||
node = ast.Python(loc=("script.rpyc", 1))
|
||||
node.code = make_pycode(code_str)
|
||||
if store is not None:
|
||||
node.store = store
|
||||
if hide is not None:
|
||||
node.hide = hide
|
||||
return node
|
||||
|
||||
def test_single_line_shorthand(self):
|
||||
"""Single-line Python without store/hide uses $ shorthand."""
|
||||
node = self._make_node("flag = True")
|
||||
self.assertEqual(node.get_code(), "$ flag = True")
|
||||
|
||||
def test_hide_before_store(self):
|
||||
"""'hide' must appear before 'in <store>' per Ren'Py grammar."""
|
||||
node = self._make_node("flag = True", hide=True, store="store.mystore")
|
||||
result = node.get_code()
|
||||
self.assertIn("python hide in mystore:", result)
|
||||
|
||||
def test_hide_only(self):
|
||||
"""python hide: block without store."""
|
||||
node = self._make_node("flag = True", hide=True)
|
||||
result = node.get_code()
|
||||
self.assertTrue(result.startswith("python hide:"))
|
||||
|
||||
def test_store_only(self):
|
||||
"""python in <store>: block without hide."""
|
||||
node = self._make_node("flag = True", store="store.mystore")
|
||||
result = node.get_code()
|
||||
self.assertTrue(result.startswith("python in mystore:"))
|
||||
|
||||
def test_multiline(self):
|
||||
"""Multiline code produces a python: block."""
|
||||
node = self._make_node("flag = True\nother = False")
|
||||
result = node.get_code()
|
||||
self.assertIn("python:", result)
|
||||
|
||||
|
||||
class TestEarlyPythonGetCode(unittest.TestCase):
|
||||
"""Tests for EarlyPython.get_code() — keyword order and missing-attribute safety."""
|
||||
|
||||
def _make_node(self, code_str, store=None, hide=None):
|
||||
node = ast.EarlyPython(loc=("script.rpyc", 1))
|
||||
node.code = make_pycode(code_str)
|
||||
if store is not None:
|
||||
node.store = store
|
||||
if hide is not None:
|
||||
node.hide = hide
|
||||
return node
|
||||
|
||||
def test_single_line_no_hide_attr(self):
|
||||
"""EarlyPython without 'hide' attribute should not raise AttributeError."""
|
||||
node = self._make_node("flag = True")
|
||||
self.assertEqual(node.get_code(), "$ flag = True")
|
||||
|
||||
def test_single_line_hide_false(self):
|
||||
"""EarlyPython with hide=False should use $ shorthand."""
|
||||
node = self._make_node("flag = True", hide=False)
|
||||
self.assertEqual(node.get_code(), "$ flag = True")
|
||||
|
||||
def test_hide_before_store(self):
|
||||
"""'hide' must appear before 'in <store>' per Ren'Py grammar."""
|
||||
node = self._make_node("flag = True", hide=True, store="store.mystore")
|
||||
result = node.get_code()
|
||||
self.assertIn("python early hide in mystore:", result)
|
||||
|
||||
def test_hide_only(self):
|
||||
"""python early hide: block without store."""
|
||||
node = self._make_node("flag = True", hide=True)
|
||||
result = node.get_code()
|
||||
self.assertTrue(result.startswith("python early hide:"))
|
||||
|
||||
def test_store_only(self):
|
||||
"""python early in <store>: block without hide."""
|
||||
node = self._make_node("flag = True", store="store.mystore")
|
||||
result = node.get_code()
|
||||
self.assertTrue(result.startswith("python early in mystore:"))
|
||||
|
||||
def test_multiline(self):
|
||||
"""Multiline code should emit 'python early:' block."""
|
||||
node = self._make_node("flag = True\nother = False")
|
||||
result = node.get_code()
|
||||
self.assertIn("python early:", result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
187
third_party/python/rpycdec/tests/test_rpa.py
vendored
Normal file
187
third_party/python/rpycdec/tests/test_rpa.py
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
|
||||
from rpycdec.rpa import create_rpa, extract_rpa
|
||||
|
||||
SRC_DIR = str(Path(__file__).resolve().parents[1] / "src")
|
||||
|
||||
|
||||
class RPATests(unittest.TestCase):
|
||||
def create_filter_fixture(self, root: Path) -> Path:
|
||||
source = root / "game"
|
||||
(source / "images").mkdir(parents=True)
|
||||
(source / "scripts").mkdir()
|
||||
(source / "script.rpy").write_text('label start:\n "Hello"\n')
|
||||
(source / "scripts" / "chapter.RPYC").write_bytes(b"compiled")
|
||||
(source / "images" / "bg.png").write_bytes(b"image")
|
||||
(source / "notes.txt").write_text("notes")
|
||||
|
||||
archive = root / "archive.rpa"
|
||||
create_rpa(str(archive), [str(source)])
|
||||
return archive
|
||||
|
||||
def test_create_rpa_round_trips_directory(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
source = root / "game"
|
||||
(source / "images").mkdir(parents=True)
|
||||
(source / "script.rpy").write_text('label start:\n "Hello"\n')
|
||||
(source / "images" / "bg.png").write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
|
||||
archive = root / "archive.rpa"
|
||||
create_rpa(str(archive), [str(source)])
|
||||
|
||||
output = root / "out"
|
||||
with archive.open("rb") as f:
|
||||
extract_rpa(f, str(output))
|
||||
|
||||
self.assertEqual(
|
||||
(output / "script.rpy").read_text(), 'label start:\n "Hello"\n'
|
||||
)
|
||||
self.assertEqual(
|
||||
(output / "images" / "bg.png").read_bytes(), b"\x89PNG\r\n\x1a\n"
|
||||
)
|
||||
|
||||
def test_rpa_cli_creates_archive_from_file(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
source = root / "00unlock.rpy"
|
||||
source.write_text("define unlock = True\n")
|
||||
archive = root / "unlock.rpa"
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = SRC_DIR + os.pathsep + env.get("PYTHONPATH", "")
|
||||
|
||||
result = run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rpycdec",
|
||||
"rpa",
|
||||
"-o",
|
||||
str(archive),
|
||||
str(source),
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
output = root / "out"
|
||||
with archive.open("rb") as f:
|
||||
extract_rpa(f, str(output))
|
||||
|
||||
self.assertEqual(
|
||||
(output / "00unlock.rpy").read_text(), "define unlock = True\n"
|
||||
)
|
||||
|
||||
def test_extract_rpa_filters_by_multiple_suffixes_case_insensitively(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
archive = self.create_filter_fixture(root)
|
||||
output = root / "out"
|
||||
|
||||
with archive.open("rb") as f:
|
||||
extract_rpa(f, str(output), suffixes=[".rpy", ".rpyc"])
|
||||
|
||||
self.assertTrue((output / "script.rpy").is_file())
|
||||
self.assertTrue((output / "scripts" / "chapter.RPYC").is_file())
|
||||
self.assertFalse((output / "images" / "bg.png").exists())
|
||||
self.assertFalse((output / "notes.txt").exists())
|
||||
|
||||
def test_extract_rpa_accepts_a_single_filter_string(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
archive = self.create_filter_fixture(root)
|
||||
output = root / "out"
|
||||
|
||||
with archive.open("rb") as f:
|
||||
extract_rpa(f, str(output), suffixes=".rpy")
|
||||
|
||||
self.assertTrue((output / "script.rpy").is_file())
|
||||
self.assertFalse((output / "scripts" / "chapter.RPYC").exists())
|
||||
|
||||
def test_extract_rpa_combines_expressions_and_suffixes_with_or(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
archive = self.create_filter_fixture(root)
|
||||
output = root / "out"
|
||||
|
||||
with archive.open("rb") as f:
|
||||
extract_rpa(
|
||||
f,
|
||||
str(output),
|
||||
expressions=[r"^images/.*\.png$"],
|
||||
suffixes=[".rpy"],
|
||||
)
|
||||
|
||||
self.assertTrue((output / "script.rpy").is_file())
|
||||
self.assertTrue((output / "images" / "bg.png").is_file())
|
||||
self.assertFalse((output / "scripts" / "chapter.RPYC").exists())
|
||||
self.assertFalse((output / "notes.txt").exists())
|
||||
|
||||
def test_unrpa_cli_supports_expression_and_multiple_suffixes(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
root = Path(td)
|
||||
archive = self.create_filter_fixture(root)
|
||||
output = root / "out"
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = SRC_DIR + os.pathsep + env.get("PYTHONPATH", "")
|
||||
|
||||
result = run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rpycdec",
|
||||
"unrpa",
|
||||
str(archive),
|
||||
"-o",
|
||||
str(output),
|
||||
"-e",
|
||||
r"^images/",
|
||||
"-s",
|
||||
".rpy",
|
||||
".rpyc",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue((output / "script.rpy").is_file())
|
||||
self.assertTrue((output / "scripts" / "chapter.RPYC").is_file())
|
||||
self.assertTrue((output / "images" / "bg.png").is_file())
|
||||
self.assertFalse((output / "notes.txt").exists())
|
||||
|
||||
def test_unrpa_cli_rejects_invalid_expression(self):
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = SRC_DIR + os.pathsep + env.get("PYTHONPATH", "")
|
||||
result = run(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"rpycdec",
|
||||
"unrpa",
|
||||
"archive.rpa",
|
||||
"-e",
|
||||
"[",
|
||||
],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn("invalid regular expression", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
296
third_party/python/rpycdec/tests/test_translate.py
vendored
Normal file
296
third_party/python/rpycdec/tests/test_translate.py
vendored
Normal file
@@ -0,0 +1,296 @@
|
||||
"""Tests for rpycdec.translate module"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
import renpy.ast
|
||||
from rpycdec.translate import (
|
||||
DialogueTranslation,
|
||||
StringTranslation,
|
||||
TranslationExtractor,
|
||||
elide_filename,
|
||||
get_translation_filename,
|
||||
quote_unicode,
|
||||
unique_identifier,
|
||||
write_dialogue_translations,
|
||||
write_string_translations,
|
||||
)
|
||||
|
||||
|
||||
class TestGetTranslationFilename(unittest.TestCase):
|
||||
"""Tests for get_translation_filename()"""
|
||||
|
||||
def test_rpyc_to_rpy(self):
|
||||
self.assertEqual(get_translation_filename("script.rpyc"), "script.rpy")
|
||||
|
||||
def test_rpymc_to_rpym(self):
|
||||
self.assertEqual(get_translation_filename("script.rpymc"), "script.rpym")
|
||||
|
||||
def test_subdirectory_preserved(self):
|
||||
"""Directory structure should be preserved (was a bug: basename stripped dirs)"""
|
||||
self.assertEqual(get_translation_filename("sub/intro.rpyc"), "sub/intro.rpy")
|
||||
|
||||
def test_nested_subdirectory_preserved(self):
|
||||
self.assertEqual(
|
||||
get_translation_filename("a/b/c/script.rpyc"), "a/b/c/script.rpy"
|
||||
)
|
||||
|
||||
def test_rpym_unchanged(self):
|
||||
self.assertEqual(get_translation_filename("script.rpym"), "script.rpym")
|
||||
|
||||
def test_rpy_unchanged(self):
|
||||
self.assertEqual(get_translation_filename("script.rpy"), "script.rpy")
|
||||
|
||||
|
||||
class TestElideFilename(unittest.TestCase):
|
||||
def test_strips_game_dir(self):
|
||||
result = elide_filename("/game/script.rpyc", "/game")
|
||||
self.assertEqual(result, "script.rpy")
|
||||
|
||||
def test_rpymc_to_rpym(self):
|
||||
result = elide_filename("/game/script.rpymc", "/game")
|
||||
self.assertEqual(result, "script.rpym")
|
||||
|
||||
def test_no_game_dir(self):
|
||||
result = elide_filename("script.rpyc")
|
||||
self.assertEqual(result, "script.rpy")
|
||||
|
||||
|
||||
class TestQuoteUnicode(unittest.TestCase):
|
||||
def test_backslash(self):
|
||||
self.assertEqual(quote_unicode("a\\b"), "a\\\\b")
|
||||
|
||||
def test_double_quote(self):
|
||||
self.assertEqual(quote_unicode('say "hi"'), 'say \\"hi\\"')
|
||||
|
||||
def test_newline(self):
|
||||
self.assertEqual(quote_unicode("line1\nline2"), "line1\\nline2")
|
||||
|
||||
def test_plain_string(self):
|
||||
self.assertEqual(quote_unicode("hello"), "hello")
|
||||
|
||||
|
||||
class TestUniqueIdentifier(unittest.TestCase):
|
||||
def test_no_label(self):
|
||||
result = unique_identifier(None, "abcd1234")
|
||||
self.assertEqual(result, "abcd1234")
|
||||
|
||||
def test_with_label(self):
|
||||
result = unique_identifier("start", "abcd1234")
|
||||
self.assertEqual(result, "start_abcd1234")
|
||||
|
||||
def test_label_dot_replaced(self):
|
||||
result = unique_identifier("chapter.one", "abcd1234")
|
||||
self.assertEqual(result, "chapter_one_abcd1234")
|
||||
|
||||
def test_collision_appends_index(self):
|
||||
existing = {"start_abcd1234"}
|
||||
result = unique_identifier("start", "abcd1234", existing)
|
||||
self.assertEqual(result, "start_abcd1234_1")
|
||||
|
||||
def test_multiple_collisions(self):
|
||||
existing = {"start_abcd1234", "start_abcd1234_1"}
|
||||
result = unique_identifier("start", "abcd1234", existing)
|
||||
self.assertEqual(result, "start_abcd1234_2")
|
||||
|
||||
|
||||
class TestTranslationExtractor(unittest.TestCase):
|
||||
def _make_say_node(self, who, what, filename="script.rpyc", linenumber=1):
|
||||
node = renpy.ast.Say(loc=(filename, linenumber))
|
||||
node.who = who
|
||||
node.what = what
|
||||
node.interact = True
|
||||
node.identifier = None
|
||||
node.explicit_identifier = False
|
||||
node.attributes = None
|
||||
node.temporary_attributes = None
|
||||
node.arguments = None
|
||||
node.with_ = None
|
||||
return node
|
||||
|
||||
def test_extract_say_dialogue(self):
|
||||
extractor = TranslationExtractor()
|
||||
node = self._make_say_node("e", "Hello world.")
|
||||
extractor.extract_file("script.rpyc", [node])
|
||||
self.assertEqual(len(extractor.dialogues), 1)
|
||||
d = extractor.dialogues[0]
|
||||
self.assertEqual(d.who, "e")
|
||||
self.assertEqual(d.what, "Hello world.")
|
||||
self.assertEqual(d.filename, "script.rpyc")
|
||||
|
||||
def test_extract_say_no_speaker(self):
|
||||
extractor = TranslationExtractor()
|
||||
node = self._make_say_node(None, "A narrator line.")
|
||||
extractor.extract_file("script.rpyc", [node])
|
||||
self.assertEqual(len(extractor.dialogues), 1)
|
||||
self.assertIsNone(extractor.dialogues[0].who)
|
||||
|
||||
def test_no_say_no_dialogue(self):
|
||||
extractor = TranslationExtractor()
|
||||
extractor.extract_file("script.rpyc", [])
|
||||
self.assertEqual(len(extractor.dialogues), 0)
|
||||
self.assertEqual(len(extractor.strings), 0)
|
||||
|
||||
def test_identifier_generated(self):
|
||||
extractor = TranslationExtractor()
|
||||
node = self._make_say_node("e", "Hello.")
|
||||
extractor.extract_file("script.rpyc", [node])
|
||||
self.assertIsNotNone(extractor.dialogues[0].identifier)
|
||||
self.assertIsInstance(extractor.dialogues[0].identifier, str)
|
||||
|
||||
|
||||
class TestWriteDialogueTranslations(unittest.TestCase):
|
||||
def test_writes_dialogue_file(self):
|
||||
dialogues = [
|
||||
DialogueTranslation(
|
||||
identifier="start_abcd1234",
|
||||
filename="script.rpyc",
|
||||
linenumber=5,
|
||||
who="e",
|
||||
what="Hello.",
|
||||
code='e "Hello."',
|
||||
)
|
||||
]
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
write_dialogue_translations(
|
||||
dialogues, tmpdir, "chinese", empty_translation=False
|
||||
)
|
||||
tl_file = os.path.join(tmpdir, "script.rpy")
|
||||
self.assertTrue(os.path.exists(tl_file))
|
||||
with open(tl_file) as f:
|
||||
content = f.read()
|
||||
self.assertIn("translate chinese start_abcd1234:", content)
|
||||
self.assertIn('e "Hello."', content)
|
||||
|
||||
def test_preserves_subdirectory_structure(self):
|
||||
"""Translation files should mirror the source directory structure"""
|
||||
dialogues = [
|
||||
DialogueTranslation(
|
||||
identifier="intro_abcd1234",
|
||||
filename="sub/intro.rpyc",
|
||||
linenumber=3,
|
||||
who=None,
|
||||
what="Welcome.",
|
||||
code='"Welcome."',
|
||||
)
|
||||
]
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
write_dialogue_translations(
|
||||
dialogues, tmpdir, "chinese", empty_translation=False
|
||||
)
|
||||
tl_file = os.path.join(tmpdir, "sub", "intro.rpy")
|
||||
self.assertTrue(
|
||||
os.path.exists(tl_file),
|
||||
f"Expected {tl_file} to exist (subdirectory structure not preserved)",
|
||||
)
|
||||
|
||||
def test_empty_translation(self):
|
||||
dialogues = [
|
||||
DialogueTranslation(
|
||||
identifier="start_abcd1234",
|
||||
filename="script.rpyc",
|
||||
linenumber=1,
|
||||
who="e",
|
||||
what="Hello.",
|
||||
code='e "Hello."',
|
||||
)
|
||||
]
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
write_dialogue_translations(
|
||||
dialogues, tmpdir, "chinese", empty_translation=True
|
||||
)
|
||||
with open(os.path.join(tmpdir, "script.rpy")) as f:
|
||||
content = f.read()
|
||||
self.assertIn('e ""', content)
|
||||
|
||||
|
||||
class TestWriteStringTranslations(unittest.TestCase):
|
||||
def test_writes_strings_file(self):
|
||||
strings = [StringTranslation(filename="script.rpyc", linenumber=2, text="Play")]
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
count = write_string_translations(
|
||||
strings, tmpdir, "chinese", empty_translation=False
|
||||
)
|
||||
self.assertEqual(count, 1)
|
||||
tl_file = os.path.join(tmpdir, "strings.rpy")
|
||||
self.assertTrue(os.path.exists(tl_file))
|
||||
with open(tl_file) as f:
|
||||
content = f.read()
|
||||
self.assertIn("translate chinese strings:", content)
|
||||
self.assertIn('old "Play"', content)
|
||||
self.assertIn('new "Play"', content)
|
||||
|
||||
def test_empty_strings_not_written(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
count = write_string_translations([], tmpdir, "chinese")
|
||||
self.assertEqual(count, 0)
|
||||
self.assertFalse(os.path.exists(os.path.join(tmpdir, "strings.rpy")))
|
||||
|
||||
|
||||
class TestCLIExtractTranslate(unittest.TestCase):
|
||||
"""Integration tests for the extract-translate CLI command"""
|
||||
|
||||
def test_language_argument_recognized(self):
|
||||
"""The --language argument must be recognized by the CLI parser"""
|
||||
from rpycdec.cli import main
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sys.argv = [
|
||||
"rpycdec",
|
||||
"extract-translate",
|
||||
tmpdir,
|
||||
"--language",
|
||||
"chinese",
|
||||
]
|
||||
# Should not raise SystemExit with non-zero code (unrecognized arguments)
|
||||
try:
|
||||
main()
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
self.fail(
|
||||
f"CLI raised SystemExit({e.code}) - --language may not be recognized"
|
||||
)
|
||||
|
||||
def test_language_short_flag(self):
|
||||
"""The -l short flag must also work"""
|
||||
from rpycdec.cli import main
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
sys.argv = ["rpycdec", "extract-translate", tmpdir, "-l", "japanese"]
|
||||
try:
|
||||
main()
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
self.fail(
|
||||
f"CLI raised SystemExit({e.code}) - -l flag may not be recognized"
|
||||
)
|
||||
|
||||
def test_creates_tl_directory(self):
|
||||
"""extract-translate must create tl/<language>/ directory in output"""
|
||||
from rpycdec.cli import main
|
||||
|
||||
with tempfile.TemporaryDirectory() as game_dir:
|
||||
with tempfile.TemporaryDirectory() as out_dir:
|
||||
sys.argv = [
|
||||
"rpycdec",
|
||||
"extract-translate",
|
||||
game_dir,
|
||||
"--language",
|
||||
"chinese",
|
||||
"--output",
|
||||
out_dir,
|
||||
]
|
||||
main()
|
||||
tl_dir = os.path.join(out_dir, "tl", "chinese")
|
||||
self.assertTrue(
|
||||
os.path.isdir(tl_dir),
|
||||
f"Expected tl/chinese/ directory at {tl_dir}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
5
third_party/python/rpycdec_bin.py
vendored
Normal file
5
third_party/python/rpycdec_bin.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from rpycdec.cli import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user