godot-input-map-auditor 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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Godot Input Map Auditor contributors
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,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: godot-input-map-auditor
3
+ Version: 0.1.0
4
+ Summary: Audit Godot input maps for device coverage, duplicate bindings, and generated references.
5
+ Author: Godot Input Map Auditor contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/NonniGB/godot-production-toolkit/tree/main/godot-input-map-auditor
8
+ Project-URL: Issues, https://github.com/NonniGB/godot-production-toolkit/issues
9
+ Keywords: godot,input,touch,gamepad,ci,gamedev
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # Godot Input Map Auditor
24
+
25
+ Audit Godot input actions for keyboard, mouse, gamepad, and touch readiness, then generate input reference docs and strongly named GDScript constants.
26
+
27
+ This is a small CI-friendly tool for projects where input coverage can drift across desktop, controller, and mobile targets.
28
+
29
+ ## Install
30
+
31
+ ```powershell
32
+ python -m pip install -e .
33
+ ```
34
+
35
+ When published:
36
+
37
+ ```powershell
38
+ python -m pip install godot-input-map-auditor
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```powershell
44
+ godot-input-audit C:\Projects\MyGame --require keyboard,touch
45
+ godot-input-audit . --write-docs docs\INPUT_REFERENCE.md
46
+ godot-input-audit . --generate-gd scripts\generated\input_actions.gd
47
+ godot-input-audit . --format json --output input-report.json
48
+ ```
49
+
50
+ Run against the sample project:
51
+
52
+ ```powershell
53
+ godot-input-audit examples\tiny-godot-project --fail-on none
54
+ ```
55
+
56
+ ## What It Checks
57
+
58
+ - Parses the `[input]` section in `project.godot`.
59
+ - Classifies `InputEventKey`, mouse, joypad, and screen-touch event classes.
60
+ - Reports actions missing required device families.
61
+ - Reports duplicate normalized bindings across actions.
62
+ - Generates `INPUT_REFERENCE.md`.
63
+ - Generates optional `InputActions` GDScript constants.
64
+
65
+ ## Documentation
66
+
67
+ - [Device classification](docs/DEVICE_RULES.md)
68
+ - [Rule reference](docs/RULE_REFERENCE.md)
69
+ - [Generated output](docs/GENERATED_OUTPUT.md)
70
+ - [Touch readiness](docs/TOUCH_READINESS.md)
71
+ - [CI usage](docs/CI.md)
72
+
73
+ ## Development
74
+
75
+ ```powershell
76
+ python -m pip install -e .
77
+ python -m unittest discover -s tests -v
78
+ godot-input-audit examples\tiny-godot-project --require keyboard --fail-on none
79
+ ```
80
+
81
+ Examples and generated docs use generic action names so the repository can be published safely.
@@ -0,0 +1,59 @@
1
+ # Godot Input Map Auditor
2
+
3
+ Audit Godot input actions for keyboard, mouse, gamepad, and touch readiness, then generate input reference docs and strongly named GDScript constants.
4
+
5
+ This is a small CI-friendly tool for projects where input coverage can drift across desktop, controller, and mobile targets.
6
+
7
+ ## Install
8
+
9
+ ```powershell
10
+ python -m pip install -e .
11
+ ```
12
+
13
+ When published:
14
+
15
+ ```powershell
16
+ python -m pip install godot-input-map-auditor
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```powershell
22
+ godot-input-audit C:\Projects\MyGame --require keyboard,touch
23
+ godot-input-audit . --write-docs docs\INPUT_REFERENCE.md
24
+ godot-input-audit . --generate-gd scripts\generated\input_actions.gd
25
+ godot-input-audit . --format json --output input-report.json
26
+ ```
27
+
28
+ Run against the sample project:
29
+
30
+ ```powershell
31
+ godot-input-audit examples\tiny-godot-project --fail-on none
32
+ ```
33
+
34
+ ## What It Checks
35
+
36
+ - Parses the `[input]` section in `project.godot`.
37
+ - Classifies `InputEventKey`, mouse, joypad, and screen-touch event classes.
38
+ - Reports actions missing required device families.
39
+ - Reports duplicate normalized bindings across actions.
40
+ - Generates `INPUT_REFERENCE.md`.
41
+ - Generates optional `InputActions` GDScript constants.
42
+
43
+ ## Documentation
44
+
45
+ - [Device classification](docs/DEVICE_RULES.md)
46
+ - [Rule reference](docs/RULE_REFERENCE.md)
47
+ - [Generated output](docs/GENERATED_OUTPUT.md)
48
+ - [Touch readiness](docs/TOUCH_READINESS.md)
49
+ - [CI usage](docs/CI.md)
50
+
51
+ ## Development
52
+
53
+ ```powershell
54
+ python -m pip install -e .
55
+ python -m unittest discover -s tests -v
56
+ godot-input-audit examples\tiny-godot-project --require keyboard --fail-on none
57
+ ```
58
+
59
+ Examples and generated docs use generic action names so the repository can be published safely.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "godot-input-map-auditor"
7
+ version = "0.1.0"
8
+ description = "Audit Godot input maps for device coverage, duplicate bindings, and generated references."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "Godot Input Map Auditor contributors" }]
13
+ keywords = ["godot", "input", "touch", "gamepad", "ci", "gamedev"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Software Development :: Quality Assurance",
22
+ "Topic :: Utilities"
23
+ ]
24
+ dependencies = []
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/NonniGB/godot-production-toolkit/tree/main/godot-input-map-auditor"
28
+ Issues = "https://github.com/NonniGB/godot-production-toolkit/issues"
29
+
30
+ [project.scripts]
31
+ godot-input-audit = "godot_input_auditor.cli:entrypoint"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+
36
+ [tool.ruff]
37
+ line-length = 100
38
+ target-version = "py311"
39
+
40
+ [tool.ruff.lint]
41
+ select = ["E", "F", "I", "UP", "B"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Godot input map audit tooling."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import entrypoint
2
+
3
+
4
+ if __name__ == "__main__":
5
+ entrypoint()
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+
5
+ from .models import Finding, InputAction
6
+
7
+
8
+ def evaluate_actions(actions: list[InputAction], required_devices: set[str]) -> list[Finding]:
9
+ findings: list[Finding] = []
10
+ if not actions:
11
+ return [
12
+ Finding(
13
+ rule_id="input_map_empty",
14
+ severity="warning",
15
+ action=None,
16
+ message="No input actions were found in the [input] section.",
17
+ )
18
+ ]
19
+
20
+ for action in actions:
21
+ if not action.events:
22
+ findings.append(
23
+ Finding(
24
+ rule_id="action_has_no_events",
25
+ severity="warning",
26
+ action=action.name,
27
+ message=f"Input action '{action.name}' has no bound events.",
28
+ )
29
+ )
30
+ missing = sorted(required_devices - action.devices)
31
+ if missing:
32
+ findings.append(
33
+ Finding(
34
+ rule_id="missing_required_device",
35
+ severity="error",
36
+ action=action.name,
37
+ message=f"Input action '{action.name}' is missing required device(s): {', '.join(missing)}.",
38
+ )
39
+ )
40
+
41
+ findings.extend(_duplicate_binding_findings(actions))
42
+ return findings
43
+
44
+
45
+ def _duplicate_binding_findings(actions: list[InputAction]) -> list[Finding]:
46
+ by_signature: dict[str, list[str]] = defaultdict(list)
47
+ for action in actions:
48
+ for event in action.events:
49
+ if event.signature:
50
+ by_signature[event.signature].append(action.name)
51
+
52
+ findings: list[Finding] = []
53
+ for signature, action_names in sorted(by_signature.items()):
54
+ unique_names = sorted(set(action_names))
55
+ if len(unique_names) > 1:
56
+ findings.append(
57
+ Finding(
58
+ rule_id="duplicate_binding",
59
+ severity="warning",
60
+ action=None,
61
+ message=(
62
+ f"Binding '{signature}' is used by multiple actions: "
63
+ f"{', '.join(unique_names)}."
64
+ ),
65
+ )
66
+ )
67
+ return findings
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from .audit import evaluate_actions
8
+ from .input_parser import parse_input_map
9
+ from .models import Finding
10
+ from .reporting import (
11
+ render_gdscript_constants,
12
+ render_json_report,
13
+ render_markdown_reference,
14
+ render_sarif_report,
15
+ render_text_report,
16
+ )
17
+
18
+
19
+ def main(argv: list[str] | None = None) -> int:
20
+ parser = _build_parser()
21
+ args = parser.parse_args(argv)
22
+ project = Path(args.project)
23
+ project_file = project if project.name == "project.godot" else project / "project.godot"
24
+
25
+ if not project_file.exists():
26
+ actions = []
27
+ findings = [
28
+ Finding(
29
+ rule_id="missing_project_godot",
30
+ severity="error",
31
+ action=None,
32
+ message="project.godot was not found.",
33
+ )
34
+ ]
35
+ else:
36
+ actions = parse_input_map(project_file.read_text(encoding="utf-8"))
37
+ findings = evaluate_actions(actions, required_devices=_parse_required(args.require))
38
+
39
+ if args.write_docs:
40
+ path = Path(args.write_docs)
41
+ path.parent.mkdir(parents=True, exist_ok=True)
42
+ path.write_text(render_markdown_reference(actions), encoding="utf-8")
43
+
44
+ if args.generate_gd:
45
+ path = Path(args.generate_gd)
46
+ path.parent.mkdir(parents=True, exist_ok=True)
47
+ path.write_text(render_gdscript_constants(actions), encoding="utf-8")
48
+
49
+ if args.format == "json":
50
+ rendered = render_json_report(actions, findings)
51
+ elif args.format == "sarif":
52
+ rendered = render_sarif_report(actions, findings)
53
+ else:
54
+ rendered = render_text_report(actions, findings)
55
+ if args.output:
56
+ output = Path(args.output)
57
+ output.parent.mkdir(parents=True, exist_ok=True)
58
+ output.write_text(rendered + "\n", encoding="utf-8")
59
+ else:
60
+ print(rendered)
61
+
62
+ return _exit_code(findings, args.fail_on)
63
+
64
+
65
+ def entrypoint() -> None:
66
+ raise SystemExit(main())
67
+
68
+
69
+ def _build_parser() -> argparse.ArgumentParser:
70
+ parser = argparse.ArgumentParser(
71
+ prog="godot-input-audit",
72
+ description="Audit Godot input actions for device coverage and duplicate bindings.",
73
+ )
74
+ parser.add_argument("--version", action="version", version="godot-input-audit 0.1.0")
75
+ parser.add_argument("project", help="Godot project directory or project.godot file.")
76
+ parser.add_argument(
77
+ "--require",
78
+ default="",
79
+ help="Comma-separated device families every action should support, e.g. keyboard,touch.",
80
+ )
81
+ parser.add_argument("--format", choices=["text", "json", "sarif"], default="text")
82
+ parser.add_argument("--output", help="Write report to a file instead of stdout.")
83
+ parser.add_argument("--write-docs", help="Write Markdown input reference.")
84
+ parser.add_argument("--generate-gd", help="Write GDScript constants file.")
85
+ parser.add_argument("--fail-on", choices=["warning", "error", "none"], default="warning")
86
+ return parser
87
+
88
+
89
+ def _parse_required(raw: str) -> set[str]:
90
+ return {part.strip() for part in raw.split(",") if part.strip()}
91
+
92
+
93
+ def _exit_code(findings: list[Finding], fail_on: str) -> int:
94
+ if fail_on == "none":
95
+ return 0
96
+ severities = {finding.severity for finding in findings}
97
+ if fail_on == "error":
98
+ return 1 if "error" in severities else 0
99
+ return 1 if ("error" in severities or "warning" in severities) else 0
100
+
101
+
102
+ if __name__ == "__main__":
103
+ sys.exit(main())
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from .models import InputAction, InputEvent
6
+
7
+ SECTION_RE = re.compile(r"^\[([^\]]+)\]$")
8
+ EVENT_RE = re.compile(r"Object\((InputEvent[A-Za-z0-9_]+)(.*?)\)", re.DOTALL)
9
+
10
+
11
+ def parse_input_map(project_godot_content: str) -> list[InputAction]:
12
+ actions: list[InputAction] = []
13
+ in_input_section = False
14
+ current_name: str | None = None
15
+ current_lines: list[str] = []
16
+ brace_depth = 0
17
+
18
+ for raw_line in project_godot_content.splitlines():
19
+ line = raw_line.strip()
20
+ section = SECTION_RE.match(line)
21
+ if section:
22
+ if current_name is not None:
23
+ actions.append(_parse_action(current_name, "\n".join(current_lines)))
24
+ current_name = None
25
+ current_lines = []
26
+ brace_depth = 0
27
+ in_input_section = section.group(1) == "input"
28
+ continue
29
+
30
+ if not in_input_section or not line or line.startswith(";") or line.startswith("#"):
31
+ continue
32
+
33
+ if current_name is None:
34
+ if "=" not in line:
35
+ continue
36
+ name, rest = line.split("=", 1)
37
+ current_name = name.strip().strip('"')
38
+ current_lines = [rest]
39
+ brace_depth = rest.count("{") - rest.count("}")
40
+ if brace_depth <= 0:
41
+ actions.append(_parse_action(current_name, "\n".join(current_lines)))
42
+ current_name = None
43
+ current_lines = []
44
+ continue
45
+
46
+ current_lines.append(line)
47
+ brace_depth += line.count("{") - line.count("}")
48
+ if brace_depth <= 0:
49
+ actions.append(_parse_action(current_name, "\n".join(current_lines)))
50
+ current_name = None
51
+ current_lines = []
52
+ brace_depth = 0
53
+
54
+ if current_name is not None:
55
+ actions.append(_parse_action(current_name, "\n".join(current_lines)))
56
+
57
+ return actions
58
+
59
+
60
+ def _parse_action(name: str, body: str) -> InputAction:
61
+ events = []
62
+ for match in EVENT_RE.finditer(body):
63
+ event_type = match.group(1)
64
+ event_body = match.group(2)
65
+ device = _classify_event(event_type)
66
+ events.append(InputEvent(event_type=event_type, device=device, signature=_signature(event_type, event_body)))
67
+ return InputAction(name=name, events=events)
68
+
69
+
70
+ def _classify_event(event_type: str) -> str:
71
+ if event_type == "InputEventKey":
72
+ return "keyboard"
73
+ if event_type in {"InputEventMouseButton", "InputEventMouseMotion"}:
74
+ return "mouse"
75
+ if event_type in {"InputEventJoypadButton", "InputEventJoypadMotion"}:
76
+ return "gamepad"
77
+ if event_type in {"InputEventScreenTouch", "InputEventScreenDrag", "InputEventGesture"}:
78
+ return "touch"
79
+ return "unknown"
80
+
81
+
82
+ def _signature(event_type: str, event_body: str) -> str:
83
+ if event_type == "InputEventKey":
84
+ return f"keyboard:key:{_field(event_body, 'physical_keycode') or _field(event_body, 'keycode')}"
85
+ if event_type == "InputEventMouseButton":
86
+ return f"mouse:button:{_field(event_body, 'button_index')}"
87
+ if event_type == "InputEventJoypadButton":
88
+ return f"gamepad:button:{_field(event_body, 'button_index')}"
89
+ if event_type == "InputEventJoypadMotion":
90
+ return f"gamepad:axis:{_field(event_body, 'axis')}"
91
+ if event_type in {"InputEventScreenTouch", "InputEventScreenDrag", "InputEventGesture"}:
92
+ return f"touch:index:{_field(event_body, 'index') or 'any'}"
93
+ return f"{event_type}:{' '.join(event_body.split())}"
94
+
95
+
96
+ def _field(event_body: str, name: str) -> str:
97
+ match = re.search(rf'"{re.escape(name)}"\s*:\s*([^,\)]+)', event_body)
98
+ if not match:
99
+ return ""
100
+ return match.group(1).strip().strip('"')
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class InputEvent:
8
+ event_type: str
9
+ device: str
10
+ signature: str
11
+
12
+ def to_dict(self) -> dict[str, str]:
13
+ return {"event_type": self.event_type, "device": self.device, "signature": self.signature}
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class InputAction:
18
+ name: str
19
+ events: list[InputEvent] = field(default_factory=list)
20
+
21
+ @property
22
+ def devices(self) -> set[str]:
23
+ return {event.device for event in self.events if event.device != "unknown"}
24
+
25
+ def to_dict(self) -> dict[str, object]:
26
+ return {
27
+ "name": self.name,
28
+ "devices": sorted(self.devices),
29
+ "events": [event.to_dict() for event in self.events],
30
+ }
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Finding:
35
+ rule_id: str
36
+ severity: str
37
+ action: str | None
38
+ message: str
39
+
40
+ def to_dict(self) -> dict[str, object]:
41
+ return {
42
+ "rule_id": self.rule_id,
43
+ "severity": self.severity,
44
+ "action": self.action,
45
+ "message": self.message,
46
+ }
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+
6
+ from .models import Finding, InputAction
7
+
8
+
9
+ def render_text_report(actions: list[InputAction], findings: list[Finding]) -> str:
10
+ lines = [
11
+ "Godot Input Map Auditor",
12
+ f"Scanned {len(actions)} action(s): {len(findings)} finding(s).",
13
+ ]
14
+ for action in actions:
15
+ devices = ", ".join(sorted(action.devices)) or "none"
16
+ lines.append(f"- {action.name}: {devices} ({len(action.events)} event(s))")
17
+ if findings:
18
+ for finding in findings:
19
+ lines.append(f"[{finding.severity.upper()}] {finding.rule_id}: {finding.message}")
20
+ else:
21
+ lines.append("No findings.")
22
+ return "\n".join(lines)
23
+
24
+
25
+ def render_json_report(actions: list[InputAction], findings: list[Finding]) -> str:
26
+ payload = {
27
+ "tool": "godot-input-map-auditor",
28
+ "summary": {
29
+ "actions": len(actions),
30
+ "findings": len(findings),
31
+ "errors": sum(1 for finding in findings if finding.severity == "error"),
32
+ "warnings": sum(1 for finding in findings if finding.severity == "warning"),
33
+ },
34
+ "actions": [action.to_dict() for action in actions],
35
+ "findings": [finding.to_dict() for finding in findings],
36
+ }
37
+ return json.dumps(payload, indent=2, sort_keys=True)
38
+
39
+
40
+ def render_sarif_report(actions: list[InputAction], findings: list[Finding]) -> str:
41
+ rules = {}
42
+ results = []
43
+ for finding in findings:
44
+ rules.setdefault(
45
+ finding.rule_id,
46
+ {
47
+ "id": finding.rule_id,
48
+ "name": finding.rule_id,
49
+ "shortDescription": {"text": finding.message},
50
+ },
51
+ )
52
+ results.append(
53
+ {
54
+ "ruleId": finding.rule_id,
55
+ "level": _sarif_level(finding.severity),
56
+ "message": {"text": finding.message},
57
+ "locations": [
58
+ {
59
+ "physicalLocation": {"artifactLocation": {"uri": "project.godot"}},
60
+ "logicalLocations": [{"name": finding.action or "input"}],
61
+ }
62
+ ],
63
+ }
64
+ )
65
+ payload = {
66
+ "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
67
+ "version": "2.1.0",
68
+ "runs": [
69
+ {
70
+ "tool": {
71
+ "driver": {
72
+ "name": "godot-input-map-auditor",
73
+ "rules": list(rules.values()),
74
+ }
75
+ },
76
+ "results": results,
77
+ }
78
+ ],
79
+ }
80
+ return json.dumps(payload, indent=2, sort_keys=True)
81
+
82
+
83
+ def render_markdown_reference(actions: list[InputAction]) -> str:
84
+ lines = [
85
+ "# Input Reference",
86
+ "",
87
+ "| Action | Devices | Events |",
88
+ "|---|---|---:|",
89
+ ]
90
+ for action in sorted(actions, key=lambda item: item.name):
91
+ devices = ", ".join(sorted(action.devices)) or "none"
92
+ lines.append(f"| {action.name} | {devices} | {len(action.events)} |")
93
+ if not actions:
94
+ lines.append("| none | none | 0 |")
95
+ lines.append("")
96
+ return "\n".join(lines)
97
+
98
+
99
+ def render_gdscript_constants(actions: list[InputAction]) -> str:
100
+ lines = [
101
+ "class_name InputActions",
102
+ "",
103
+ "# Generated by godot-input-map-auditor.",
104
+ ]
105
+ for action in sorted(actions, key=lambda item: item.name):
106
+ lines.append(f'const {_constant_name(action.name)} = "{action.name}"')
107
+ lines.append("")
108
+ return "\n".join(lines)
109
+
110
+
111
+ def _constant_name(action: str) -> str:
112
+ value = re.sub(r"[^A-Za-z0-9]+", "_", action).strip("_").upper()
113
+ if not value:
114
+ return "ACTION"
115
+ if value[0].isdigit():
116
+ return f"ACTION_{value}"
117
+ return value
118
+
119
+
120
+ def _sarif_level(severity: str) -> str:
121
+ if severity == "error":
122
+ return "error"
123
+ if severity == "warning":
124
+ return "warning"
125
+ return "note"
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: godot-input-map-auditor
3
+ Version: 0.1.0
4
+ Summary: Audit Godot input maps for device coverage, duplicate bindings, and generated references.
5
+ Author: Godot Input Map Auditor contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/NonniGB/godot-production-toolkit/tree/main/godot-input-map-auditor
8
+ Project-URL: Issues, https://github.com/NonniGB/godot-production-toolkit/issues
9
+ Keywords: godot,input,touch,gamepad,ci,gamedev
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Classifier: Topic :: Utilities
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # Godot Input Map Auditor
24
+
25
+ Audit Godot input actions for keyboard, mouse, gamepad, and touch readiness, then generate input reference docs and strongly named GDScript constants.
26
+
27
+ This is a small CI-friendly tool for projects where input coverage can drift across desktop, controller, and mobile targets.
28
+
29
+ ## Install
30
+
31
+ ```powershell
32
+ python -m pip install -e .
33
+ ```
34
+
35
+ When published:
36
+
37
+ ```powershell
38
+ python -m pip install godot-input-map-auditor
39
+ ```
40
+
41
+ ## Quick Start
42
+
43
+ ```powershell
44
+ godot-input-audit C:\Projects\MyGame --require keyboard,touch
45
+ godot-input-audit . --write-docs docs\INPUT_REFERENCE.md
46
+ godot-input-audit . --generate-gd scripts\generated\input_actions.gd
47
+ godot-input-audit . --format json --output input-report.json
48
+ ```
49
+
50
+ Run against the sample project:
51
+
52
+ ```powershell
53
+ godot-input-audit examples\tiny-godot-project --fail-on none
54
+ ```
55
+
56
+ ## What It Checks
57
+
58
+ - Parses the `[input]` section in `project.godot`.
59
+ - Classifies `InputEventKey`, mouse, joypad, and screen-touch event classes.
60
+ - Reports actions missing required device families.
61
+ - Reports duplicate normalized bindings across actions.
62
+ - Generates `INPUT_REFERENCE.md`.
63
+ - Generates optional `InputActions` GDScript constants.
64
+
65
+ ## Documentation
66
+
67
+ - [Device classification](docs/DEVICE_RULES.md)
68
+ - [Rule reference](docs/RULE_REFERENCE.md)
69
+ - [Generated output](docs/GENERATED_OUTPUT.md)
70
+ - [Touch readiness](docs/TOUCH_READINESS.md)
71
+ - [CI usage](docs/CI.md)
72
+
73
+ ## Development
74
+
75
+ ```powershell
76
+ python -m pip install -e .
77
+ python -m unittest discover -s tests -v
78
+ godot-input-audit examples\tiny-godot-project --require keyboard --fail-on none
79
+ ```
80
+
81
+ Examples and generated docs use generic action names so the repository can be published safely.
@@ -0,0 +1,19 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/godot_input_auditor/__init__.py
5
+ src/godot_input_auditor/__main__.py
6
+ src/godot_input_auditor/audit.py
7
+ src/godot_input_auditor/cli.py
8
+ src/godot_input_auditor/input_parser.py
9
+ src/godot_input_auditor/models.py
10
+ src/godot_input_auditor/reporting.py
11
+ src/godot_input_map_auditor.egg-info/PKG-INFO
12
+ src/godot_input_map_auditor.egg-info/SOURCES.txt
13
+ src/godot_input_map_auditor.egg-info/dependency_links.txt
14
+ src/godot_input_map_auditor.egg-info/entry_points.txt
15
+ src/godot_input_map_auditor.egg-info/top_level.txt
16
+ tests/test_audit.py
17
+ tests/test_cli.py
18
+ tests/test_parser.py
19
+ tests/test_reporting.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ godot-input-audit = godot_input_auditor.cli:entrypoint
@@ -0,0 +1,40 @@
1
+ import unittest
2
+
3
+ from godot_input_auditor.audit import evaluate_actions
4
+ from godot_input_auditor.models import InputAction, InputEvent
5
+
6
+
7
+ class AuditTests(unittest.TestCase):
8
+ def test_missing_required_devices_are_reported(self) -> None:
9
+ action = InputAction(
10
+ name="confirm",
11
+ events=[InputEvent(event_type="InputEventKey", device="keyboard", signature="key:13")],
12
+ )
13
+
14
+ findings = evaluate_actions([action], required_devices={"keyboard", "touch"})
15
+
16
+ self.assertEqual(findings[0].rule_id, "missing_required_device")
17
+ self.assertIn("touch", findings[0].message)
18
+
19
+ def test_duplicate_bindings_are_reported(self) -> None:
20
+ actions = [
21
+ InputAction(
22
+ name="move_left",
23
+ events=[InputEvent(event_type="InputEventKey", device="keyboard", signature="key:65")],
24
+ ),
25
+ InputAction(
26
+ name="menu_left",
27
+ events=[InputEvent(event_type="InputEventKey", device="keyboard", signature="key:65")],
28
+ ),
29
+ ]
30
+
31
+ findings = evaluate_actions(actions, required_devices=set())
32
+
33
+ self.assertEqual(len(findings), 1)
34
+ self.assertEqual(findings[0].rule_id, "duplicate_binding")
35
+ self.assertIn("move_left", findings[0].message)
36
+ self.assertIn("menu_left", findings[0].message)
37
+
38
+
39
+ if __name__ == "__main__":
40
+ unittest.main()
@@ -0,0 +1,80 @@
1
+ from contextlib import redirect_stdout
2
+ from io import StringIO
3
+ import tempfile
4
+ import unittest
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from godot_input_auditor.cli import main
9
+
10
+
11
+ class CliTests(unittest.TestCase):
12
+ def test_cli_prints_version(self) -> None:
13
+ stdout = StringIO()
14
+
15
+ with self.assertRaises(SystemExit) as raised:
16
+ with redirect_stdout(stdout):
17
+ main(["--version"])
18
+
19
+ self.assertEqual(raised.exception.code, 0)
20
+ self.assertIn("godot-input-audit 0.1.0", stdout.getvalue())
21
+
22
+ def test_cli_generates_docs_and_constants(self) -> None:
23
+ with tempfile.TemporaryDirectory() as tmp:
24
+ project = Path(tmp)
25
+ (project / "project.godot").write_text(
26
+ """
27
+ [input]
28
+ confirm={
29
+ "events": [Object(InputEventKey,"physical_keycode":13)]
30
+ }
31
+ """,
32
+ encoding="utf-8",
33
+ )
34
+ docs = project / "docs" / "INPUT_REFERENCE.md"
35
+ constants = project / "scripts" / "generated" / "input_actions.gd"
36
+
37
+ exit_code = main(
38
+ [
39
+ str(project),
40
+ "--require",
41
+ "keyboard,touch",
42
+ "--write-docs",
43
+ str(docs),
44
+ "--generate-gd",
45
+ str(constants),
46
+ ]
47
+ )
48
+
49
+ self.assertEqual(exit_code, 1)
50
+ self.assertIn("confirm", docs.read_text(encoding="utf-8"))
51
+ self.assertIn('CONFIRM = "confirm"', constants.read_text(encoding="utf-8"))
52
+
53
+ def test_cli_outputs_sarif_report(self) -> None:
54
+ with tempfile.TemporaryDirectory() as tmp:
55
+ project = Path(tmp)
56
+ (project / "project.godot").write_text(
57
+ """
58
+ [input]
59
+ confirm={
60
+ "events": [Object(InputEventKey,"physical_keycode":13)]
61
+ }
62
+ """,
63
+ encoding="utf-8",
64
+ )
65
+ stdout = StringIO()
66
+
67
+ with redirect_stdout(stdout):
68
+ exit_code = main([str(project), "--require", "keyboard,touch", "--format", "sarif", "--fail-on", "none"])
69
+
70
+ sarif = json.loads(stdout.getvalue())
71
+ self.assertEqual(exit_code, 0)
72
+ self.assertEqual(sarif["version"], "2.1.0")
73
+ driver = sarif["runs"][0]["tool"]["driver"]
74
+ self.assertEqual(driver["name"], "godot-input-map-auditor")
75
+ self.assertTrue(driver["rules"])
76
+ self.assertTrue(sarif["runs"][0]["results"])
77
+
78
+
79
+ if __name__ == "__main__":
80
+ unittest.main()
@@ -0,0 +1,43 @@
1
+ import unittest
2
+
3
+ from godot_input_auditor.input_parser import parse_input_map
4
+
5
+
6
+ PROJECT = """
7
+ config_version=5
8
+
9
+ [input]
10
+ move_left={
11
+ "deadzone": 0.5,
12
+ "events": [Object(InputEventKey,"physical_keycode":65), Object(InputEventJoypadButton,"button_index":14)]
13
+ }
14
+ tap_select={
15
+ "deadzone": 0.5,
16
+ "events": [Object(InputEventScreenTouch,"index":0)]
17
+ }
18
+ zoom={
19
+ "deadzone": 0.5,
20
+ "events": [Object(InputEventMouseButton,"button_index":4)]
21
+ }
22
+
23
+ [rendering]
24
+ renderer/rendering_method="mobile"
25
+ """
26
+
27
+
28
+ class ParserTests(unittest.TestCase):
29
+ def test_parses_actions_and_classifies_device_families(self) -> None:
30
+ actions = parse_input_map(PROJECT)
31
+ by_name = {action.name: action for action in actions}
32
+
33
+ self.assertEqual(set(by_name), {"move_left", "tap_select", "zoom"})
34
+ self.assertEqual(by_name["move_left"].devices, {"keyboard", "gamepad"})
35
+ self.assertEqual(by_name["tap_select"].devices, {"touch"})
36
+ self.assertEqual(by_name["zoom"].devices, {"mouse"})
37
+
38
+ def test_returns_empty_list_when_input_section_missing(self) -> None:
39
+ self.assertEqual(parse_input_map("config_version=5\n"), [])
40
+
41
+
42
+ if __name__ == "__main__":
43
+ unittest.main()
@@ -0,0 +1,42 @@
1
+ import json
2
+ import unittest
3
+
4
+ from godot_input_auditor.models import InputAction, InputEvent
5
+ from godot_input_auditor.reporting import (
6
+ render_gdscript_constants,
7
+ render_json_report,
8
+ render_markdown_reference,
9
+ )
10
+
11
+
12
+ class ReportingTests(unittest.TestCase):
13
+ def test_markdown_reference_lists_actions_and_devices(self) -> None:
14
+ action = InputAction(
15
+ name="move_left",
16
+ events=[InputEvent(event_type="InputEventKey", device="keyboard", signature="key:65")],
17
+ )
18
+
19
+ markdown = render_markdown_reference([action])
20
+
21
+ self.assertIn("# Input Reference", markdown)
22
+ self.assertIn("| move_left | keyboard | 1 |", markdown)
23
+
24
+ def test_gdscript_constants_are_generated(self) -> None:
25
+ actions = [InputAction(name="move_left", events=[]), InputAction(name="ui_accept", events=[])]
26
+
27
+ gdscript = render_gdscript_constants(actions)
28
+
29
+ self.assertIn('const MOVE_LEFT = "move_left"', gdscript)
30
+ self.assertIn('const UI_ACCEPT = "ui_accept"', gdscript)
31
+
32
+ def test_json_report_contains_summary(self) -> None:
33
+ action = InputAction(name="tap", events=[InputEvent("InputEventScreenTouch", "touch", "touch:0")])
34
+
35
+ report = json.loads(render_json_report([action], []))
36
+
37
+ self.assertEqual(report["summary"]["actions"], 1)
38
+ self.assertEqual(report["actions"][0]["devices"], ["touch"])
39
+
40
+
41
+ if __name__ == "__main__":
42
+ unittest.main()