Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Ariane is a map viewer and editor for Grand Theft Auto III, Vice City and San An
- Preview models in 3D before placing them
- Build reusable prefabs from map selections, then browse, import and place them as a group
- Import custom DFF models and TXD textures
- Use object assets from an existing SA-MP/open.mp installation and import literal Pawn mappings through the agent CLI/MCP

### Saving and iteration

Expand Down
49 changes: 46 additions & 3 deletions tools/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,52 @@ draw distances. Names supply weak search hints. Dimensions, collision availabili
native IPL usage and thumbnails are not extracted by this basic importer;
use live `assets inspect ID --ensure-renderable` and `assets preview ID` for
runtime checks. `--has-collision` and dimension filters need an enriched index.
All IDE files under `data` are scanned, so `--defined-only` checks which entries
are actually defined in the running editor. Rebuild when changing games or mods;
use a separate `--db` for each installation because model IDs overlap.
All IDE files under `data` and explicit modloader `IDE` additions are scanned,
so `--defined-only` checks which entries are actually defined in the running
editor. Rebuild when changing games or mods; use a separate `--db` for each
installation because model IDs overlap.

### SA-MP/open.mp assets and Pawn mappings

Ariane does not distribute SA-MP, open.mp or GTA assets. Install open.mp or
SA-MP first, then create an isolated modloader package from those local files:

```powershell
.venv-agent\Scripts\arianectl.exe assets install-samp `
--gta-dir 'C:\Games\GTA San Andreas'
```

On Windows the command auto-detects open.mp's shared `SAMP` directory beneath
`%LOCALAPPDATA%\mp.open.launcher`. Use `--source` for another installed copy.
It requires `SAMP.ide`, `SAMP.img` and `SAMPCOL.img`, copies them under
`modloader\SAMP`, writes only a small gta.dat-style manifest, and rebuilds the
local catalogue. `SAMP.ipl` is deliberately not loaded because it contains
unrelated default placements. Restart Ariane after installing or updating the
package. `--force` is required to update an existing generated package.

Literal Pawn mappings can then be opened as a reversible scratch proposal:

```powershell
.venv-agent\Scripts\arianectl.exe import-pawn '.\mapping.pwn' `
--output '.\agent-output\mapping.ipl'
```

The importer supports `CreateObject`, `CreateDynamicObject`,
`CreatePlayerObject` and `RemoveBuildingForPlayer`. It preserves all three
rotation axes and applies building removals at their authored coordinates and
radius while the scene is open (Ariane's native suppression is model-specific
and planar, so the Pawn Z coordinate is retained but not used). Building
removals remain in the Pawn source because the IPL format cannot persist
`RemoveBuildingForPlayer`. By default the scratch session stays active for
validation and visual review; finish it with `session commit` or `session
rollback`, then use `save` after committing. `--commit --save` is available for
an explicitly requested one-shot import.

Pawn code is parsed, never executed. Numeric literals are accepted; variables,
macros, computed expressions and material/text calls are reported as warnings
instead of being guessed. Existing IPL destinations are never overwritten.
The MCP adapter exposes the same workflow as `inspect_pawn_mapping` and
`import_pawn_mapping`.

An optional existing gtastuff checkout supplies richer geometry, collision,
IPL and thumbnail metadata:
Expand Down
25 changes: 25 additions & 0 deletions tools/agent/ariane_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
from .service import ArianeService, ScenePatchError, DEFAULT_STATE_DIR
else:
from service import ArianeService, ScenePatchError, DEFAULT_STATE_DIR
if __package__:
from .samp import parse_pawn_mapping
else:
from samp import parse_pawn_mapping


service = ArianeService(
Expand Down Expand Up @@ -636,6 +640,27 @@ def open_mapping_scene(logical_path: str, physical_path: str) -> dict:
return service.engine("scene", [logical_path, str(Path(physical_path).resolve())])


@mcp.tool()
def inspect_pawn_mapping(source_path: str) -> dict:
"""Parse a .pwn mapping safely and report supported objects, removals and warnings without editing."""
return parse_pawn_mapping(Path(source_path)).summary()


@mcp.tool()
def import_pawn_mapping(source_path: str, logical_path: str, physical_path: str,
group: str | None = None, commit: bool = False,
save: bool = False) -> dict:
"""Import literal SA-MP/open.mp mapping calls into a new reviewable scratch IPL.

CreateObject, CreateDynamicObject, CreatePlayerObject and
RemoveBuildingForPlayer are supported. Existing IPLs are never overwritten.
Keep commit/save false for visual review, then use the normal finish and save tools.
"""
return service.import_pawn_mapping(
source_path, logical_path, physical_path, group=group,
commit=commit, save=save)


@mcp.tool()
def compare_mapping_variants(first: str, second: str) -> dict:
"""Compare two named checkpoints using durable keys and their saved creative brief."""
Expand Down
34 changes: 32 additions & 2 deletions tools/agent/arianectl.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
from .service import ArianeService, ScenePatchError, DEFAULT_STATE_DIR
else:
from service import ArianeService, ScenePatchError, DEFAULT_STATE_DIR
if __package__:
from .samp import install_samp_assets
else:
from samp import install_samp_assets


def emit(payload: object) -> None:
Expand Down Expand Up @@ -185,6 +189,16 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--state-dir", type=Path, default=DEFAULT_STATE_DIR)
sub = parser.add_subparsers(dest="command", required=True)
add_engine_commands(sub)
pawn_import = sub.add_parser("import-pawn", help="Import literal SA-MP/open.mp mapping calls")
pawn_import.add_argument("source", type=Path)
pawn_import.add_argument("--output", type=Path, required=True,
help="new IPL destination; existing files are never overwritten")
pawn_import.add_argument("--logical", help="logical IPL path used by Ariane")
pawn_import.add_argument("--group")
pawn_import.add_argument("--commit", action="store_true",
help="commit immediately instead of leaving a reviewable scratch session")
pawn_import.add_argument("--save", action="store_true",
help="save the IPL after commit; requires --commit")
call = sub.add_parser("call", help="Call a shared service method; JSON parameters inline or from a file")
call.add_argument("method")
inputs = call.add_mutually_exclusive_group()
Expand All @@ -207,6 +221,13 @@ def main(argv: list[str] | None = None) -> int:
sources.add_argument("--gta-dir", type=Path, help="build a basic catalogue from your GTA data/*.ide files")
index.add_argument("--semantic-assets", type=Path,
help="optional AI-enriched asset JSON; kept external when its license is unknown")
install_samp = asset_sub.add_parser("install-samp",
help="use assets from an existing SA-MP/open.mp installation")
install_samp.add_argument("--gta-dir", type=Path, required=True)
install_samp.add_argument("--source", type=Path,
help="directory containing SAMP.ide, SAMP.img and SAMPCOL.img; open.mp is auto-detected")
install_samp.add_argument("--force", action="store_true", help="update an existing generated package")
install_samp.add_argument("--no-index", action="store_true", help="do not rebuild the local asset catalogue")
search = asset_sub.add_parser("search")
search.add_argument("query", nargs="?", default="")
search.add_argument("--category"); search.add_argument("--max-width", type=float)
Expand Down Expand Up @@ -300,20 +321,29 @@ def main(argv: list[str] | None = None) -> int:
sub.add_parser("validate-composition")
args = parser.parse_args(argv)
try:
if args.command in {"call", "survey", "environment"}:
if args.command in {"call", "survey", "environment", "import-pawn"}:
service = ArianeService(args.socket, args.db, args.timeout, args.discovery_dir, args.state_dir)
if args.command == "call":
params = json.loads(args.file.read_text() if args.file else args.params)
if not isinstance(params, dict): raise ValueError("parameters must be a JSON object")
payload = service.dispatch(args.method, params)
elif args.command == "survey":
payload = service.vibe.observe(args.output_directory, args.radius, args.grid, args.pixel_x, args.pixel_y)
elif args.command == "import-pawn":
logical = args.logical or f"ariane/{args.source.stem}.ipl"
payload = service.import_pawn_mapping(
str(args.source), logical, str(args.output), group=args.group,
commit=args.commit, save=args.save)
else:
settings = {key: getattr(args, key) for key in ("hour", "minute", "weather_a", "weather_b", "blend")
if getattr(args, key) is not None}
payload = service.environment(**settings)
elif args.command == "assets":
if args.asset_command == "index": payload = build_index(
if args.asset_command == "install-samp":
payload = install_samp_assets(args.gta_dir, args.source, force=args.force)
if not args.no_index:
payload["index"] = build_index(database=args.db, gta_dir=args.gta_dir)
elif args.asset_command == "index": payload = build_index(
args.gtastuff, args.db, semantic_assets=args.semantic_assets, gta_dir=args.gta_dir)
elif args.asset_command == "search":
service = ArianeService(args.socket, args.db, args.timeout,
Expand Down
55 changes: 46 additions & 9 deletions tools/agent/asset_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,19 +170,56 @@ def _load_semantic_assets(path: Path | None) -> dict[int, dict]:
if item.get("Id", item.get("id")) is not None}


def _gta_models(gta_dir: Path) -> list[dict]:
"""Read object definitions locally; no game assets or external catalogue needed.
def _casefold_directory(parent: Path, name: str) -> Path | None:
if not parent.is_dir():
return None
return next((path for path in parent.iterdir()
if path.is_dir() and path.name.lower() == name.lower()), None)


Scan data IDE files in deterministic order. Runtime --defined-only and inspect
remain authoritative for active definitions, collision and dimensions.
def _gta_ide_paths(gta_dir: Path) -> list[Path]:
"""Return base IDEs plus explicit modloader IDE additions.

Mod files are user-owned and remain outside the catalogue. Only IDE paths
declared by a mod's gta.dat-style text manifest are indexed, matching the
engine's addition contract instead of treating every loose IDE as active.
"""
data = next((p for p in gta_dir.iterdir() if p.is_dir() and p.name.lower() == "data"), None)
data = _casefold_directory(gta_dir, "data")
if data is None:
raise FileNotFoundError(f"GTA data directory not found in {gta_dir}")
paths = {path.resolve() for path in data.rglob("*") if path.suffix.lower() == ".ide"}
modloader = _casefold_directory(gta_dir, "modloader")
if modloader is None:
return sorted(paths, key=lambda path: str(path).lower())
for mod in sorted((path for path in modloader.iterdir() if path.is_dir()),
key=lambda path: path.name.lower()):
manifests = (path for path in mod.rglob("*")
if path.is_file() and path.suffix.lower() == ".txt")
for manifest in sorted(manifests, key=lambda path: str(path).lower()):
for raw in manifest.read_text(encoding="latin-1", errors="replace").splitlines():
line = raw.split("#", 1)[0].strip()
if not line.upper().startswith("IDE "):
continue
relative = Path(line[4:].strip().replace("\\", "/"))
candidate = (mod / relative).resolve()
try:
candidate.relative_to(mod.resolve())
except ValueError:
continue
if candidate.is_file() and candidate.suffix.lower() == ".ide":
paths.add(candidate)
return sorted(paths, key=lambda path: str(path).lower())


def _gta_models(gta_dir: Path) -> list[dict]:
"""Read object definitions locally; no game assets or external catalogue needed.

Scan base and declared modloader IDE files in deterministic order. Runtime
--defined-only and inspect remain authoritative for active definitions,
collision and dimensions.
"""
models = {}
for path in sorted(data.rglob("*")):
if path.suffix.lower() != ".ide":
continue
for path in _gta_ide_paths(gta_dir):
section = ""
for line_number, line in enumerate(path.read_text(encoding="latin-1").splitlines(), 1):
line = line.split("#", 1)[0].strip()
Expand Down Expand Up @@ -211,7 +248,7 @@ def _gta_models(gta_dir: Path) -> list[dict]:
except (ValueError, IndexError) as exc:
raise ValueError(f"Invalid object definition at {path}:{line_number}: {exc}") from exc
if not models:
raise ValueError(f"No object definitions found beneath {data}")
raise ValueError(f"No object definitions found beneath {gta_dir}")
return list(models.values())


Expand Down
Loading