inscript-lang 2.11.0__tar.gz → 2.13.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.
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/PKG-INFO +1 -1
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/errors.py +18 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/export_pipeline.py +112 -1
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript.py +64 -8
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/PKG-INFO +1 -1
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/SOURCES.txt +3 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/top_level.txt +3 -0
- inscript_lang-2.13.0/inscript_studio_api.py +295 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/pyproject.toml +2 -1
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/setup.py +1 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_game.py +40 -0
- inscript_lang-2.13.0/studio_bridge.py +585 -0
- inscript_lang-2.13.0/studio_readiness.py +408 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/README.md +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/analyzer.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/ast_nodes.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/compiler.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/environment.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/hot_reload.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_dap.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_fmt.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/dependency_links.txt +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/entry_points.txt +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_lang.egg-info/requires.txt +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/inscript_test.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/interpreter.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/lexer.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/parser.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/pygame_backend.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/repl.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/scene_tree.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/setup.cfg +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_assets.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_extended.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_extended_2.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/stdlib_values.py +0 -0
- {inscript_lang-2.11.0 → inscript_lang-2.13.0}/vm.py +0 -0
|
@@ -85,6 +85,24 @@ class InScriptError(Exception):
|
|
|
85
85
|
self.code = code or ERROR_CODES.get(cls_name, "E0000")
|
|
86
86
|
super().__init__(self._format())
|
|
87
87
|
|
|
88
|
+
def to_dict(self) -> dict:
|
|
89
|
+
"""v2.12.0: Structured dict for --json-errors / Studio error panel."""
|
|
90
|
+
return {
|
|
91
|
+
"type": "error",
|
|
92
|
+
"code": self.code,
|
|
93
|
+
"class": type(self).__name__,
|
|
94
|
+
"message": self.message,
|
|
95
|
+
"line": self.line,
|
|
96
|
+
"col": self.col,
|
|
97
|
+
"source_line": self.source_line,
|
|
98
|
+
"hint": self.hint,
|
|
99
|
+
"call_trace": self.call_trace,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def to_json(self) -> str:
|
|
103
|
+
import json as _j
|
|
104
|
+
return _j.dumps(self.to_dict(), separators=(",", ":"))
|
|
105
|
+
|
|
88
106
|
def _format(self) -> str:
|
|
89
107
|
code = self.code
|
|
90
108
|
cls = type(self).__name__
|
|
@@ -42,7 +42,7 @@ MANIFEST_FILE = "inscript.toml"
|
|
|
42
42
|
ENTRY_DEFAULT = os.path.join("src", "main.ins")
|
|
43
43
|
BUILD_MANIFEST = "build.toml"
|
|
44
44
|
|
|
45
|
-
VALID_TARGETS = ("desktop", "web", "android", "ibc")
|
|
45
|
+
VALID_TARGETS = ("desktop", "web", "android", "ios", "ibc")
|
|
46
46
|
|
|
47
47
|
# Pyodide CDN base used in web builds
|
|
48
48
|
PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.27.0/full/"
|
|
@@ -645,3 +645,114 @@ def run_build(target: str, project_dir: str = ".") -> int:
|
|
|
645
645
|
for e in r.errors:
|
|
646
646
|
print(f"[build] {e}", file=sys.stderr)
|
|
647
647
|
return 0 if r.success else 1
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
651
|
+
# v2.13.0: Target: iOS
|
|
652
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
653
|
+
|
|
654
|
+
def build_ios(project_dir: str = ".") -> BuildResult:
|
|
655
|
+
"""
|
|
656
|
+
Build an iOS app via BeeWare Briefcase.
|
|
657
|
+
Requires: macOS + Xcode + pip install briefcase
|
|
658
|
+
|
|
659
|
+
Output layout:
|
|
660
|
+
build/ios/
|
|
661
|
+
pyproject.toml — Briefcase config
|
|
662
|
+
app.py — InScript iOS entry point
|
|
663
|
+
build.toml — build manifest
|
|
664
|
+
"""
|
|
665
|
+
import time, subprocess
|
|
666
|
+
t0 = time.perf_counter()
|
|
667
|
+
errors: List[str] = []
|
|
668
|
+
artifacts: List[str] = []
|
|
669
|
+
|
|
670
|
+
manifest = _read_manifest(project_dir)
|
|
671
|
+
pkg = manifest.get("package", {})
|
|
672
|
+
version = pkg.get("version", "0.0.0")
|
|
673
|
+
name = pkg.get("name", "mygame")
|
|
674
|
+
entry = pkg.get("entry", ENTRY_DEFAULT)
|
|
675
|
+
bundle = pkg.get("bundle", f"dev.inscript.{name}")
|
|
676
|
+
|
|
677
|
+
out_dir = os.path.join(project_dir, BUILD_DIR, "ios")
|
|
678
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
679
|
+
|
|
680
|
+
bp_path = os.path.join(out_dir, "pyproject.toml")
|
|
681
|
+
with open(bp_path, "w") as f:
|
|
682
|
+
f.write(textwrap.dedent(f"""\
|
|
683
|
+
[tool.briefcase]
|
|
684
|
+
project_name = "{name}"
|
|
685
|
+
bundle = "{bundle}"
|
|
686
|
+
version = "{version}"
|
|
687
|
+
description = "Built with InScript"
|
|
688
|
+
license = "MIT"
|
|
689
|
+
|
|
690
|
+
[tool.briefcase.app.{name}]
|
|
691
|
+
formal_name = "{name}"
|
|
692
|
+
description = "Built with InScript"
|
|
693
|
+
sources = ["."]
|
|
694
|
+
requires = ["inscript-lang"]
|
|
695
|
+
|
|
696
|
+
[tool.briefcase.app.{name}.iOS]
|
|
697
|
+
requires = ["toga-iOS"]
|
|
698
|
+
"""))
|
|
699
|
+
artifacts.append(bp_path)
|
|
700
|
+
|
|
701
|
+
app_py = os.path.join(out_dir, "app.py")
|
|
702
|
+
with open(app_py, "w") as f:
|
|
703
|
+
f.write(textwrap.dedent(f"""\
|
|
704
|
+
# InScript iOS entry — generated by inscript build --target ios
|
|
705
|
+
import sys
|
|
706
|
+
try:
|
|
707
|
+
import inscript
|
|
708
|
+
inscript.main(["{entry}"])
|
|
709
|
+
except Exception as e:
|
|
710
|
+
print(f"InScript launch error: {{e}}", file=sys.stderr)
|
|
711
|
+
"""))
|
|
712
|
+
artifacts.append(app_py)
|
|
713
|
+
|
|
714
|
+
for sub in ("src", "assets", "scenes"):
|
|
715
|
+
s = os.path.join(project_dir, sub)
|
|
716
|
+
d = os.path.join(out_dir, sub)
|
|
717
|
+
if os.path.isdir(s):
|
|
718
|
+
if os.path.exists(d): shutil.rmtree(d)
|
|
719
|
+
shutil.copytree(s, d)
|
|
720
|
+
|
|
721
|
+
from inscript import VERSION as _IV
|
|
722
|
+
bm = _write_build_manifest(out_dir, {
|
|
723
|
+
"target": "ios", "version": version, "entry": entry,
|
|
724
|
+
"inscript_version": _IV, "bundle": bundle,
|
|
725
|
+
"artifacts": {"pyproject": "pyproject.toml", "app": "app.py"},
|
|
726
|
+
"asset_hashes": {},
|
|
727
|
+
})
|
|
728
|
+
artifacts.append(bm)
|
|
729
|
+
|
|
730
|
+
# Check platform and briefcase
|
|
731
|
+
if sys.platform != "darwin":
|
|
732
|
+
msg = "iOS builds require macOS. Current platform: " + sys.platform
|
|
733
|
+
errors.append(msg)
|
|
734
|
+
print(f"[build:ios] ⚠ {msg}")
|
|
735
|
+
elapsed = (time.perf_counter() - t0) * 1000
|
|
736
|
+
return BuildResult("ios", False, out_dir, artifacts, errors, elapsed)
|
|
737
|
+
|
|
738
|
+
briefcase_ok = shutil.which("briefcase") is not None
|
|
739
|
+
if not briefcase_ok:
|
|
740
|
+
msg = ("BeeWare Briefcase not installed. "
|
|
741
|
+
"Install: pip install briefcase "
|
|
742
|
+
"Then: cd build/ios && briefcase create iOS && briefcase build iOS")
|
|
743
|
+
errors.append(msg)
|
|
744
|
+
print(f"[build:ios] ⚠ {msg}")
|
|
745
|
+
elapsed = (time.perf_counter() - t0) * 1000
|
|
746
|
+
return BuildResult("ios", False, out_dir, artifacts, errors, elapsed)
|
|
747
|
+
|
|
748
|
+
try:
|
|
749
|
+
subprocess.run([sys.executable, "-m", "briefcase", "create", "iOS"],
|
|
750
|
+
cwd=out_dir, check=True)
|
|
751
|
+
subprocess.run([sys.executable, "-m", "briefcase", "build", "iOS"],
|
|
752
|
+
cwd=out_dir, check=True)
|
|
753
|
+
print(f"[build:ios] ✅ iOS app built → {out_dir}")
|
|
754
|
+
except subprocess.CalledProcessError as e:
|
|
755
|
+
errors.append(f"briefcase failed: {e}")
|
|
756
|
+
|
|
757
|
+
elapsed = (time.perf_counter() - t0) * 1000
|
|
758
|
+
return BuildResult("ios", len(errors) == 0, out_dir, artifacts, errors, elapsed)
|
|
@@ -24,7 +24,7 @@ from errors import (InScriptError, LexerError, ParseError,
|
|
|
24
24
|
SemanticError, InScriptRuntimeError,
|
|
25
25
|
MultiError, InScriptWarning)
|
|
26
26
|
|
|
27
|
-
VERSION = "2.
|
|
27
|
+
VERSION = "2.13.0"
|
|
28
28
|
|
|
29
29
|
MANIFEST_FILENAME = "inscript.toml"
|
|
30
30
|
LOCK_FILENAME = "inscript.lock"
|
|
@@ -2385,21 +2385,34 @@ def run_source(source: str, filename: str = "<stdin>",
|
|
|
2385
2385
|
no_warn_unused: bool = False,
|
|
2386
2386
|
warn_as_error: bool = False,
|
|
2387
2387
|
strict: bool = False,
|
|
2388
|
-
profile: bool = False
|
|
2388
|
+
profile: bool = False,
|
|
2389
|
+
json_errors: bool = False) -> int:
|
|
2389
2390
|
"""Lex, parse, analyze, and interpret InScript source code."""
|
|
2391
|
+
|
|
2392
|
+
def _emit_error(e):
|
|
2393
|
+
"""v2.12.0: emit as JSON or plain text depending on flag."""
|
|
2394
|
+
if json_errors:
|
|
2395
|
+
from inscript_studio_api import error_stream
|
|
2396
|
+
from errors import InScriptError as _ISE
|
|
2397
|
+
if isinstance(e, _ISE):
|
|
2398
|
+
error_stream([e])
|
|
2399
|
+
else:
|
|
2400
|
+
error_stream([{"type": "error", "code": "E0000",
|
|
2401
|
+
"class": type(e).__name__, "message": str(e),
|
|
2402
|
+
"line": 0, "col": 0}])
|
|
2403
|
+
else:
|
|
2404
|
+
print(e, file=sys.stderr)
|
|
2390
2405
|
# ── 1. Lex ────────────────────────────────────────────────────────────
|
|
2391
2406
|
try:
|
|
2392
2407
|
tokens = tokenize(source, filename)
|
|
2393
2408
|
except LexerError as e:
|
|
2394
|
-
|
|
2395
|
-
return 1
|
|
2409
|
+
_emit_error(e); return 1
|
|
2396
2410
|
|
|
2397
2411
|
# ── 2. Parse ──────────────────────────────────────────────────────────
|
|
2398
2412
|
try:
|
|
2399
2413
|
program = parse(source, filename)
|
|
2400
2414
|
except ParseError as e:
|
|
2401
|
-
|
|
2402
|
-
return 1
|
|
2415
|
+
_emit_error(e); return 1
|
|
2403
2416
|
|
|
2404
2417
|
# ── 3. Analyze (optional) — Phase 3: multi-error + warnings ──────────
|
|
2405
2418
|
if type_check:
|
|
@@ -2442,7 +2455,7 @@ def run_source(source: str, filename: str = "<stdin>",
|
|
|
2442
2455
|
interp.run(program)
|
|
2443
2456
|
return 0
|
|
2444
2457
|
except InScriptError as e:
|
|
2445
|
-
|
|
2458
|
+
_emit_error(e)
|
|
2446
2459
|
return 1
|
|
2447
2460
|
except KeyboardInterrupt:
|
|
2448
2461
|
print("\n[InScript] Interrupted.", file=sys.stderr)
|
|
@@ -2728,6 +2741,17 @@ Examples:
|
|
|
2728
2741
|
help="v2.11.0: Project root for --build (default: current dir)")
|
|
2729
2742
|
parser.add_argument("--new", metavar="NAME",
|
|
2730
2743
|
help="v2.11.0: Scaffold a new InScript project: inscript --new mygame")
|
|
2744
|
+
# v2.12.0: studio readiness
|
|
2745
|
+
parser.add_argument("--json-errors", action="store_true",
|
|
2746
|
+
help="v2.12.0: Output errors as newline-delimited JSON for IDE panels")
|
|
2747
|
+
parser.add_argument("--inspect", metavar="DIR", nargs="?", const=".",
|
|
2748
|
+
help="v2.12.0: Project introspection → JSON (scenes, nodes, assets, fns)")
|
|
2749
|
+
parser.add_argument("--check-v3", action="store_true",
|
|
2750
|
+
help="v2.12.0: Run v3.0.0 readiness gates; exits 0 if all pass")
|
|
2751
|
+
parser.add_argument("--studio-bridge", action="store_true",
|
|
2752
|
+
help="v2.12.0: Start Electron bridge JSON-RPC server")
|
|
2753
|
+
parser.add_argument("--bridge-port", type=int, default=8765,
|
|
2754
|
+
help="v2.12.0: Port for --studio-bridge (default: 8765)")
|
|
2731
2755
|
parser.add_argument("--lsp", action="store_true",
|
|
2732
2756
|
help="Start the Language Server (requires: pip install pygls)")
|
|
2733
2757
|
parser.add_argument("--game", action="store_true",
|
|
@@ -3012,6 +3036,37 @@ Examples:
|
|
|
3012
3036
|
except Exception as _bld:
|
|
3013
3037
|
print(f"[build] {_bld}", file=sys.stderr); return 1
|
|
3014
3038
|
|
|
3039
|
+
# ── v2.12.0 studio readiness ───────────────────────────────────────────
|
|
3040
|
+
if getattr(args, 'check_v3', False):
|
|
3041
|
+
from studio_readiness import run_all_gates
|
|
3042
|
+
base = getattr(args, 'project_dir', '.') or '.'
|
|
3043
|
+
_, all_passed = run_all_gates(base_dir=base)
|
|
3044
|
+
return 0 if all_passed else 1
|
|
3045
|
+
|
|
3046
|
+
if getattr(args, 'inspect', None) is not None:
|
|
3047
|
+
try:
|
|
3048
|
+
from inscript_studio_api import project_inspect
|
|
3049
|
+
import json as _j
|
|
3050
|
+
data = project_inspect(args.inspect)
|
|
3051
|
+
print(_j.dumps(data, indent=2))
|
|
3052
|
+
except Exception as _ie:
|
|
3053
|
+
print(f"[inspect] {_ie}", file=sys.stderr); return 1
|
|
3054
|
+
return 0
|
|
3055
|
+
|
|
3056
|
+
if getattr(args, 'studio_bridge', False):
|
|
3057
|
+
from studio_bridge import StudioBridge
|
|
3058
|
+
import time as _time
|
|
3059
|
+
port = getattr(args, 'bridge_port', 8765)
|
|
3060
|
+
bridge = StudioBridge(port=port)
|
|
3061
|
+
bridge.start()
|
|
3062
|
+
print(f"[studio-bridge] Listening on {bridge.url}")
|
|
3063
|
+
print(f"[studio-bridge] Ctrl+C to stop")
|
|
3064
|
+
try:
|
|
3065
|
+
while True: _time.sleep(1)
|
|
3066
|
+
except KeyboardInterrupt:
|
|
3067
|
+
bridge.stop()
|
|
3068
|
+
return 0
|
|
3069
|
+
|
|
3015
3070
|
# v1.9.1: --no-typecheck is deprecated — emit a warning and honour it
|
|
3016
3071
|
if getattr(args, 'no_typecheck', False):
|
|
3017
3072
|
print(
|
|
@@ -3097,7 +3152,8 @@ Examples:
|
|
|
3097
3152
|
return run_source(_source, filename=args.file, type_check=type_check,
|
|
3098
3153
|
no_warn=no_warn, no_warn_unused=no_warn_unused,
|
|
3099
3154
|
warn_as_error=warn_as_error, strict=strict,
|
|
3100
|
-
profile=profile
|
|
3155
|
+
profile=profile,
|
|
3156
|
+
json_errors=getattr(args, 'json_errors', False))
|
|
3101
3157
|
|
|
3102
3158
|
|
|
3103
3159
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -9,6 +9,7 @@ hot_reload.py
|
|
|
9
9
|
inscript.py
|
|
10
10
|
inscript_dap.py
|
|
11
11
|
inscript_fmt.py
|
|
12
|
+
inscript_studio_api.py
|
|
12
13
|
inscript_test.py
|
|
13
14
|
interpreter.py
|
|
14
15
|
lexer.py
|
|
@@ -24,6 +25,8 @@ stdlib_extended.py
|
|
|
24
25
|
stdlib_extended_2.py
|
|
25
26
|
stdlib_game.py
|
|
26
27
|
stdlib_values.py
|
|
28
|
+
studio_bridge.py
|
|
29
|
+
studio_readiness.py
|
|
27
30
|
vm.py
|
|
28
31
|
inscript_lang.egg-info/PKG-INFO
|
|
29
32
|
inscript_lang.egg-info/SOURCES.txt
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
inscript_studio_api.py — InScript v2.12.0 Studio Plugin API
|
|
4
|
+
================================================================
|
|
5
|
+
Stable extension points that InScript Studio (Electron) and third-party
|
|
6
|
+
plugins use to integrate with the runtime.
|
|
7
|
+
|
|
8
|
+
StudioPlugin — base class for plugins; override hook methods
|
|
9
|
+
StudioAPI — registry; emit events to all registered plugins
|
|
10
|
+
project_inspect — static analysis → JSON scene/node/fn/asset list
|
|
11
|
+
error_stream — format errors as newline-delimited JSON for the IDE panel
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
import os, re, json
|
|
16
|
+
from typing import Any, Dict, List, Optional
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
# Plugin base class
|
|
21
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
class StudioPlugin:
|
|
24
|
+
"""
|
|
25
|
+
Base class for Studio plugins. Override any hook you care about.
|
|
26
|
+
|
|
27
|
+
Plugins are registered with the global StudioAPI singleton:
|
|
28
|
+
from inscript_studio_api import get_api
|
|
29
|
+
get_api().register(MyPlugin())
|
|
30
|
+
"""
|
|
31
|
+
name: str = "unnamed_plugin"
|
|
32
|
+
|
|
33
|
+
def on_scene_save(self, scene_path: str, tree) -> None:
|
|
34
|
+
"""Called when a .inscene file is saved from the editor."""
|
|
35
|
+
|
|
36
|
+
def on_asset_import(self, handle) -> None:
|
|
37
|
+
"""Called when an AssetHandle is registered in the AssetRegistry."""
|
|
38
|
+
|
|
39
|
+
def on_error(self, error: dict) -> None:
|
|
40
|
+
"""Called when the interpreter raises an InScriptError.
|
|
41
|
+
error is a dict matching InScriptError.to_dict()."""
|
|
42
|
+
|
|
43
|
+
def on_breakpoint(self, file: str, line: int, env: dict) -> None:
|
|
44
|
+
"""Called when a breakpoint is hit (DAP integration)."""
|
|
45
|
+
|
|
46
|
+
def on_reload(self, result) -> None:
|
|
47
|
+
"""Called after every successful hot reload."""
|
|
48
|
+
|
|
49
|
+
def on_build(self, target: str, output_dir: str, success: bool) -> None:
|
|
50
|
+
"""Called after inscript build completes."""
|
|
51
|
+
|
|
52
|
+
def __repr__(self):
|
|
53
|
+
return f"<StudioPlugin {self.name}>"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
# StudioAPI registry
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
class StudioAPI:
|
|
61
|
+
"""
|
|
62
|
+
Central registry for Studio plugins. Emits lifecycle events to all
|
|
63
|
+
registered plugins. Singleton — use get_api() rather than instantiating.
|
|
64
|
+
"""
|
|
65
|
+
def __init__(self):
|
|
66
|
+
self._plugins: List[StudioPlugin] = []
|
|
67
|
+
|
|
68
|
+
def register(self, plugin: StudioPlugin) -> "StudioAPI":
|
|
69
|
+
if plugin not in self._plugins:
|
|
70
|
+
self._plugins.append(plugin)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def unregister(self, plugin: StudioPlugin) -> bool:
|
|
74
|
+
if plugin in self._plugins:
|
|
75
|
+
self._plugins.remove(plugin)
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def plugin_count(self) -> int:
|
|
80
|
+
return len(self._plugins)
|
|
81
|
+
|
|
82
|
+
def emit(self, event: str, *args, **kwargs) -> List[Any]:
|
|
83
|
+
"""
|
|
84
|
+
Emit an event to all registered plugins.
|
|
85
|
+
Returns list of return values (Nones filtered out).
|
|
86
|
+
"""
|
|
87
|
+
results = []
|
|
88
|
+
for plugin in list(self._plugins):
|
|
89
|
+
handler = getattr(plugin, event, None)
|
|
90
|
+
if handler is None:
|
|
91
|
+
continue
|
|
92
|
+
try:
|
|
93
|
+
rv = handler(*args, **kwargs)
|
|
94
|
+
if rv is not None:
|
|
95
|
+
results.append(rv)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"[StudioAPI] Plugin '{plugin.name}' error in {event}: {e}")
|
|
98
|
+
return results
|
|
99
|
+
|
|
100
|
+
# Convenience emitters matching StudioPlugin hooks
|
|
101
|
+
def emit_scene_save(self, scene_path: str, tree) -> None:
|
|
102
|
+
self.emit("on_scene_save", scene_path, tree)
|
|
103
|
+
|
|
104
|
+
def emit_asset_import(self, handle) -> None:
|
|
105
|
+
self.emit("on_asset_import", handle)
|
|
106
|
+
|
|
107
|
+
def emit_error(self, error: dict) -> None:
|
|
108
|
+
self.emit("on_error", error)
|
|
109
|
+
|
|
110
|
+
def emit_breakpoint(self, file: str, line: int, env: dict) -> None:
|
|
111
|
+
self.emit("on_breakpoint", file, line, env)
|
|
112
|
+
|
|
113
|
+
def emit_reload(self, result) -> None:
|
|
114
|
+
self.emit("on_reload", result)
|
|
115
|
+
|
|
116
|
+
def emit_build(self, target: str, output_dir: str, success: bool) -> None:
|
|
117
|
+
self.emit("on_build", target, output_dir, success)
|
|
118
|
+
|
|
119
|
+
def __repr__(self):
|
|
120
|
+
return f"<StudioAPI plugins={len(self._plugins)}>"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Module-level singleton
|
|
124
|
+
_GLOBAL_API = StudioAPI()
|
|
125
|
+
|
|
126
|
+
def get_api() -> StudioAPI:
|
|
127
|
+
return _GLOBAL_API
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
131
|
+
# Project introspection — static analysis without running the interpreter
|
|
132
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
# Patterns for extracting declarations from .ins source (fast, no full parse)
|
|
135
|
+
_RE_NODE = re.compile(r'^\s*node\s+(\w+)\s*\{', re.MULTILINE)
|
|
136
|
+
_RE_SCENE = re.compile(r'^\s*scene\s+(\w+)\s*\{', re.MULTILINE)
|
|
137
|
+
_RE_STRUCT = re.compile(r'^\s*struct\s+(\w+)\s*\{', re.MULTILINE)
|
|
138
|
+
_RE_FN = re.compile(r'^\s*(?:export\s+)?fn\s+(\w+)\s*\(', re.MULTILINE)
|
|
139
|
+
_RE_TEXTURE = re.compile(r'@texture\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
|
|
140
|
+
_RE_SOUND = re.compile(r'@sound\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
|
|
141
|
+
_RE_TILEMAP = re.compile(r'@tilemap\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
|
|
142
|
+
_RE_FONT = re.compile(r'@font\s*\(\s*["\']([^"\']+)["\']', re.MULTILINE)
|
|
143
|
+
_RE_IMPORT = re.compile(r'^\s*import\s+["\']([^"\']+)["\']', re.MULTILINE)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _scan_ins_file(file_path: str) -> dict:
|
|
147
|
+
"""
|
|
148
|
+
Scan a single .ins file and return a summary dict:
|
|
149
|
+
{file, nodes, scenes, structs, functions, assets, imports}
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
with open(file_path, encoding="utf-8") as f:
|
|
153
|
+
src = f.read()
|
|
154
|
+
except OSError:
|
|
155
|
+
return {}
|
|
156
|
+
|
|
157
|
+
rel = file_path # caller normalises
|
|
158
|
+
|
|
159
|
+
assets = []
|
|
160
|
+
for kind, pat in (("texture", _RE_TEXTURE), ("sound", _RE_SOUND),
|
|
161
|
+
("tilemap", _RE_TILEMAP), ("font", _RE_FONT)):
|
|
162
|
+
for m in pat.finditer(src):
|
|
163
|
+
assets.append({"type": kind, "path": m.group(1)})
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
"file": rel,
|
|
167
|
+
"nodes": [m.group(1) for m in _RE_NODE.finditer(src)],
|
|
168
|
+
"scenes": [m.group(1) for m in _RE_SCENE.finditer(src)],
|
|
169
|
+
"structs": [m.group(1) for m in _RE_STRUCT.finditer(src)],
|
|
170
|
+
"functions": [m.group(1) for m in _RE_FN.finditer(src)],
|
|
171
|
+
"assets": assets,
|
|
172
|
+
"imports": [m.group(1) for m in _RE_IMPORT.finditer(src)],
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _scan_inscene_file(file_path: str) -> Optional[dict]:
|
|
177
|
+
"""Read a .inscene file and return {file, scene_name, root, nodes}."""
|
|
178
|
+
try:
|
|
179
|
+
with open(file_path, encoding="utf-8") as f:
|
|
180
|
+
content = f.read()
|
|
181
|
+
except OSError:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
name_m = re.search(r'^name\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
|
185
|
+
root_m = re.search(r'^root\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
|
186
|
+
nodes = re.findall(r'^type\s*=\s*"([^"]+)"', content, re.MULTILINE)
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
"file": file_path,
|
|
190
|
+
"scene_name": name_m.group(1) if name_m else "",
|
|
191
|
+
"root": root_m.group(1) if root_m else "",
|
|
192
|
+
"nodes": nodes,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def project_inspect(project_dir: str = ".") -> dict:
|
|
197
|
+
"""
|
|
198
|
+
Statically analyse a project directory and return a JSON-serialisable
|
|
199
|
+
summary of all declarations. Used by the Studio scene panel, node
|
|
200
|
+
inspector, and asset browser.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
{
|
|
204
|
+
"project_dir": str,
|
|
205
|
+
"version": str, # inscript.VERSION
|
|
206
|
+
"sources": [...], # per-file scan results
|
|
207
|
+
"scenes": [...], # merged list of scene names
|
|
208
|
+
"nodes": [...], # merged list of node names
|
|
209
|
+
"structs": [...],
|
|
210
|
+
"functions": [...],
|
|
211
|
+
"assets": [...],
|
|
212
|
+
"scene_files": [...], # .inscene file summaries
|
|
213
|
+
"imports": [...],
|
|
214
|
+
}
|
|
215
|
+
"""
|
|
216
|
+
from inscript import VERSION
|
|
217
|
+
|
|
218
|
+
project_dir = os.path.abspath(project_dir)
|
|
219
|
+
sources = []
|
|
220
|
+
scene_files = []
|
|
221
|
+
|
|
222
|
+
all_scenes = []
|
|
223
|
+
all_nodes = []
|
|
224
|
+
all_structs = []
|
|
225
|
+
all_functions = []
|
|
226
|
+
all_assets = []
|
|
227
|
+
all_imports = set()
|
|
228
|
+
|
|
229
|
+
# Walk the project for .ins and .inscene files
|
|
230
|
+
for root, dirs, files in os.walk(project_dir):
|
|
231
|
+
# Skip build output and hidden dirs
|
|
232
|
+
dirs[:] = [d for d in dirs
|
|
233
|
+
if d not in ("build", "__pycache__", "inscript_runtime")
|
|
234
|
+
and not d.startswith(".")]
|
|
235
|
+
for fn in sorted(files):
|
|
236
|
+
full = os.path.join(root, fn)
|
|
237
|
+
rel = os.path.relpath(full, project_dir)
|
|
238
|
+
if fn.endswith(".ins"):
|
|
239
|
+
scan = _scan_ins_file(full)
|
|
240
|
+
scan["file"] = rel
|
|
241
|
+
sources.append(scan)
|
|
242
|
+
all_scenes += [{"name": n, "file": rel} for n in scan["nodes"]]
|
|
243
|
+
all_nodes += [{"name": n, "file": rel} for n in scan["nodes"]]
|
|
244
|
+
all_structs += [{"name": n, "file": rel} for n in scan["structs"]]
|
|
245
|
+
all_functions += [{"name": n, "file": rel} for n in scan["functions"]]
|
|
246
|
+
all_assets += [dict(a, file=rel) for a in scan["assets"]]
|
|
247
|
+
all_imports |= set(scan["imports"])
|
|
248
|
+
elif fn.endswith(".inscene"):
|
|
249
|
+
sf = _scan_inscene_file(full)
|
|
250
|
+
if sf:
|
|
251
|
+
sf["file"] = rel
|
|
252
|
+
scene_files.append(sf)
|
|
253
|
+
all_scenes.append({"name": sf["scene_name"], "file": rel,
|
|
254
|
+
"root": sf["root"]})
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
"project_dir": project_dir,
|
|
258
|
+
"version": VERSION,
|
|
259
|
+
"sources": sources,
|
|
260
|
+
"scenes": all_scenes,
|
|
261
|
+
"nodes": all_nodes,
|
|
262
|
+
"structs": all_structs,
|
|
263
|
+
"functions": all_functions,
|
|
264
|
+
"assets": all_assets,
|
|
265
|
+
"scene_files": scene_files,
|
|
266
|
+
"imports": sorted(all_imports),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
271
|
+
# Error stream — newline-delimited JSON for IDE panels
|
|
272
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
def error_stream(errors: list, file=None) -> None:
|
|
275
|
+
"""
|
|
276
|
+
Write errors as newline-delimited JSON to `file` (default: sys.stderr).
|
|
277
|
+
Each line is a self-contained JSON object.
|
|
278
|
+
|
|
279
|
+
Format:
|
|
280
|
+
{"type":"error","code":"E0042","message":"...","line":5,"col":3}
|
|
281
|
+
{"type":"error","code":"E0001","message":"...","line":12,"col":1}
|
|
282
|
+
"""
|
|
283
|
+
import sys as _sys
|
|
284
|
+
out = file or _sys.stderr
|
|
285
|
+
for err in errors:
|
|
286
|
+
if hasattr(err, "to_dict"):
|
|
287
|
+
d = err.to_dict()
|
|
288
|
+
elif isinstance(err, dict):
|
|
289
|
+
d = err
|
|
290
|
+
else:
|
|
291
|
+
d = {"type": "error", "code": "E0000",
|
|
292
|
+
"class": type(err).__name__, "message": str(err),
|
|
293
|
+
"line": 0, "col": 0}
|
|
294
|
+
out.write(json.dumps(d, separators=(",", ":")) + "\n")
|
|
295
|
+
out.flush()
|
|
@@ -61,7 +61,8 @@ py-modules = [
|
|
|
61
61
|
# ── Backends ──────────────────────────────────────────────────────────────
|
|
62
62
|
"pygame_backend",
|
|
63
63
|
# ── v2.7.0+ runtime modules ───────────────────────────────────────────────
|
|
64
|
-
"scene_tree", "hot_reload", "export_pipeline"
|
|
64
|
+
"scene_tree", "hot_reload", "export_pipeline",
|
|
65
|
+
"studio_bridge", "inscript_studio_api", "studio_readiness"
|
|
65
66
|
]
|
|
66
67
|
|
|
67
68
|
[tool.setuptools.package-data]
|
|
@@ -56,6 +56,7 @@ setup(
|
|
|
56
56
|
"pygame_backend",
|
|
57
57
|
# ── v2.7.0+ runtime modules ───────────────────────────────────────────
|
|
58
58
|
"scene_tree", "hot_reload", "export_pipeline",
|
|
59
|
+
"studio_bridge", "inscript_studio_api", "studio_readiness",
|
|
59
60
|
],
|
|
60
61
|
package_data = {"": ["examples/*.ins", "lsp/*.py", "*.md"]},
|
|
61
62
|
install_requires = [],
|