rpgsync 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rpgsync-0.2.0/CHANGELOG.md +95 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/PKG-INFO +4 -7
- {rpgsync-0.1.0 → rpgsync-0.2.0}/README.md +3 -6
- {rpgsync-0.1.0 → rpgsync-0.2.0}/pyproject.toml +1 -1
- {rpgsync-0.1.0 → rpgsync-0.2.0}/pyproject.toml.orig +11 -9
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/cli.py +69 -20
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/commands.py +142 -18
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/compat.py +9 -1
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/database.py +166 -4
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/db.py +768 -1
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/dbschema.py +74 -40
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/dsl.py +482 -36
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/dynparams.py +3 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/dynrpg.py +70 -2
- rpgsync-0.2.0/rpgsync/layout.py +241 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/lcf.py +15 -12
- rpgsync-0.2.0/rpgsync/maniac.py +1195 -0
- rpgsync-0.2.0/rpgsync/maniac_ext.py +367 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/named.py +21 -1
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/project.py +22 -1
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/pyexpr.py +463 -23
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/resources.py +53 -10
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/scaffold.py +26 -3
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/script.py +150 -34
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/sync.py +77 -4
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/tables.py +27 -5
- rpgsync-0.1.0/CHANGELOG.md +0 -29
- rpgsync-0.1.0/rpgsync/maniac.py +0 -387
- {rpgsync-0.1.0 → rpgsync-0.2.0}/LICENSE +0 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/__init__.py +0 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/__main__.py +0 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/fmt.py +0 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/maptree.py +0 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/messages.py +0 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/py.typed +0 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/scriptfiles.py +0 -0
- {rpgsync-0.1.0 → rpgsync-0.2.0}/rpgsync/venv.py +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Versions
|
|
4
|
+
|
|
5
|
+
rpgsync is released on [PyPI](https://pypi.org/project/rpgsync/) as `0.MINOR.PATCH`:
|
|
6
|
+
|
|
7
|
+
- **MINOR** goes up when the scripts change: `rpgsync pull` writes a game's scripts
|
|
8
|
+
differently (another layout, other names, another way to write a command), or scripts
|
|
9
|
+
written by it may not work with the previous minor version (a DSL name renamed or
|
|
10
|
+
removed).
|
|
11
|
+
- **PATCH** goes up for everything else: fixes and additions after which `rpgsync pull`
|
|
12
|
+
writes the same scripts.
|
|
13
|
+
|
|
14
|
+
A game's `Scripts/pyproject.toml` depends on the version that set it up,
|
|
15
|
+
`rpgsync~=0.MINOR.PATCH` (that version and its later patches), and its `Scripts/uv.lock`
|
|
16
|
+
pins the exact one. [Upgrading rpgsync](README.md#upgrading-rpgsync) moves a game to a
|
|
17
|
+
newer one; [Releasing](docs/development.md#releasing) makes a new one.
|
|
18
|
+
|
|
19
|
+
## 0.2.0
|
|
20
|
+
|
|
21
|
+
The scripts of Maniac Patch games change: `rpgsync pull` writes them differently.
|
|
22
|
+
|
|
23
|
+
- Maniac Patch commands have typed, named forms: `maniac.show_string_picture(...)`,
|
|
24
|
+
`maniac.console_print(...)` & co, `maniac.get_media_option(...)` /
|
|
25
|
+
`set_media_option(...)`, `maniac.get_game_info("map_size", into=...)`, and
|
|
26
|
+
`maniac.script("line 1", "line 2", results=[...])` holding a script and its further
|
|
27
|
+
lines in one call. `write_picture` with all its parameters zero is no longer raw.
|
|
28
|
+
- String variables: `database/strings.py` names them, and Control Strings are Python
|
|
29
|
+
statements on `strings` (`strings.name = "Alex"`, `+=`, `str(variables.n).rjust(4, "0")`,
|
|
30
|
+
`db_name("item", 3)`, `read_file(...)`, `.replace()`, `int()`, `len()`, `.find()`,
|
|
31
|
+
`.split(into=...)`, `.pop_line()`, `.to_file()`), and Conditional Branch compares them
|
|
32
|
+
(`if strings.name == "Alex":`, `"x" in strings[1]`).
|
|
33
|
+
- Maniac options of standard commands are Python: Control Variables operands and
|
|
34
|
+
operations (`variables.x = variables.a * 2`, `sqrt()`, `sin()`, `min()`, `abs()`,
|
|
35
|
+
`lerp()`, `array_sum()`, `party[0].hp`, `game.frames`, `switches[n]`, `a if b > c else d`,
|
|
36
|
+
`variables[10:1:-1]` backwards ranges, `variables[variables.a:variables.b + 1]`), loops
|
|
37
|
+
(`for variables.i in range(1, 11):`, `while variables.hp > 0:`, `continue`,
|
|
38
|
+
`break_loop(levels=2)`), `wait(frames=...)`, `common_events[variables.n]()`,
|
|
39
|
+
`events[n].erase()` / `.restore()`, names and values from variables in `play_bgm()`,
|
|
40
|
+
`play_se()`, `face()`, `message_options(width=..., font=...)`, pixel scrolling
|
|
41
|
+
(`pan_screen_by()`, `pan_screen_to()`), `call_system_function()`,
|
|
42
|
+
`disable_system_functions()`.
|
|
43
|
+
- The Maniac Patch's editor stores some standard commands with other parameters (a
|
|
44
|
+
Control Variables of one variable with 0 as last id, zero parameters more on loops,
|
|
45
|
+
faces without their trailing zeros...): rpgsync detects the layout most of a game
|
|
46
|
+
uses, writes the same Python for both, and marks a command stored the other way
|
|
47
|
+
with `# layout: maniac` / `# layout: rpg_maker`. A 2003 game's Wait without its key
|
|
48
|
+
parameter is `wait(1.0, key=None)` instead of a raw line.
|
|
49
|
+
- `database/terms.py` and `database/system.py` export the Terms and the System settings
|
|
50
|
+
(generated from liblcf's schema: a field named there appears once rpgsync's schema
|
|
51
|
+
is generated again; chunks it doesn't know are kept). The Maniac Patch's System
|
|
52
|
+
fields are named: title and save scene hooks, message window font, size, spacing and
|
|
53
|
+
face layout. Skill `failure_message` 3 (Maniac's Hit Rate Calc) and `-9` in
|
|
54
|
+
`menu_commands` (Game End hidden) are documented.
|
|
55
|
+
- Common events run on `"battle_start"` and `"battle_parallel"` (Maniac Patch triggers
|
|
56
|
+
6 and 7).
|
|
57
|
+
- `rpgsync check`: in a game declaring `"maniac"`, `database/game_types.py` lets
|
|
58
|
+
`variables[n]` stand where the patch reads a value (`randint(1, variables.max)`);
|
|
59
|
+
without it, Maniac options of standard commands are reported as warnings.
|
|
60
|
+
- `tools/gen_dbschema.py` reads the EasyRPG Editor's liblcf (`../Editor/lib/liblcf`) by
|
|
61
|
+
default.
|
|
62
|
+
|
|
63
|
+
## 0.1.2
|
|
64
|
+
|
|
65
|
+
- `rpgsync check --resources` names every file in its warnings, instead of the first 8 and
|
|
66
|
+
how many more: the files the database's entries use, sorted, with their number, and
|
|
67
|
+
the imported files never used. A list too long for its line goes under it, wrapped to
|
|
68
|
+
the terminal's width without cutting a name. A file used several times in one game file
|
|
69
|
+
(`Map0003.lmu`) is no longer named again in "also used at".
|
|
70
|
+
- `rpgsync check --resources` reads a name DynParams builds for a command
|
|
71
|
+
(`pictures[2].show(f"w{variables.sheet}")`) whole, in that command's folder, numbers
|
|
72
|
+
from variables as digits: `Picture/w4.png` counts as used, as a name of one letter
|
|
73
|
+
then a number is no longer too short to tell, and `Picture/wall.png` doesn't. The name
|
|
74
|
+
of a command whose parameters alone DynParams rewrites is checked like any other.
|
|
75
|
+
|
|
76
|
+
## 0.1.1
|
|
77
|
+
|
|
78
|
+
- `rpgsync check`: the pages that one later page keeps from ever being active are
|
|
79
|
+
named in one warning, in fewer words.
|
|
80
|
+
- `rpgsync check` names files relative to the current folder (`Scripts/Map0004.py:12`
|
|
81
|
+
run from the game folder), ty's problems too, so terminals and editors can open them.
|
|
82
|
+
- `rpgsync check --resources`: a name ending with a space (`"Theme "`, whose file is
|
|
83
|
+
`Theme .mp3`) is found, instead of reported as not imported and its file as never used.
|
|
84
|
+
- rpgsync knows CortiXCustomCrit.dll (Corti's critical hits): its `dyn.ccrit_*`
|
|
85
|
+
commands, and its `[CortiCustomCrit]` and `[CortiLog]` sections of `DynRPG.ini`.
|
|
86
|
+
|
|
87
|
+
## 0.1.0
|
|
88
|
+
|
|
89
|
+
First release on PyPI.
|
|
90
|
+
|
|
91
|
+
- A new game's scripts depend on `rpgsync~=0.1.0` from PyPI, pinned by their `uv.lock`,
|
|
92
|
+
instead of a link to this computer's rpgsync folder.
|
|
93
|
+
- A developer of rpgsync links their rpgsync folder as `Scripts/.rpgsync/rpgsync`:
|
|
94
|
+
rpgsync installs it, editable, in the scripts' environment, and leaves
|
|
95
|
+
`pyproject.toml` and `uv.lock` as they are.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rpgsync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A Python twin of your RPG Maker 2000/2003 game
|
|
5
5
|
Keywords: rpg-maker,rpg-maker-2000,rpg-maker-2003,easyrpg,gamedev,dsl
|
|
6
6
|
Author: Nicolas Oulianov
|
|
@@ -40,8 +40,9 @@ A Python twin of your RPG Maker 2000/2003 game.
|
|
|
40
40
|
- Implement automated tests to verify game mechanics
|
|
41
41
|
- Improve your game using the modern developer's toolkit
|
|
42
42
|
|
|
43
|
+
<img width="1920" height="754" alt="rpgsync" src="https://github.com/user-attachments/assets/11a3fc5a-7884-4c7c-9aef-ee9ee54aa4bc" />
|
|
43
44
|
|
|
44
|
-
##
|
|
45
|
+
## Example
|
|
45
46
|
|
|
46
47
|
```python
|
|
47
48
|
@event(10, "Jeremy", x=15, y=38)
|
|
@@ -81,11 +82,7 @@ class Ulysse(Event): # The other event!
|
|
|
81
82
|
pass
|
|
82
83
|
```
|
|
83
84
|
|
|
84
|
-
Every command, option and value is typed and documented
|
|
85
|
-
[`rpgsync/dsl.py`](https://github.com/oulianov/rpgsync/blob/master/rpgsync/dsl.py): your
|
|
86
|
-
editor shows the docs on hover and flags wrong values (a misspelled direction, a sprite
|
|
87
|
-
index above 7, ...). [Writing scripts](https://github.com/oulianov/rpgsync/blob/master/docs/scripts.md)
|
|
88
|
-
explains events and pages, variables by name, the database and the map tree.
|
|
85
|
+
Every command, option and value is typed and documented. The database, the map tree, the variable names, battle command events are also transformer. [Learn more here.](https://github.com/oulianov/rpgsync/blob/master/docs/scripts.md)
|
|
89
86
|
|
|
90
87
|
## Demo
|
|
91
88
|
|
|
@@ -9,8 +9,9 @@ A Python twin of your RPG Maker 2000/2003 game.
|
|
|
9
9
|
- Implement automated tests to verify game mechanics
|
|
10
10
|
- Improve your game using the modern developer's toolkit
|
|
11
11
|
|
|
12
|
+
<img width="1920" height="754" alt="rpgsync" src="https://github.com/user-attachments/assets/11a3fc5a-7884-4c7c-9aef-ee9ee54aa4bc" />
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Example
|
|
14
15
|
|
|
15
16
|
```python
|
|
16
17
|
@event(10, "Jeremy", x=15, y=38)
|
|
@@ -50,11 +51,7 @@ class Ulysse(Event): # The other event!
|
|
|
50
51
|
pass
|
|
51
52
|
```
|
|
52
53
|
|
|
53
|
-
Every command, option and value is typed and documented
|
|
54
|
-
[`rpgsync/dsl.py`](https://github.com/oulianov/rpgsync/blob/master/rpgsync/dsl.py): your
|
|
55
|
-
editor shows the docs on hover and flags wrong values (a misspelled direction, a sprite
|
|
56
|
-
index above 7, ...). [Writing scripts](https://github.com/oulianov/rpgsync/blob/master/docs/scripts.md)
|
|
57
|
-
explains events and pages, variables by name, the database and the map tree.
|
|
54
|
+
Every command, option and value is typed and documented. The database, the map tree, the variable names, battle command events are also transformer. [Learn more here.](https://github.com/oulianov/rpgsync/blob/master/docs/scripts.md)
|
|
58
55
|
|
|
59
56
|
## Demo
|
|
60
57
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "rpgsync"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "A Python twin of your RPG Maker 2000/2003 game"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -8,7 +8,14 @@ license = "MIT"
|
|
|
8
8
|
# through a link (the older scripts' .rpgsync/rpgsync source), and the scripts can't
|
|
9
9
|
# install rpgsync. LICENSE goes in the sdist through source-include below.
|
|
10
10
|
authors = [{ name = "Nicolas Oulianov" }]
|
|
11
|
-
keywords = [
|
|
11
|
+
keywords = [
|
|
12
|
+
"rpg-maker",
|
|
13
|
+
"rpg-maker-2000",
|
|
14
|
+
"rpg-maker-2003",
|
|
15
|
+
"easyrpg",
|
|
16
|
+
"gamedev",
|
|
17
|
+
"dsl",
|
|
18
|
+
]
|
|
12
19
|
classifiers = [
|
|
13
20
|
"Development Status :: 3 - Alpha",
|
|
14
21
|
"Environment :: Console",
|
|
@@ -51,15 +58,10 @@ source-include = ["LICENSE", "CHANGELOG.md"]
|
|
|
51
58
|
|
|
52
59
|
[tool.pytest.ini_options]
|
|
53
60
|
testpaths = ["tests"]
|
|
54
|
-
addopts = "-n auto"
|
|
61
|
+
addopts = "-n auto" # pytest-xdist: one worker per CPU
|
|
55
62
|
|
|
56
63
|
[dependency-groups]
|
|
57
|
-
dev = [
|
|
58
|
-
"pytest>=9.1.1",
|
|
59
|
-
"pytest-xdist>=3.8.0",
|
|
60
|
-
"ruff>=0.16.10",
|
|
61
|
-
"ty>=0.0.85",
|
|
62
|
-
]
|
|
64
|
+
dev = ["pytest>=9.1.1", "pytest-xdist>=3.8.0", "ruff>=0.16.10", "ty>=0.0.85"]
|
|
63
65
|
|
|
64
66
|
[tool.ruff]
|
|
65
67
|
line-length = 120
|
|
@@ -429,7 +429,14 @@ def _run(syncer: CliSyncer, units, prefer: str | None, initial: bool = False, fo
|
|
|
429
429
|
|
|
430
430
|
Part = tuple[str, str] # (text, rich style)
|
|
431
431
|
|
|
432
|
-
PLURAL = {
|
|
432
|
+
PLURAL = {
|
|
433
|
+
"event": "events",
|
|
434
|
+
"common event": "common events",
|
|
435
|
+
"troop": "troops",
|
|
436
|
+
"map": "maps",
|
|
437
|
+
"entry": "entries",
|
|
438
|
+
"field": "fields",
|
|
439
|
+
}
|
|
433
440
|
|
|
434
441
|
|
|
435
442
|
def _ids(ids: list[int], limit: int = 8) -> str:
|
|
@@ -460,7 +467,9 @@ def _entry_diff(old: list, new: list, what: str) -> list[Part]:
|
|
|
460
467
|
("removed", sorted(i for i in a.keys() - b.keys() if i > 0), "red"),
|
|
461
468
|
):
|
|
462
469
|
if ids:
|
|
463
|
-
|
|
470
|
+
named = [getattr(b.get(i) or a.get(i), "name", None) for i in ids] if what == "field" else []
|
|
471
|
+
shown = ", ".join(named) if named and all(named) else _ids(ids) # terms/system: the field names
|
|
472
|
+
parts.append(("%s %s %s" % (PLURAL[what] if len(ids) > 1 else what, shown, label), style))
|
|
464
473
|
if isinstance(pa, TreeOrder) and isinstance(pb, TreeOrder):
|
|
465
474
|
both = set(pa.order) & set(pb.order) # maps added or removed are listed above
|
|
466
475
|
if [m for m in pa.order if m in both] != [m for m in pb.order if m in both]:
|
|
@@ -536,7 +545,9 @@ def _status(syncer: Syncer, units: list[Unit]) -> dict[str, list[StatusLine]]:
|
|
|
536
545
|
data = data_of[u.bin_path]
|
|
537
546
|
if data is None:
|
|
538
547
|
continue
|
|
539
|
-
what = {"map": "event", "common": "common event", "troop": "troop", "maptree": "map"}.get(
|
|
548
|
+
what = u.what or {"map": "event", "common": "common event", "troop": "troop", "maptree": "map"}.get(
|
|
549
|
+
u.kind, "entry"
|
|
550
|
+
)
|
|
540
551
|
py_name = u.script_label(project.script_dir)
|
|
541
552
|
bin_name = os.path.relpath(u.bin_path, project.game_dir)
|
|
542
553
|
if u.kind == "database":
|
|
@@ -923,7 +934,7 @@ def check(
|
|
|
923
934
|
for found, part in _checked_units(proj, units, rules, resources, jobs):
|
|
924
935
|
for kind, where, problem in found:
|
|
925
936
|
color = "red" if kind == "error" else "yellow"
|
|
926
|
-
err.print(*([(where, "cyan"), ": "] if where else []), (problem, color), kind=kind)
|
|
937
|
+
err.print(*([(_here(where), "cyan"), ": "] if where else []), (problem, color), kind=kind)
|
|
927
938
|
errors += kind == "error"
|
|
928
939
|
warnings += kind == "warning"
|
|
929
940
|
if files is not None and part is not None:
|
|
@@ -1125,7 +1136,7 @@ def _check_resources(resources: Collector, proj: Project, ui: UI, err: UI, compl
|
|
|
1125
1136
|
grouped: dict[tuple[str, str], list] = {} # (database file, folder) -> its references
|
|
1126
1137
|
uses: dict[tuple[str, str], list] = {} # (folder, file) -> the events using it
|
|
1127
1138
|
for ref in found.missing:
|
|
1128
|
-
if not ref.event: # the database keeps many (default entries never shown): one
|
|
1139
|
+
if not ref.event: # the database keeps many (default entries never shown): one warning per folder
|
|
1129
1140
|
grouped.setdefault((ref.path, ref.folder), []).append(ref)
|
|
1130
1141
|
else: # one line per file, at its first use
|
|
1131
1142
|
uses.setdefault((ref.folder, resource_key(ref.name)), []).append(ref)
|
|
@@ -1133,30 +1144,51 @@ def _check_resources(resources: Collector, proj: Project, ui: UI, err: UI, compl
|
|
|
1133
1144
|
ref = refs[0]
|
|
1134
1145
|
where = "%s:%d" % (ref.path, ref.line) if ref.line else ref.path
|
|
1135
1146
|
problem = "%s uses %s/%s, which is not imported" % (ref.what, ref.folder, ref.name)
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
more = " and %d more" % (len(
|
|
1141
|
-
problem += " (also used at %s%s)" % (", ".join(others), more)
|
|
1142
|
-
err.print((where, "cyan"), ": ", (problem, "yellow"), kind="warning")
|
|
1147
|
+
# its other places, each once: a game file (Map0003.lmu) has no lines
|
|
1148
|
+
others = list(dict.fromkeys(os.path.basename(r.path) + (":%d" % r.line if r.line else "") for r in refs[1:]))
|
|
1149
|
+
others = [o for o in others if o != os.path.basename(where)]
|
|
1150
|
+
if others:
|
|
1151
|
+
more = " and %d more" % (len(others) - 3) if len(others) > 3 else ""
|
|
1152
|
+
problem += " (also used at %s%s)" % (", ".join(others[:3]), more)
|
|
1153
|
+
err.print((_here(where), "cyan"), ": ", (problem, "yellow"), kind="warning")
|
|
1143
1154
|
for (path, folder), refs in grouped.items():
|
|
1144
|
-
where = "%s:%d" % (path, refs[0].line) if refs[0].line else path
|
|
1145
|
-
names = list(dict.fromkeys(r.name for r in refs))
|
|
1146
|
-
shown = ", ".join(names[:8]) + (" and %d more" % (len(names) - 8) if len(names) > 8 else "")
|
|
1155
|
+
where = _here("%s:%d" % (path, refs[0].line) if refs[0].line else path)
|
|
1147
1156
|
if len(refs) == 1:
|
|
1148
1157
|
problem = "%s uses %s/%s, which is not imported" % (refs[0].what, folder, refs[0].name)
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1158
|
+
err.print((where, "cyan"), ": ", (problem, "yellow"), kind="warning")
|
|
1159
|
+
continue
|
|
1160
|
+
names = sorted(dict.fromkeys(r.name for r in refs), key=str.casefold)
|
|
1161
|
+
files = "%d %s file%s" % (len(names), folder, "s that are" if len(names) > 1 else " that is")
|
|
1162
|
+
_print_list(err, where, "%d entries use %s not imported" % (len(refs), files), names)
|
|
1152
1163
|
for folder, names in found.unused.items():
|
|
1153
|
-
problem = "%d imported file%s never used
|
|
1154
|
-
err
|
|
1164
|
+
problem = "%d imported file%s never used" % (len(names), "s" * (len(names) > 1))
|
|
1165
|
+
_print_list(err, folder + "/", problem, names)
|
|
1155
1166
|
if found.note:
|
|
1156
1167
|
ui.print(found.note, kind="info")
|
|
1157
1168
|
return len(uses) + len(grouped) + len(found.unused)
|
|
1158
1169
|
|
|
1159
1170
|
|
|
1171
|
+
def _print_list(err: UI, where: str, problem: str, names: list[str]) -> None:
|
|
1172
|
+
"""A warning about files, all of them named: on its line when they fit, else under it,
|
|
1173
|
+
in indented lines as wide as the terminal, a name never cut in two."""
|
|
1174
|
+
listed = ", ".join(names)
|
|
1175
|
+
width = err.console.width
|
|
1176
|
+
if len(err.marker("warning")) + len("%s: %s: %s" % (where, problem, listed)) <= width:
|
|
1177
|
+
err.print((where, "cyan"), ": ", ("%s: %s" % (problem, listed), "yellow"), kind="warning")
|
|
1178
|
+
return
|
|
1179
|
+
err.print((where, "cyan"), ": ", (problem + ":", "yellow"), kind="warning")
|
|
1180
|
+
indent = " "
|
|
1181
|
+
row = ""
|
|
1182
|
+
for i, name in enumerate(names):
|
|
1183
|
+
item = name + ("," if i < len(names) - 1 else "")
|
|
1184
|
+
if row and len(indent) + len(row) + 1 + len(item) > width:
|
|
1185
|
+
err.print((indent + row, "yellow"))
|
|
1186
|
+
row = item
|
|
1187
|
+
else:
|
|
1188
|
+
row = row + " " + item if row else item
|
|
1189
|
+
err.print((indent + row, "yellow"))
|
|
1190
|
+
|
|
1191
|
+
|
|
1160
1192
|
def _ty(script_dir: str) -> str | None:
|
|
1161
1193
|
"""The ty of the scripts folder's environment, else one on the PATH."""
|
|
1162
1194
|
for rel in (".venv/bin/ty", ".venv/Scripts/ty.exe"):
|
|
@@ -1183,10 +1215,27 @@ def _check_types(script_dir: str, ui: UI, err: UI, paths: list[Path] | None = No
|
|
|
1183
1215
|
if result.returncode == 0 or not lines:
|
|
1184
1216
|
return 0
|
|
1185
1217
|
for line in lines:
|
|
1218
|
+
m = re.match(r"([^:\s][^:]*\.pyi?)(:\d+:\d+: .*)$", line) # ty's paths: relative to the scripts folder
|
|
1219
|
+
if m:
|
|
1220
|
+
line = _here(os.path.join(script_dir, m.group(1))) + m.group(2)
|
|
1186
1221
|
err.print((line, "red"), kind="error")
|
|
1187
1222
|
return max(len(lines), 1)
|
|
1188
1223
|
|
|
1189
1224
|
|
|
1225
|
+
def _here(where: str) -> str:
|
|
1226
|
+
"""A place `check` names (a path, path:line, or a file of the game: RPG_RT.lmt), with
|
|
1227
|
+
its path relative to the current folder when it is in it: Scripts/Map0004.py:12, run
|
|
1228
|
+
from the game folder."""
|
|
1229
|
+
m = re.match(r"(.+?)((?::\d+)*)$", where)
|
|
1230
|
+
if m is None or not os.path.isabs(m.group(1)):
|
|
1231
|
+
return where
|
|
1232
|
+
try:
|
|
1233
|
+
path = os.path.relpath(m.group(1))
|
|
1234
|
+
except ValueError: # on another drive (Windows)
|
|
1235
|
+
return where
|
|
1236
|
+
return where if path == os.pardir or path.startswith(os.pardir + os.sep) else path + m.group(2)
|
|
1237
|
+
|
|
1238
|
+
|
|
1190
1239
|
def clean(
|
|
1191
1240
|
project: ProjectArg = None,
|
|
1192
1241
|
yes: Annotated[bool, typer.Option("--yes", "-y", help="Do not ask for confirmation.")] = False,
|
|
@@ -221,6 +221,10 @@ class Ctx(BaseModel):
|
|
|
221
221
|
handles: dict[str, dict[int, str]] = Field(default_factory=dict) # variables/switches id -> Python name
|
|
222
222
|
choice_sep: str | None = None # how the editor joined show_choices options (None: engine default)
|
|
223
223
|
dynparams: bool = False # DynPlugins/DynParams.dll: variables wherever RPG Maker takes a number
|
|
224
|
+
# most commands of the game are stored the way the Maniac Patch editor writes them
|
|
225
|
+
# (Control Variables of one variable with 0 as last id...): a statement without a
|
|
226
|
+
# `# layout:` comment is compiled that way (see rpgsync.layout)
|
|
227
|
+
maniac_layout: bool = False
|
|
224
228
|
|
|
225
229
|
def enc(self, s: str, node=None) -> bytes:
|
|
226
230
|
try:
|
|
@@ -561,9 +565,19 @@ def C(code, params=(), string=b"") -> Command:
|
|
|
561
565
|
TEXT_CODES = {10110: ("text", 20110), 12410: ("comment", 22410)}
|
|
562
566
|
|
|
563
567
|
|
|
568
|
+
# Maniac Patch: p4 selectors (4 bits each) of width, height, font (0 the command's text,
|
|
569
|
+
# 1 strings[p7], 2 strings[variables[p7]]), font size, character and line spacing;
|
|
570
|
+
# p5-p10 their values (0: unchanged)
|
|
571
|
+
MSG_MANIAC = ["width", "height", "font", "font_size", "char_spacing", "line_spacing"]
|
|
572
|
+
|
|
573
|
+
|
|
564
574
|
def _msg_options():
|
|
565
575
|
def dec(c, ctx):
|
|
566
|
-
|
|
576
|
+
from .maniac_ext import text_src
|
|
577
|
+
from .pyexpr import _fits, mval_src
|
|
578
|
+
|
|
579
|
+
n = len(c.params)
|
|
580
|
+
if n not in (4, 11) or (c.string and n == 4):
|
|
567
581
|
return None
|
|
568
582
|
kw = []
|
|
569
583
|
if c.params[0]:
|
|
@@ -573,19 +587,46 @@ def _msg_options():
|
|
|
573
587
|
kw.append(("avoid_hero", bool(c.params[2]) if c.params[2] == 1 else c.params[2]))
|
|
574
588
|
if c.params[3]:
|
|
575
589
|
kw.append(("continue_events", bool(c.params[3]) if c.params[3] == 1 else c.params[3]))
|
|
590
|
+
if n == 11:
|
|
591
|
+
m = c.params[4]
|
|
592
|
+
if not _fits(m, 6):
|
|
593
|
+
return None
|
|
594
|
+
for slot, name in enumerate(MSG_MANIAC):
|
|
595
|
+
mode, v = (m >> 4 * slot) & 0xF, c.params[5 + slot]
|
|
596
|
+
if name == "font":
|
|
597
|
+
src = text_src(mode, v, ctx.dec(c.string))
|
|
598
|
+
if src is None:
|
|
599
|
+
return None
|
|
600
|
+
if src != '""':
|
|
601
|
+
kw.append((name, Raw(src=src)))
|
|
602
|
+
elif mode or v:
|
|
603
|
+
if mode > 2:
|
|
604
|
+
return None
|
|
605
|
+
kw.append((name, Raw(src=mval_src(mode, v))))
|
|
576
606
|
return render_call("message_options", [], kw)
|
|
577
607
|
|
|
578
608
|
def enc(call, ctx):
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
609
|
+
from .maniac_ext import text_val
|
|
610
|
+
from .pyexpr import mval_val
|
|
611
|
+
|
|
612
|
+
a = Args(call, ["transparent", "position", "avoid_hero", "continue_events"] + MSG_MANIAC)
|
|
613
|
+
p = [
|
|
614
|
+
a.int("transparent", 0),
|
|
615
|
+
a.enum("position", ["top", "middle", "bottom"], "bottom"),
|
|
616
|
+
a.int("avoid_hero", 0),
|
|
617
|
+
a.int("continue_events", 0),
|
|
618
|
+
]
|
|
619
|
+
if not set(MSG_MANIAC) & set(a.values):
|
|
620
|
+
return C(10120, p)
|
|
621
|
+
modes, values, font = 0, [], ""
|
|
622
|
+
for slot, name in enumerate(MSG_MANIAC):
|
|
623
|
+
if name == "font":
|
|
624
|
+
mode, v, font = text_val(a.get(name, ""), "message_options(): font", call.node)
|
|
625
|
+
else:
|
|
626
|
+
mode, v = mval_val(a.get(name, 0), name, modes=3)
|
|
627
|
+
modes |= mode << 4 * slot
|
|
628
|
+
values.append(v)
|
|
629
|
+
return C(10120, p + [modes] + values, ctx.enc(font, call.node))
|
|
589
630
|
|
|
590
631
|
return dec, enc
|
|
591
632
|
|
|
@@ -594,7 +635,22 @@ rich(10120, "message_options")(_msg_options())
|
|
|
594
635
|
|
|
595
636
|
|
|
596
637
|
def _face():
|
|
638
|
+
# Maniac Patch: p3 selectors of the faceset (0 the command's text, 1 strings[p4],
|
|
639
|
+
# 2 strings[variables[p4]]) and of the index (1 variables[n], 2 variables[variables[n]]);
|
|
640
|
+
# the Maniac editor drops trailing zero parameters (rpgsync.layout)
|
|
597
641
|
def dec(c, ctx):
|
|
642
|
+
from .maniac_ext import text_src
|
|
643
|
+
from .pyexpr import _fits, mval_src
|
|
644
|
+
|
|
645
|
+
if len(c.params) in (4, 5):
|
|
646
|
+
p = c.params + [0] * (5 - len(c.params))
|
|
647
|
+
if not _fits(p[3], 2) or (p[3] >> 4) > 2 or p[1] not in (0, 1) or p[2] not in (0, 1):
|
|
648
|
+
return None
|
|
649
|
+
face = text_src(p[3] & 0xF, p[4], ctx.dec(c.string))
|
|
650
|
+
if face is None:
|
|
651
|
+
return None
|
|
652
|
+
kw = [(k, True) for k, v in (("right", p[1]), ("flip", p[2])) if v]
|
|
653
|
+
return render_call("face", [Raw(src=face), Raw(src=mval_src(p[3] >> 4, p[0]))], kw)
|
|
598
654
|
if not c.string:
|
|
599
655
|
if not c.params:
|
|
600
656
|
return "face(None)"
|
|
@@ -612,9 +668,19 @@ def _face():
|
|
|
612
668
|
return render_call("face", [ctx.dec(c.string), c.params[0]], kw)
|
|
613
669
|
|
|
614
670
|
def enc(call, ctx):
|
|
671
|
+
from .maniac_ext import text_val
|
|
672
|
+
from .pyexpr import is_int, mval_val
|
|
673
|
+
|
|
615
674
|
a = Args(call, ["faceset", "index", "right", "flip"])
|
|
616
675
|
if a.get("faceset") is None and len(a.values) <= 1:
|
|
617
676
|
return C(10130)
|
|
677
|
+
face, index = a.get("faceset"), a.get("index", 0)
|
|
678
|
+
if face is not None and not isinstance(face, str) or not is_int(index):
|
|
679
|
+
fmode, fvar, text = text_val(face, "face(): the faceset", call.node)
|
|
680
|
+
imode, i = mval_val(index, "face(): the index", modes=3)
|
|
681
|
+
return C(
|
|
682
|
+
10130, [i, a.int("right", 0), a.int("flip", 0), fmode | imode << 4, fvar], ctx.enc(text, call.node)
|
|
683
|
+
)
|
|
618
684
|
return C(10130, [a.int("index", 0), a.int("right", 0), a.int("flip", 0)], ctx.enc(a.str("faceset"), call.node))
|
|
619
685
|
|
|
620
686
|
return dec, enc
|
|
@@ -657,20 +723,42 @@ rich(10810, "teleport")(_teleport())
|
|
|
657
723
|
|
|
658
724
|
|
|
659
725
|
def _wait():
|
|
726
|
+
# Maniac Patch: p1 256 = in frames (else tenths of seconds; 1 = until a key is
|
|
727
|
+
# pressed), p2 the selector of the value (1 variables[n], 2 variables[variables[n]])
|
|
660
728
|
def dec(c, ctx):
|
|
661
|
-
|
|
662
|
-
|
|
729
|
+
from .pyexpr import mval_src
|
|
730
|
+
|
|
731
|
+
p = c.params
|
|
732
|
+
n = len(p)
|
|
733
|
+
if n == 3 and p[2] in (1, 2) and p[1] in (0, 256):
|
|
734
|
+
unit = "frames" if p[1] == 256 else "tenths"
|
|
735
|
+
return render_call("wait", [], [(unit, Raw(src=mval_src(p[2], p[0])))])
|
|
736
|
+
if n == 2 and p[1] == 256:
|
|
737
|
+
return render_call("wait", [], [("frames", p[0])])
|
|
738
|
+
if n not in (1, 2):
|
|
663
739
|
return None
|
|
664
740
|
kw = []
|
|
665
741
|
if n == 2 and c.params[1]:
|
|
666
742
|
if c.params[1] != 1:
|
|
667
743
|
return None
|
|
668
744
|
kw.append(("key", True))
|
|
745
|
+
if n == 1 and ctx.engine != "2k":
|
|
746
|
+
kw.append(("key", None)) # stored without it (RPG Maker 2000's form)
|
|
669
747
|
t = c.params[0] / 10
|
|
670
748
|
return render_call("wait", [Raw(src=repr(t))], kw)
|
|
671
749
|
|
|
672
750
|
def enc(call, ctx):
|
|
673
|
-
|
|
751
|
+
from .pyexpr import mval_val
|
|
752
|
+
|
|
753
|
+
a = Args(call, ["seconds", "key", "frames", "tenths"])
|
|
754
|
+
for unit, opt in (("frames", 256), ("tenths", 0)):
|
|
755
|
+
if unit in a.values:
|
|
756
|
+
if set(a.values) != {unit}:
|
|
757
|
+
raise CompileError("wait(%s=...) takes no other argument" % unit, call.node)
|
|
758
|
+
mode, v = mval_val(a.get(unit), unit, modes=3)
|
|
759
|
+
if mode == 0 and unit == "tenths":
|
|
760
|
+
raise CompileError("wait(): a number of seconds is wait(seconds)", call.node)
|
|
761
|
+
return C(11410, [v, opt] + ([mode] if mode else []))
|
|
674
762
|
s = a.get("seconds", 0)
|
|
675
763
|
if not isinstance(s, (int, float)) or isinstance(s, bool):
|
|
676
764
|
raise CompileError("wait(): seconds must be a number", call.node)
|
|
@@ -678,6 +766,8 @@ def _wait():
|
|
|
678
766
|
if abs(tenths - s * 10) > 1e-6:
|
|
679
767
|
raise CompileError("wait(): RPG Maker waits in steps of 0.1 seconds", call.node)
|
|
680
768
|
p = [int(tenths)]
|
|
769
|
+
if "key" in a.values and a.get("key") is None:
|
|
770
|
+
return C(11410, p)
|
|
681
771
|
if ctx.engine != "2k" or a.bool("key"):
|
|
682
772
|
p.append(int(a.bool("key")))
|
|
683
773
|
return C(11410, p)
|
|
@@ -717,18 +807,52 @@ for _code, _name, _args in [
|
|
|
717
807
|
|
|
718
808
|
|
|
719
809
|
def _sound(code, name, has_fade):
|
|
810
|
+
# Maniac Patch: one more parameter with the selectors (4 bits each) of the name (0 the
|
|
811
|
+
# command's text, 1 strings[n], 2 strings[variables[n]]) and of each value (1
|
|
812
|
+
# variables[n], 2 variables[variables[n]]), then the name's string variable
|
|
720
813
|
keys = (["fade"] if has_fade else []) + ["volume", "tempo", "balance"]
|
|
721
814
|
defaults = ([0] if has_fade else []) + [100, 100, 50]
|
|
722
815
|
|
|
723
816
|
def dec(c, ctx):
|
|
724
|
-
|
|
817
|
+
from .maniac_ext import text_src
|
|
818
|
+
from .pyexpr import _fits, mval_src
|
|
819
|
+
|
|
820
|
+
n = len(keys)
|
|
821
|
+
if len(c.params) == n:
|
|
822
|
+
kw = [(k, v) for k, v, d in zip(keys, c.params, defaults) if v != d]
|
|
823
|
+
return render_call(name, [ctx.dec(c.string)], kw)
|
|
824
|
+
if len(c.params) not in (n + 1, n + 2):
|
|
825
|
+
return None
|
|
826
|
+
m, var = c.params[n], c.params[n + 1] if len(c.params) == n + 2 else 0
|
|
827
|
+
if m == 0 or not _fits(m, n + 1) or (len(c.params) == n + 2) != (m & 0xF != 0):
|
|
725
828
|
return None
|
|
726
|
-
|
|
727
|
-
|
|
829
|
+
sound = text_src(m & 0xF, var, ctx.dec(c.string))
|
|
830
|
+
if sound is None:
|
|
831
|
+
return None
|
|
832
|
+
kw = []
|
|
833
|
+
for slot, (k, v, d) in enumerate(zip(keys, c.params, defaults), 1):
|
|
834
|
+
mode = (m >> 4 * slot) & 0xF
|
|
835
|
+
if mode > 2:
|
|
836
|
+
return None
|
|
837
|
+
if mode or v != d:
|
|
838
|
+
kw.append((k, Raw(src=mval_src(mode, v))))
|
|
839
|
+
return render_call(name, [Raw(src=sound)], kw)
|
|
728
840
|
|
|
729
841
|
def enc(call, ctx):
|
|
842
|
+
from .maniac_ext import text_val
|
|
843
|
+
from .pyexpr import mval_val
|
|
844
|
+
|
|
730
845
|
a = Args(call, ["name"] + keys)
|
|
731
|
-
|
|
846
|
+
sound = a.get("name", "")
|
|
847
|
+
if isinstance(sound, str) and all(isinstance(a.get(k, 0), int) for k in keys):
|
|
848
|
+
return C(code, [a.int(k, d) for k, d in zip(keys, defaults)], ctx.enc(a.str("name", ""), call.node))
|
|
849
|
+
mode, var, text = text_val(sound, "%s(): the name" % name, call.node)
|
|
850
|
+
params, modes = [], mode
|
|
851
|
+
for slot, (k, d) in enumerate(zip(keys, defaults), 1):
|
|
852
|
+
kmode, v = mval_val(a.get(k, d), k, modes=3)
|
|
853
|
+
params.append(v)
|
|
854
|
+
modes |= kmode << 4 * slot
|
|
855
|
+
return C(code, params + [modes] + ([var] if mode else []), ctx.enc(text, call.node))
|
|
732
856
|
|
|
733
857
|
return dec, enc
|
|
734
858
|
|
|
@@ -25,7 +25,7 @@ import os
|
|
|
25
25
|
import tomllib
|
|
26
26
|
from collections.abc import Iterable, Sequence
|
|
27
27
|
|
|
28
|
-
from . import dynparams, dynrpg, maniac
|
|
28
|
+
from . import dynparams, dynrpg, maniac, maniac_ext
|
|
29
29
|
from .commands import CODE_NAMES
|
|
30
30
|
from .lcf import Command
|
|
31
31
|
|
|
@@ -118,6 +118,14 @@ def command_problems(
|
|
|
118
118
|
hint = ' (a %s command: add "%s" to [tool.rpgsync] patches)' % (patch, patch) if patch else ""
|
|
119
119
|
add(True, "unknown command %d%s" % (code, hint))
|
|
120
120
|
continue
|
|
121
|
+
if "maniac" not in patches:
|
|
122
|
+
option = maniac_ext.maniac_option(c)
|
|
123
|
+
if option is not None:
|
|
124
|
+
add(
|
|
125
|
+
False,
|
|
126
|
+
'%s uses a Maniac Patch option (%s): add "maniac" to [tool.rpgsync] patches'
|
|
127
|
+
% (describe(code), option),
|
|
128
|
+
)
|
|
121
129
|
if engine == "2k":
|
|
122
130
|
if code in ONLY_2K3:
|
|
123
131
|
add(True, "%s only exists in RPG Maker 2003" % describe(code))
|