weaponlab-mcp 0.1.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.
- weaponlab_mcp-0.1.0/.gitignore +6 -0
- weaponlab_mcp-0.1.0/CHANGELOG.md +18 -0
- weaponlab_mcp-0.1.0/LICENSE +21 -0
- weaponlab_mcp-0.1.0/PKG-INFO +98 -0
- weaponlab_mcp-0.1.0/PUBLISHING.md +37 -0
- weaponlab_mcp-0.1.0/README.md +80 -0
- weaponlab_mcp-0.1.0/docs/PROBE_CONTRACT.md +175 -0
- weaponlab_mcp-0.1.0/examples/demo/probe/demo_probe.py +108 -0
- weaponlab_mcp-0.1.0/examples/demo/probe/game.json +33 -0
- weaponlab_mcp-0.1.0/examples/demo/weaponlab/data/actions.json +20 -0
- weaponlab_mcp-0.1.0/examples/demo/weaponlab/data/budgets.json +3 -0
- weaponlab_mcp-0.1.0/examples/demo/weaponlab/data/cases.json +54 -0
- weaponlab_mcp-0.1.0/examples/demo/weaponlab/data/metrics.json +9 -0
- weaponlab_mcp-0.1.0/examples/demo/weaponlab/data/weapons.json +12 -0
- weaponlab_mcp-0.1.0/examples/demo/weaponlab/specs/Demo_Rifle.json +86 -0
- weaponlab_mcp-0.1.0/examples/demo/weaponlab.json +11 -0
- weaponlab_mcp-0.1.0/pyproject.toml +29 -0
- weaponlab_mcp-0.1.0/server.json +33 -0
- weaponlab_mcp-0.1.0/src/weaponlab/__init__.py +3 -0
- weaponlab_mcp-0.1.0/src/weaponlab/__main__.py +79 -0
- weaponlab_mcp-0.1.0/src/weaponlab/blender/render_parts.py +175 -0
- weaponlab_mcp-0.1.0/src/weaponlab/blender/roll.py +98 -0
- weaponlab_mcp-0.1.0/src/weaponlab/blender/track.py +74 -0
- weaponlab_mcp-0.1.0/src/weaponlab/config.py +102 -0
- weaponlab_mcp-0.1.0/src/weaponlab/contract.py +221 -0
- weaponlab_mcp-0.1.0/src/weaponlab/gate.py +336 -0
- weaponlab_mcp-0.1.0/src/weaponlab/motion.py +72 -0
- weaponlab_mcp-0.1.0/src/weaponlab/probe.py +179 -0
- weaponlab_mcp-0.1.0/src/weaponlab/rig.py +291 -0
- weaponlab_mcp-0.1.0/src/weaponlab/server.py +233 -0
- weaponlab_mcp-0.1.0/src/weaponlab/spec.py +140 -0
- weaponlab_mcp-0.1.0/tests/conftest.py +30 -0
- weaponlab_mcp-0.1.0/tests/test_contract.py +57 -0
- weaponlab_mcp-0.1.0/tests/test_demo.py +48 -0
- weaponlab_mcp-0.1.0/tests/test_gate.py +74 -0
- weaponlab_mcp-0.1.0/tests/test_toangtown.py +56 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
The general core of the tools built for ToangTown's weapons (2026-10-13 and 14), split out so any game can use them.
|
|
6
|
+
|
|
7
|
+
* The probe contract (`weaponlab-probe/1`, docs/PROBE_CONTRACT.md): a job file in, a rows file out, screenshots and
|
|
8
|
+
parts dumps at moments. `weaponlab contract job|rows|parts` checks a probe's files. Run against ToangTown's probe
|
|
9
|
+
(149 jobs, 1,021 dumps), it found two real bugs there: a tagged entry's view rows under the plain weapon id, and a
|
|
10
|
+
gun built into an arms mesh dumped as arms.
|
|
11
|
+
* The Gate: cases, metrics (the contract's standard ones built in), named budgets, runs, the baseline, check evals,
|
|
12
|
+
the verdict, proposals and approval. Rows that break the contract are refused.
|
|
13
|
+
* Rig Lab without Blender: pieces and twins, the spec's facts, a magazine's line into its well, a sliding part's
|
|
14
|
+
line, a melee hilt against the fist's grip channel and the turn and move that lay it in.
|
|
15
|
+
* Motion Lab in Blender: a forearm's roll (auto per clip, every frame checked against the forearm's range), a part's
|
|
16
|
+
track on its axis, dev renders.
|
|
17
|
+
* One MCP server (`weaponlab-mcp`, 30 tools) and a command line (`weaponlab`).
|
|
18
|
+
* A demo game with two planted bugs and a 150-line probe; tests walk the whole loop on it.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Khoa Hoang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: weaponlab-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An MCP server that finds and fixes first-person weapon bugs behind a gate: your game's probe measures, the Gate judges, Rig Lab and Motion Lab read and mend.
|
|
5
|
+
Author: Khoa Hoang
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: animation,blender,first-person,game-development,mcp,testing,unreal
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Games/Entertainment
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: mcp<3,>=2.0
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Weapon Lab
|
|
20
|
+
|
|
21
|
+
<!-- mcp-name: io.github.hpdkhoa/weaponlab-mcp -->
|
|
22
|
+
|
|
23
|
+
An MCP server that finds and fixes first-person weapon bugs behind a gate: hands off the gun, a magazine crooked in
|
|
24
|
+
its well, a blade held wrong, arms covering the screen.
|
|
25
|
+
|
|
26
|
+
Each bug becomes a **case**: a metric, a weapon, an action and a pass rule. Your game's own **probe** measures the
|
|
27
|
+
weapons. The **Gate** passes a change only when its target cases pass and nothing else got worse. Fix each kind of
|
|
28
|
+
bug once, then check every weapon with it.
|
|
29
|
+
|
|
30
|
+
Weapon Lab never runs your game itself. You write a small probe for your engine that follows the
|
|
31
|
+
[probe contract](docs/PROBE_CONTRACT.md): a job file in, a rows file out. Everything else works on what the probe
|
|
32
|
+
writes back.
|
|
33
|
+
|
|
34
|
+
## What it does
|
|
35
|
+
|
|
36
|
+
| Group | Tools | What for |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| Gate | `gate_run`, `gate_verdict`, `gate_propose`, `gate_approve`, `gate_set_baseline`, `gate_cases`, ... | Keeps the bugs as cases, measures, compares against a baseline, judges a change, records a person's approval. |
|
|
39
|
+
| Probe | `probe_run`, `probe_plan`, `probe_status` | Starts your game's probe. Tries a fix before it is written: tuned fields, staged assets, screenshots, parts dumps. |
|
|
40
|
+
| Rig Lab | `rig_pieces`, `rig_facts`, `rig_align`, `rig_part_line`, `rig_fist` | Reads the posed weapon from a parts dump: its pieces, real-world facts (the magazine behind the barrel), a magazine's line into its well, a hilt in the fist. |
|
|
41
|
+
| Motion Lab | `motion_roll`, `motion_roll_profile`, `motion_roll_set`, `motion_track`, `motion_render` | Rebuilds clips in Blender: turns a hand about its forearm inside the forearm's range, puts a sliding part back on its axis. |
|
|
42
|
+
| Specs | `spec_list`, `spec_read`, `spec_validate`, `spec_write` | One file a weapon: marks (grip, support), anchors (pieces to find), facts, limits. |
|
|
43
|
+
| Contract | `contract_check` | Checks a probe's job, rows or parts dump. |
|
|
44
|
+
|
|
45
|
+
## Try it on the demo
|
|
46
|
+
|
|
47
|
+
The demo is a toy game with two planted bugs and a probe in about 150 lines. It needs no engine.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e .
|
|
51
|
+
cd examples/demo
|
|
52
|
+
weaponlab gate run probe,rig
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
(`python -m weaponlab` works the same when pip's scripts folder is not on your PATH.)
|
|
56
|
+
|
|
57
|
+
The rifle's left hand misses its handguard by 3.75 cm. The knife's hilt lies 90 degrees across the fist.
|
|
58
|
+
`tests/test_demo.py` walks the whole loop: measure, find the fix, try it by tune, judge it.
|
|
59
|
+
|
|
60
|
+
## Use it in your game
|
|
61
|
+
|
|
62
|
+
1. `weaponlab init` in your project. It writes `weaponlab.json` and empty data files.
|
|
63
|
+
2. Write your probe from [the contract](docs/PROBE_CONTRACT.md), starting from `examples/demo/probe/demo_probe.py`.
|
|
64
|
+
Set its command in `weaponlab.json`.
|
|
65
|
+
3. List your weapons in `weaponlab/data/weapons.json` and their actions in `actions.json`.
|
|
66
|
+
4. Add a case for each bug you know. Measure everything once and set the baseline.
|
|
67
|
+
5. Fix through the loop: try by tune or staging, look at the screenshots, propose, approve, import.
|
|
68
|
+
|
|
69
|
+
Check your probe on its own with `weaponlab contract job|rows|parts <file>`.
|
|
70
|
+
|
|
71
|
+
## Add it to an MCP client
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{"mcpServers": {"weaponlab": {"command": "weaponlab-mcp", "env": {"WEAPONLAB_PROJECT": "C:/path/to/your/game"}}}}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Setting | Meaning |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `WEAPONLAB_PROJECT` | The project folder with `weaponlab.json`. The current folder by default. |
|
|
80
|
+
| `WEAPONLAB_BLENDER` | Blender, for Motion Lab. Found on the PATH or in the usual install folders otherwise. |
|
|
81
|
+
|
|
82
|
+
## Requirements
|
|
83
|
+
|
|
84
|
+
- Python 3.10 or later.
|
|
85
|
+
- Blender 4.4 or later for Motion Lab and renders. Nothing else needs it.
|
|
86
|
+
- Hand bones named as Unreal's mannequin names them (`hand_r`, `index_01_r`, `lowerarm_r`) for the fist measure and
|
|
87
|
+
the forearm roll.
|
|
88
|
+
|
|
89
|
+
## Where it came from
|
|
90
|
+
|
|
91
|
+
Weapon Lab is the general part of the tools built for ToangTown, an Unreal 5.8 game with 46 weapons. Its first probe
|
|
92
|
+
is ToangTown's. The [contract's lessons](docs/PROBE_CONTRACT.md#lessons-from-the-first-probe) come from that probe.
|
|
93
|
+
Pose Lab ([poselab-mcp](https://github.com/hpdkhoa/poselab-mcp)) answers the body's side: clipping, the view, a grip
|
|
94
|
+
solver. The Gate can use it as a provider.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Publishing Weapon Lab
|
|
2
|
+
|
|
3
|
+
The order matters. The MCP Registry accepts the listing only after the PyPI package is live. It also checks that the
|
|
4
|
+
README on PyPI carries `mcp-name: io.github.hpdkhoa/weaponlab-mcp`. README.md holds that line as an HTML comment.
|
|
5
|
+
|
|
6
|
+
## 1. GitHub
|
|
7
|
+
|
|
8
|
+
Push `main` to https://github.com/hpdkhoa/weaponlab-mcp and tag the release (`git tag v0.1.0`, `git push --tags`).
|
|
9
|
+
|
|
10
|
+
## 2. PyPI
|
|
11
|
+
|
|
12
|
+
Use your PyPI API token from your password manager. Do not keep it in a file.
|
|
13
|
+
|
|
14
|
+
python -m pip install build twine
|
|
15
|
+
python -m build
|
|
16
|
+
python -m twine check dist/*
|
|
17
|
+
python -m twine upload dist/*
|
|
18
|
+
|
|
19
|
+
twine asks for the token (user name `__token__`). Check it after: `pip install weaponlab-mcp` in a fresh
|
|
20
|
+
environment, then `weaponlab-mcp` starts the server (it waits for a client on stdin; Ctrl+C stops it).
|
|
21
|
+
|
|
22
|
+
## 3. The MCP Registry
|
|
23
|
+
|
|
24
|
+
In this folder (server.json is here):
|
|
25
|
+
|
|
26
|
+
mcp-publisher login github
|
|
27
|
+
mcp-publisher publish
|
|
28
|
+
|
|
29
|
+
`login github` proves you own the io.github.hpdkhoa namespace. Search for it afterwards:
|
|
30
|
+
https://registry.modelcontextprotocol.io/v0/servers?search=weaponlab
|
|
31
|
+
|
|
32
|
+
## Each release
|
|
33
|
+
|
|
34
|
+
Run the tests first: `python -m pytest tests` (the ToangTown checks run only on the machine that has the game).
|
|
35
|
+
|
|
36
|
+
Raise the version in three places: `pyproject.toml`, `src/weaponlab/__init__.py`, and `server.json` (both `version`
|
|
37
|
+
fields). Add the release to CHANGELOG.md. Build and upload to PyPI first, then `mcp-publisher publish`.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Weapon Lab
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.hpdkhoa/weaponlab-mcp -->
|
|
4
|
+
|
|
5
|
+
An MCP server that finds and fixes first-person weapon bugs behind a gate: hands off the gun, a magazine crooked in
|
|
6
|
+
its well, a blade held wrong, arms covering the screen.
|
|
7
|
+
|
|
8
|
+
Each bug becomes a **case**: a metric, a weapon, an action and a pass rule. Your game's own **probe** measures the
|
|
9
|
+
weapons. The **Gate** passes a change only when its target cases pass and nothing else got worse. Fix each kind of
|
|
10
|
+
bug once, then check every weapon with it.
|
|
11
|
+
|
|
12
|
+
Weapon Lab never runs your game itself. You write a small probe for your engine that follows the
|
|
13
|
+
[probe contract](docs/PROBE_CONTRACT.md): a job file in, a rows file out. Everything else works on what the probe
|
|
14
|
+
writes back.
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
| Group | Tools | What for |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| Gate | `gate_run`, `gate_verdict`, `gate_propose`, `gate_approve`, `gate_set_baseline`, `gate_cases`, ... | Keeps the bugs as cases, measures, compares against a baseline, judges a change, records a person's approval. |
|
|
21
|
+
| Probe | `probe_run`, `probe_plan`, `probe_status` | Starts your game's probe. Tries a fix before it is written: tuned fields, staged assets, screenshots, parts dumps. |
|
|
22
|
+
| Rig Lab | `rig_pieces`, `rig_facts`, `rig_align`, `rig_part_line`, `rig_fist` | Reads the posed weapon from a parts dump: its pieces, real-world facts (the magazine behind the barrel), a magazine's line into its well, a hilt in the fist. |
|
|
23
|
+
| Motion Lab | `motion_roll`, `motion_roll_profile`, `motion_roll_set`, `motion_track`, `motion_render` | Rebuilds clips in Blender: turns a hand about its forearm inside the forearm's range, puts a sliding part back on its axis. |
|
|
24
|
+
| Specs | `spec_list`, `spec_read`, `spec_validate`, `spec_write` | One file a weapon: marks (grip, support), anchors (pieces to find), facts, limits. |
|
|
25
|
+
| Contract | `contract_check` | Checks a probe's job, rows or parts dump. |
|
|
26
|
+
|
|
27
|
+
## Try it on the demo
|
|
28
|
+
|
|
29
|
+
The demo is a toy game with two planted bugs and a probe in about 150 lines. It needs no engine.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e .
|
|
33
|
+
cd examples/demo
|
|
34
|
+
weaponlab gate run probe,rig
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
(`python -m weaponlab` works the same when pip's scripts folder is not on your PATH.)
|
|
38
|
+
|
|
39
|
+
The rifle's left hand misses its handguard by 3.75 cm. The knife's hilt lies 90 degrees across the fist.
|
|
40
|
+
`tests/test_demo.py` walks the whole loop: measure, find the fix, try it by tune, judge it.
|
|
41
|
+
|
|
42
|
+
## Use it in your game
|
|
43
|
+
|
|
44
|
+
1. `weaponlab init` in your project. It writes `weaponlab.json` and empty data files.
|
|
45
|
+
2. Write your probe from [the contract](docs/PROBE_CONTRACT.md), starting from `examples/demo/probe/demo_probe.py`.
|
|
46
|
+
Set its command in `weaponlab.json`.
|
|
47
|
+
3. List your weapons in `weaponlab/data/weapons.json` and their actions in `actions.json`.
|
|
48
|
+
4. Add a case for each bug you know. Measure everything once and set the baseline.
|
|
49
|
+
5. Fix through the loop: try by tune or staging, look at the screenshots, propose, approve, import.
|
|
50
|
+
|
|
51
|
+
Check your probe on its own with `weaponlab contract job|rows|parts <file>`.
|
|
52
|
+
|
|
53
|
+
## Add it to an MCP client
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{"mcpServers": {"weaponlab": {"command": "weaponlab-mcp", "env": {"WEAPONLAB_PROJECT": "C:/path/to/your/game"}}}}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
| Setting | Meaning |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `WEAPONLAB_PROJECT` | The project folder with `weaponlab.json`. The current folder by default. |
|
|
62
|
+
| `WEAPONLAB_BLENDER` | Blender, for Motion Lab. Found on the PATH or in the usual install folders otherwise. |
|
|
63
|
+
|
|
64
|
+
## Requirements
|
|
65
|
+
|
|
66
|
+
- Python 3.10 or later.
|
|
67
|
+
- Blender 4.4 or later for Motion Lab and renders. Nothing else needs it.
|
|
68
|
+
- Hand bones named as Unreal's mannequin names them (`hand_r`, `index_01_r`, `lowerarm_r`) for the fist measure and
|
|
69
|
+
the forearm roll.
|
|
70
|
+
|
|
71
|
+
## Where it came from
|
|
72
|
+
|
|
73
|
+
Weapon Lab is the general part of the tools built for ToangTown, an Unreal 5.8 game with 46 weapons. Its first probe
|
|
74
|
+
is ToangTown's. The [contract's lessons](docs/PROBE_CONTRACT.md#lessons-from-the-first-probe) come from that probe.
|
|
75
|
+
Pose Lab ([poselab-mcp](https://github.com/hpdkhoa/poselab-mcp)) answers the body's side: clipping, the view, a grip
|
|
76
|
+
solver. The Gate can use it as a provider.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# The probe contract (weaponlab-probe/1)
|
|
2
|
+
|
|
3
|
+
Weapon Lab never runs your game itself. A **probe** does: a program you write for your game, in any language or
|
|
4
|
+
engine. Weapon Lab writes a **job file** and starts your probe with its path. The probe plays each weapon's actions,
|
|
5
|
+
measures them and writes a **rows file**. It may also write screenshots and **parts dumps**. Weapon Lab checks what
|
|
6
|
+
comes back, and the Gate judges the rows.
|
|
7
|
+
|
|
8
|
+
`weaponlab contract job|rows|parts <file>` checks a file against this contract, so you can test a probe on its own.
|
|
9
|
+
`examples/demo/probe/demo_probe.py` is a complete probe in about 200 lines. It measures a toy game.
|
|
10
|
+
|
|
11
|
+
## How Weapon Lab starts the probe
|
|
12
|
+
|
|
13
|
+
The project's `weaponlab.json` names the command. `{job}` becomes the job file's path:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{"probe": {"command": ["MyGame.exe", "-probe={job}", "-log={work}/probe/game-{stamp}.log"],
|
|
17
|
+
"timeout_s": 1800, "refuse": ["MyGame.exe"], "busy": ["UnrealEditor.exe"], "wait_s": 3600,
|
|
18
|
+
"job_extra": {"quit": true}, "low_priority": true}}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `{job}` is the job file, `{work}` the project's work folder, `{stamp}` the run's time stamp.
|
|
22
|
+
- The probe reads the job, runs, writes the rows file at the job's `out`, and exits.
|
|
23
|
+
- Exit code 0 means it finished. Weapon Lab still checks the rows file. A missing or broken file is an error.
|
|
24
|
+
- Weapon Lab waits up to `timeout_s`, or longer when the job's own length needs it, scaled by `1 / slomo`.
|
|
25
|
+
- `refuse` lists processes Weapon Lab never runs beside, such as the player's own copy of the game. It stops at once.
|
|
26
|
+
- `busy` lists other heavy jobs, such as an editor or Blender. Weapon Lab waits up to `wait_s` for them to end.
|
|
27
|
+
- `job_extra` adds keys to every job for the probe's own use.
|
|
28
|
+
- `low_priority` starts the probe below normal priority, so the machine stays usable.
|
|
29
|
+
|
|
30
|
+
## The gun's space
|
|
31
|
+
|
|
32
|
+
Every position in a job, a rows file or a parts dump is in the gun's space, in centimetres:
|
|
33
|
+
|
|
34
|
+
- +Y runs along the barrel, toward the muzzle.
|
|
35
|
+
- +Z is up.
|
|
36
|
+
- +X is the gun's left.
|
|
37
|
+
- The origin is the probe's choice, but it must stay the same for one weapon across runs. A spec's marks use it.
|
|
38
|
+
|
|
39
|
+
## The job file
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"contract": "weaponlab-probe/1",
|
|
44
|
+
"out": "C:/proj/weaponlab/work/probe/rows-20261014-120000.json",
|
|
45
|
+
"weapons": [
|
|
46
|
+
{
|
|
47
|
+
"id": "Rifle_M4",
|
|
48
|
+
"tag": "fix1",
|
|
49
|
+
"actions": [
|
|
50
|
+
{"name": "idle", "seconds": 1.6, "at": [1.2]},
|
|
51
|
+
{"name": "reload", "run": "reload", "seconds": 3.0, "at": [0.6, 1.2]}
|
|
52
|
+
],
|
|
53
|
+
"bones": {"bolt": "Bolt_j"},
|
|
54
|
+
"support_l": [0.0, 25.0, 1.0],
|
|
55
|
+
"tune": {"ArmsGunOffset": {"X": 0, "Y": 18.5, "Z": -1.4}}
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"overrides": {"/Game/Anims/A_Idle": "/Game/Staging/S1/A_Idle"},
|
|
59
|
+
"shots": "C:/proj/weaponlab/work/shots/fix1",
|
|
60
|
+
"parts": "C:/proj/weaponlab/work/parts/fix1",
|
|
61
|
+
"slomo": 0.2,
|
|
62
|
+
"view": true
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Field | Required | Meaning |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `contract` | no (default `weaponlab-probe/1`) | The contract version. A probe refuses a version it does not know. |
|
|
69
|
+
| `out` | yes | Where the probe writes the rows file. |
|
|
70
|
+
| `weapons[].id` | yes | The weapon, by the game's own id. |
|
|
71
|
+
| `weapons[].tag` | no | A label for this entry. Its rows report the weapon as `<id>@<tag>`. One job can run the same weapon twice, for example before and after a tune. |
|
|
72
|
+
| `weapons[].actions[].name` | yes | The action: `idle`, `fire`, `reload` or any name the probe knows. |
|
|
73
|
+
| `weapons[].actions[].seconds` | no | How long to watch it. |
|
|
74
|
+
| `weapons[].actions[].run` | no | The probe's own command for the action. Weapon Lab passes it through from `actions.json`. |
|
|
75
|
+
| `weapons[].actions[].at` | no | Moments, in seconds from the action's start, for screenshots and parts dumps. |
|
|
76
|
+
| `weapons[].actions[]` other keys | no | Passed through from `actions.json` for the probe's own use. |
|
|
77
|
+
| `weapons[].bones` | no | A spec mark's name to a bone. The probe reports how far each travels (`events.<mark>_travel_cm`). |
|
|
78
|
+
| `weapons[].support_l` | no | The spec's support mark. The probe reports the left palm's distance to it. |
|
|
79
|
+
| `weapons[].tune` | no | Fields of the weapon's definition, set before the weapon is drawn and put back after the entry. This is how a fix is tried before it is written. |
|
|
80
|
+
| `overrides` | no | Assets the probe loads in place of others: staged clips or models, tried without replacing anything. |
|
|
81
|
+
| `shots` | no | A folder for screenshots. Empty or missing means none. |
|
|
82
|
+
| `parts` | no | A folder for parts dumps. Empty or missing means none. |
|
|
83
|
+
| `slomo` | no | A time scale in (0, 1]. Dumps are slow, so the game runs slower while they are taken. |
|
|
84
|
+
| `view` | no | Measure the screen cover (`view.*` metrics). |
|
|
85
|
+
|
|
86
|
+
A probe ignores keys it does not know. A probe that cannot do something it was asked (`tune`, `overrides`) says so
|
|
87
|
+
in `notes` and does not fail.
|
|
88
|
+
|
|
89
|
+
## The rows file
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"rows": [
|
|
94
|
+
{"weapon": "Rifle_M4@fix1", "action": "idle", "metric": "hands.l_gap_cm", "value": 0.22},
|
|
95
|
+
{"weapon": "Rifle_M4@fix1", "action": "idle", "metric": "view.hand_l_shown", "value": 0.27},
|
|
96
|
+
{"weapon": "Rifle_M4@fix1", "action": "reload", "metric": "hands.l_to_support_cm", "value": null, "note": "no support mark"}
|
|
97
|
+
],
|
|
98
|
+
"notes": ["Rifle_M4@fix1: ArmsGunOffset tuned"]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- One row per weapon, action and metric.
|
|
103
|
+
- `value` is a finite number, or `null` when the probe could not measure it. The Gate skips a null.
|
|
104
|
+
- `note` is optional text for a person.
|
|
105
|
+
- A metric name has a group and a name: `hands.l_gap_cm`. The standard metrics are below. A project adds its own in
|
|
106
|
+
its `metrics.json`.
|
|
107
|
+
- `notes` holds text about the run as a whole.
|
|
108
|
+
|
|
109
|
+
## Screenshots and parts dumps
|
|
110
|
+
|
|
111
|
+
- A screenshot at a moment: `<shots>/<weapon>_<action>_<t>.png`, with `t` to two decimals (`Rifle_M4_idle_1.20.png`).
|
|
112
|
+
- A screenshot of the idle view without a moment: `<shots>/<weapon>_idle.png`.
|
|
113
|
+
- A parts dump at a moment: `<parts>/<weapon>_<action>_<t>.obj`.
|
|
114
|
+
|
|
115
|
+
A parts dump is a Wavefront OBJ of every drawn part as posed at that moment, in the gun's space:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
# eye 22.0 -31.1 36.2
|
|
119
|
+
# hold rig AKS offset 0.0 18.5 -1.4
|
|
120
|
+
g arms|Arms|/Game/Arms/SK_Arms|hand_l
|
|
121
|
+
v 1.0 2.0 3.0
|
|
122
|
+
...
|
|
123
|
+
f 1 2 3
|
|
124
|
+
g gun|Gun|/Game/Guns/M4/SK_M4|root
|
|
125
|
+
g part|Mag|/Game/Guns/M4/SM_Mag|Mag1
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- A group name has four fields: `role|component|asset|bone`.
|
|
129
|
+
- `role` is `arms`, `gun` (the weapon's own model), `part` (a separate piece of it, such as a magazine) or `item`
|
|
130
|
+
(something carried in a hand).
|
|
131
|
+
- `bone` is the bone the triangles follow. Use `-` for a static mesh.
|
|
132
|
+
- `# eye x y z` is the camera in the gun's space.
|
|
133
|
+
- Any other `# key ...` line is free for the probe's own facts. Weapon Lab keeps them as a header.
|
|
134
|
+
- Every group needs triangles. A dump needs a `gun` group or, when the weapon is drawn only as pieces (a melee
|
|
135
|
+
model swapped into another weapon's hands), a `part` group.
|
|
136
|
+
|
|
137
|
+
Hand bones follow Unreal's mannequin names (`hand_l`, `index_01_r`, `pinky_01_r`, `middle_01_r`, `thumb_03_r`,
|
|
138
|
+
`lowerarm_r`). The fist measure and the forearm roll read those names.
|
|
139
|
+
|
|
140
|
+
## The standard metrics
|
|
141
|
+
|
|
142
|
+
| Metric | Unit | Better | Meaning |
|
|
143
|
+
|---|---|---|---|
|
|
144
|
+
| `hands.arms_shown` | 0/1 | higher | The weapon is held by visible arms. |
|
|
145
|
+
| `hands.l_gap_cm` | cm | lower | The left palm's distance to the gun's surface, the worst over the action. |
|
|
146
|
+
| `hands.r_gap_cm` | cm | lower | The right palm's distance to the gun's surface. |
|
|
147
|
+
| `hands.r_handle_gap_cm` | cm | lower | The right palm's distance to the grip. |
|
|
148
|
+
| `hands.l_drift_cm` | cm | lower | How far the left hand moves in the gun's space during the action. |
|
|
149
|
+
| `hands.l_to_support_cm` | cm | lower | The left palm's distance to the spec's support mark. |
|
|
150
|
+
| `hands.r_hold_drift_cm` | cm | lower | How far a held melee weapon moves in the right hand. |
|
|
151
|
+
| `hands.l_ride_<mark>_cm` | cm | lower | The left hand's distance to a moving part it rides, such as a pump. |
|
|
152
|
+
| `view.cover_total` | share | lower | The share of the screen the arms and weapon cover. |
|
|
153
|
+
| `view.cover_lower_left` | share | lower | The share of the lower left third they cover. |
|
|
154
|
+
| `view.cover_bottom_middle` | share | lower | The share of the bottom middle third they cover. |
|
|
155
|
+
| `view.hand_l_shown` | share | higher | The share of the left hand the player sees. |
|
|
156
|
+
| `view.hand_r_shown` | share | higher | The share of the right hand the player sees. |
|
|
157
|
+
| `view.item_shown` | share | higher | The share of a carried item the player sees. |
|
|
158
|
+
| `parts.copies_magazine` | count | none | Magazines drawn at once. |
|
|
159
|
+
| `events.played` | 0/1 | higher | The action's clip played. |
|
|
160
|
+
| `events.item_in_hand` | 0/1 | none | An item was in the hand during the action. |
|
|
161
|
+
| `events.<mark>_travel_cm` | cm | higher | How far a spec's moving bone travels. |
|
|
162
|
+
| `swing.edge_lead` | cos | higher | How well a blade's edge leads its cut: 1 is the edge, 0 the flat. |
|
|
163
|
+
|
|
164
|
+
"Better: none" means the Gate never calls a change in it a regression. Only a case's rule judges it.
|
|
165
|
+
|
|
166
|
+
## Lessons from the first probe
|
|
167
|
+
|
|
168
|
+
ToangTown's probe (Unreal 5.8, C++) found these. A new probe should handle them too.
|
|
169
|
+
|
|
170
|
+
- **The first action of a run can read wrong.** One read the hands 44 cm off the gun. Weapon Lab adds a warm-up entry
|
|
171
|
+
and drops its rows.
|
|
172
|
+
- **Actions depend on the state the last one left.** Start fire and chamber actions from a full magazine.
|
|
173
|
+
- **Melee swing rows vary between runs.** Run an unchanged weapon twice before you trust a small change.
|
|
174
|
+
- **A dump stalls the game for a moment.** Take dumps in slow motion.
|
|
175
|
+
- **Set tuned fields before the weapon is drawn.** Some fields are read only when it is equipped.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""A complete probe for a toy game, written to the probe contract (docs/PROBE_CONTRACT.md). It is the template for
|
|
2
|
+
your own: read the job, play each action, measure, write the rows and the dumps.
|
|
3
|
+
|
|
4
|
+
The toy game has two weapons, each with a bug that shows in the numbers:
|
|
5
|
+
- Demo_Rifle: its model sits 4 cm too high on the arms (game.json "Demo_Rifle.offset"), so the left palm misses the
|
|
6
|
+
handguard by 4 cm. Tuning "offset" to [0, 0, 0] fixes it.
|
|
7
|
+
- Demo_Knife: its hilt runs forward out of the fist (+Y) instead of along the fist's channel (+X). Rig Lab's fist
|
|
8
|
+
measure finds the turn ("rot") and the move ("offset") that lay it in; tune them to try the fix.
|
|
9
|
+
|
|
10
|
+
python demo_probe.py <job.json>"""
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
HERE = Path(__file__).resolve().parent
|
|
16
|
+
GAME = json.loads((HERE / "game.json").read_text(encoding="utf-8"))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def box(c, size):
|
|
20
|
+
"""A box's 12 triangles around centre c (cm)."""
|
|
21
|
+
(x, y, z), (a, b, h) = c, (s / 2.0 for s in size)
|
|
22
|
+
v = [(x + i * a, y + j * b, z + k * h) for i in (-1, 1) for j in (-1, 1) for k in (-1, 1)]
|
|
23
|
+
q = [(0, 1, 3, 2), (4, 6, 7, 5), (0, 4, 5, 1), (2, 3, 7, 6), (0, 2, 6, 4), (1, 5, 7, 3)]
|
|
24
|
+
return [(v[a], v[b], v[c]) for a, b, c, d in q] + [(v[a], v[c], v[d]) for a, b, c, d in q]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def turn(m, p):
|
|
28
|
+
return tuple(sum(m[i][k] * p[k] for k in range(3)) for i in range(3))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def model(wid, d):
|
|
32
|
+
"""The weapon's pieces in gun space: [(role, component, bone, triangles)], after its definition's offset and turn."""
|
|
33
|
+
off, rot = d.get("offset", [0, 0, 0]), d.get("rot", [[1, 0, 0], [0, 1, 0], [0, 0, 1]])
|
|
34
|
+
move = lambda tris: [tuple(tuple(a + o for a, o in zip(turn(rot, p), off)) for p in t) for t in tris]
|
|
35
|
+
if wid == "Demo_Rifle":
|
|
36
|
+
return [("gun", "Rifle", "-", move(box((0, 10, 0), (4, 40, 6)))), # receiver and handguard
|
|
37
|
+
("gun", "Rifle", "-", move(box((0, 40, 1), (1.5, 20, 1.5)))), # barrel
|
|
38
|
+
("part", "Mag", "Mag", move(box((0, 4, -9), (2.4, 6, 12))))] # magazine under the well
|
|
39
|
+
# the knife: hilt from the pommel at y -6 to y 5, blade to y 25, along +Y before its turn
|
|
40
|
+
return [("gun", "Knife", "-", move(box((0, -0.5, 0), (2.2, 11, 2.6)) + box((0, 15, 0), (0.5, 20, 3))))]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def arms():
|
|
44
|
+
"""The hands in gun space: the left palm under the handguard, the right fist's channel along +X at the origin."""
|
|
45
|
+
out = [("hand_l", box((0, 25, -4), (7, 9, 2.5)))]
|
|
46
|
+
out += [("hand_r", box((0, 0, -2.5), (8, 9, 2))), ("middle_01_r", box((0, 0, 3), (2, 2, 2))),
|
|
47
|
+
("middle_02_r", box((0, 0, 4.5), (2, 2, 2))), ("middle_03_r", box((0, 0, 2), (2, 2, 2))),
|
|
48
|
+
("index_01_r", box((3.5, 0, 0), (2, 2, 2))), ("pinky_01_r", box((-3.5, 0, 0), (2, 2, 2))),
|
|
49
|
+
("thumb_03_r", box((3, -2.5, 2), (1.5, 1.5, 1.5)))]
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def dump(path, wid, d):
|
|
54
|
+
lines = ["# demo probe parts: gun space, cm", "# eye 0.0 -20.0 12.0"]
|
|
55
|
+
n = 1
|
|
56
|
+
groups = [("arms", "Arms", "/Demo/Arms", b, t) for b, t in arms()] + [(r, c, "/Demo/" + wid, b, t) for r, c, b, t in model(wid, d)]
|
|
57
|
+
for role, comp, asset, bone, tris in groups:
|
|
58
|
+
lines.append("g %s|%s|%s|%s" % (role, comp, asset, bone))
|
|
59
|
+
for t in tris:
|
|
60
|
+
lines += ["v %.3f %.3f %.3f" % p for p in t]
|
|
61
|
+
for _ in tris:
|
|
62
|
+
lines.append("f %d %d %d" % (n, n + 1, n + 2))
|
|
63
|
+
n += 3
|
|
64
|
+
Path(path).write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main(job_path):
|
|
68
|
+
job = json.loads(Path(job_path).read_text(encoding="utf-8"))
|
|
69
|
+
if job.get("contract", "weaponlab-probe/1") != "weaponlab-probe/1":
|
|
70
|
+
raise SystemExit("unknown contract %s" % job.get("contract"))
|
|
71
|
+
rows, notes = [], []
|
|
72
|
+
if job.get("overrides"):
|
|
73
|
+
notes.append("the demo game has no assets: overrides ignored")
|
|
74
|
+
if job.get("shots"):
|
|
75
|
+
notes.append("the demo game draws nothing: no screenshots")
|
|
76
|
+
for w in job["weapons"]:
|
|
77
|
+
wid = w["id"] + ("@" + w["tag"] if w.get("tag") else "")
|
|
78
|
+
d = dict(GAME.get(w["id"], {}))
|
|
79
|
+
for k, v in (w.get("tune") or {}).items():
|
|
80
|
+
if k in d:
|
|
81
|
+
d[k] = v
|
|
82
|
+
notes.append("%s: %s tuned" % (wid, k))
|
|
83
|
+
else:
|
|
84
|
+
notes.append("%s: no field %s" % (wid, k))
|
|
85
|
+
pieces = model(w["id"], d)
|
|
86
|
+
tris = [t for _, _, _, ts in pieces for t in ts]
|
|
87
|
+
hands = dict(arms())
|
|
88
|
+
for a in w["actions"]:
|
|
89
|
+
row = lambda m, v, note=None: rows.append(dict({"weapon": wid, "action": a["name"], "metric": m, "value": v}, **({"note": note} if note else {})))
|
|
90
|
+
row("hands.arms_shown", 1)
|
|
91
|
+
row("events.played", 1)
|
|
92
|
+
if w["id"] == "Demo_Rifle":
|
|
93
|
+
# the palm's top against the handguard's underside, where the palm is (in or out of it, both wrong)
|
|
94
|
+
palm_top = max(p[2] for t in hands["hand_l"] for p in t)
|
|
95
|
+
under = min(p[2] for t in tris for p in t if 20.0 <= p[1] <= 30.0)
|
|
96
|
+
row("hands.l_gap_cm", round(abs(under - palm_top), 3))
|
|
97
|
+
low = -min(p[2] for t in tris for p in t)
|
|
98
|
+
if job.get("view", True) and a["name"] == "idle":
|
|
99
|
+
row("view.cover_lower_left", round(min(1.0, 0.05 + 0.01 * low), 3))
|
|
100
|
+
row("view.hand_l_shown", round(max(0.0, 0.3 - 0.02 * max(0.0, low - 15)), 3))
|
|
101
|
+
for t in a.get("at", []):
|
|
102
|
+
if job.get("parts"):
|
|
103
|
+
dump(Path(job["parts"]) / ("%s_%s_%.2f.obj" % (wid, a["name"], t)), w["id"], d)
|
|
104
|
+
Path(job["out"]).write_text(json.dumps({"rows": rows, "notes": notes}, indent=1), encoding="utf-8")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
if __name__ == "__main__":
|
|
108
|
+
main(sys.argv[1])
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Demo_Rifle": {
|
|
3
|
+
"offset": [
|
|
4
|
+
0,
|
|
5
|
+
0,
|
|
6
|
+
4
|
|
7
|
+
]
|
|
8
|
+
},
|
|
9
|
+
"Demo_Knife": {
|
|
10
|
+
"rot": [
|
|
11
|
+
[
|
|
12
|
+
1,
|
|
13
|
+
0,
|
|
14
|
+
0
|
|
15
|
+
],
|
|
16
|
+
[
|
|
17
|
+
0,
|
|
18
|
+
1,
|
|
19
|
+
0
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
0,
|
|
23
|
+
0,
|
|
24
|
+
1
|
|
25
|
+
]
|
|
26
|
+
],
|
|
27
|
+
"offset": [
|
|
28
|
+
0,
|
|
29
|
+
0,
|
|
30
|
+
0
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "Demo_Rifle-support",
|
|
4
|
+
"weapon": "Demo_Rifle",
|
|
5
|
+
"action": "idle",
|
|
6
|
+
"metric": "hands.l_gap_cm",
|
|
7
|
+
"rule": {
|
|
8
|
+
"op": "<=",
|
|
9
|
+
"value": 1.0
|
|
10
|
+
},
|
|
11
|
+
"about": "the left palm on the handguard",
|
|
12
|
+
"source": "demo",
|
|
13
|
+
"status": "open"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "Demo_Rifle-view",
|
|
17
|
+
"weapon": "Demo_Rifle",
|
|
18
|
+
"action": "idle",
|
|
19
|
+
"metric": "view.cover_lower_left",
|
|
20
|
+
"rule": {
|
|
21
|
+
"op": "<=",
|
|
22
|
+
"value": "view_lower_left"
|
|
23
|
+
},
|
|
24
|
+
"about": "the arms leave the lower left clear",
|
|
25
|
+
"source": "demo",
|
|
26
|
+
"status": "open"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "Demo_Rifle-facts",
|
|
30
|
+
"weapon": "Demo_Rifle",
|
|
31
|
+
"action": "idle",
|
|
32
|
+
"metric": "facts.false",
|
|
33
|
+
"rule": {
|
|
34
|
+
"op": "==",
|
|
35
|
+
"value": 0
|
|
36
|
+
},
|
|
37
|
+
"about": "the magazine under the receiver, behind the barrel",
|
|
38
|
+
"source": "demo",
|
|
39
|
+
"status": "open"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "Demo_Knife-fist",
|
|
43
|
+
"weapon": "Demo_Knife",
|
|
44
|
+
"action": "idle",
|
|
45
|
+
"metric": "fist.angle_deg",
|
|
46
|
+
"rule": {
|
|
47
|
+
"op": "<=",
|
|
48
|
+
"value": 10
|
|
49
|
+
},
|
|
50
|
+
"about": "the hilt along the fist's channel",
|
|
51
|
+
"source": "demo",
|
|
52
|
+
"status": "open"
|
|
53
|
+
}
|
|
54
|
+
]
|