bg3-core 1.0.0b1__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.
- bg3_core-1.0.0b1/.gitignore +58 -0
- bg3_core-1.0.0b1/PKG-INFO +72 -0
- bg3_core-1.0.0b1/README.md +65 -0
- bg3_core-1.0.0b1/bg3_core/__init__.py +0 -0
- bg3_core-1.0.0b1/bg3_core/bump_mod_version.py +288 -0
- bg3_core-1.0.0b1/bg3_core/cli.py +2025 -0
- bg3_core-1.0.0b1/bg3_core/cli_common.py +204 -0
- bg3_core-1.0.0b1/bg3_core/env_paths.py +318 -0
- bg3_core-1.0.0b1/bg3_core/larian_parsers.py +379 -0
- bg3_core-1.0.0b1/bg3_core/library_catalog.py +379 -0
- bg3_core-1.0.0b1/bg3_core/lsf.py +443 -0
- bg3_core-1.0.0b1/bg3_core/lspk.py +387 -0
- bg3_core-1.0.0b1/bg3_core/manage_load_order.py +1528 -0
- bg3_core-1.0.0b1/bg3_core/manage_saves.py +648 -0
- bg3_core-1.0.0b1/bg3_core/mod_overrides.py +252 -0
- bg3_core-1.0.0b1/bg3_core/mod_resolver.py +436 -0
- bg3_core-1.0.0b1/bg3_core/modio_client.py +429 -0
- bg3_core-1.0.0b1/bg3_core/modio_create_collection.py +485 -0
- bg3_core-1.0.0b1/bg3_core/modio_extract.py +948 -0
- bg3_core-1.0.0b1/bg3_core/modio_search.py +381 -0
- bg3_core-1.0.0b1/bg3_core/modio_sync.py +472 -0
- bg3_core-1.0.0b1/bg3_core/modio_update.py +408 -0
- bg3_core-1.0.0b1/bg3_core/modio_update_metadata.py +384 -0
- bg3_core-1.0.0b1/bg3_core/purge_library.py +132 -0
- bg3_core-1.0.0b1/bg3_core/query_mod_database.py +507 -0
- bg3_core-1.0.0b1/bg3_core/render_modio_description.py +263 -0
- bg3_core-1.0.0b1/bg3_core/save_inspector.py +478 -0
- bg3_core-1.0.0b1/bg3_core/setup_environment.py +718 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-create-modio-collection/SKILL.md +45 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-document-mods/SKILL.md +50 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-document-mods/references/library_catalog_spec.md +22 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-document-mods/resources/document_subagent_template.json +11 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-document-single-mod/SKILL.md +48 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-document-single-mod/resources/return_message_templates.md +19 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-document-single-mod/resources/single_mod_readme_template.md +27 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-manage-load-order/SKILL.md +47 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-search-mods/SKILL.md +52 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-search-mods/references/search_ranking_rubric.md +27 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-search-mods/resources/conflict_subagent_template.json +18 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-update-mod-documentation/SKILL.md +48 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-update-mod-documentation/references/catalog_sync_flow.md +19 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-update-mod-documentation/resources/update_subagent_template.json +11 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-update-single-mod/SKILL.md +50 -0
- bg3_core-1.0.0b1/bg3_core/skills/bg3-update-single-mod/resources/update_status_messages.md +31 -0
- bg3_core-1.0.0b1/bg3_core/skills_installer.py +263 -0
- bg3_core-1.0.0b1/bg3_core/tools.py +693 -0
- bg3_core-1.0.0b1/bg3_core/update_library_catalog.py +314 -0
- bg3_core-1.0.0b1/pyproject.toml +17 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# Baldur's Gate 3 Mod Studio & Mod.io Toolkit - Git Ignore Rules
|
|
3
|
+
# ==============================================================================
|
|
4
|
+
|
|
5
|
+
# Secrets & Environment Configurations
|
|
6
|
+
.env
|
|
7
|
+
*.env
|
|
8
|
+
.env.*.local
|
|
9
|
+
|
|
10
|
+
# Python runtime & caches
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*$py.class
|
|
14
|
+
*.so
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
dist/
|
|
19
|
+
build/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.eggs/
|
|
22
|
+
.cache/
|
|
23
|
+
|
|
24
|
+
# Virtual Environments
|
|
25
|
+
.venv/
|
|
26
|
+
venv/
|
|
27
|
+
env/
|
|
28
|
+
ENV/
|
|
29
|
+
|
|
30
|
+
# IDE & Editor Files
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
*.swp
|
|
34
|
+
*.swo
|
|
35
|
+
*~
|
|
36
|
+
|
|
37
|
+
# Operating System Files
|
|
38
|
+
.DS_Store
|
|
39
|
+
Thumbs.db
|
|
40
|
+
desktop.ini
|
|
41
|
+
|
|
42
|
+
# Build artifacts, logs & archives
|
|
43
|
+
*.log
|
|
44
|
+
*.tmp
|
|
45
|
+
*.bak
|
|
46
|
+
*.pak
|
|
47
|
+
*.loca
|
|
48
|
+
*.zip
|
|
49
|
+
|
|
50
|
+
# Scratch / temporary experimental folders
|
|
51
|
+
scratch/
|
|
52
|
+
|
|
53
|
+
# Externalized User Content (Ignore any local studio folders or accidental spills)
|
|
54
|
+
build_guides/
|
|
55
|
+
mods/
|
|
56
|
+
my_mods/
|
|
57
|
+
BG3_Mod_Studio/
|
|
58
|
+
.agents/ledgers/artifacts-ledger.md
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bg3-core
|
|
3
|
+
Version: 1.0.0b1
|
|
4
|
+
Summary: Baldur's Gate 3 Mod Studio Core Runtime, Parsers, and API Client
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# bg3-core
|
|
9
|
+
|
|
10
|
+
Foundational runtime, binary parsers, cross-platform path resolution, and mod.io REST API client for the **Baldur's Gate 3 Mod Studio** workspace.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Domain Architecture & Scope
|
|
15
|
+
|
|
16
|
+
`bg3-core` serves as the foundational, leaf-level dependency package in the monorepo DAG. It is designed to be completely independent of higher-level domain packages (`bg3-authoring` and `bg3-theorycraft`), providing low-level binary engines and runtime utilities that require zero external binary tools (such as LSLib's `divine.exe`, Wine, or .NET runtimes).
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
graph TD
|
|
20
|
+
Authoring["bg3-authoring"] --> Core["bg3-core"]
|
|
21
|
+
Theorycraft["bg3-theorycraft"] --> Core["bg3-core"]
|
|
22
|
+
CLI["bg3 CLI Router"] --> Core
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Core Modules & Exported Interfaces
|
|
28
|
+
|
|
29
|
+
| Module | Core Responsibility | Key Exported Symbols |
|
|
30
|
+
| :--- | :--- | :--- |
|
|
31
|
+
| `bg3_core.larian_parsers` | Larian binary formats, version encoding, and localization | `encode_version64()`, `decode_version64()`, `parse_loca_bytes()`, `compile_xml_bytes_to_loca()`, `split_larian_items()` |
|
|
32
|
+
| `bg3_core.lspk` | Pure-Python LSPK v18 archive reader, builder, and unpacker | `LspkArchive`, `build_lspk_archive()`, `lz4_block_decompress()`, `lz4_frame_or_block_decompress()`, `decompress_pak_file_data()` |
|
|
33
|
+
| `bg3_core.save_inspector` | Save game metadata extraction, AST audit, and mod pruning | `extract_save_metadata()`, `audit_against_modsettings()`, `prune_missing_mods_from_save()` |
|
|
34
|
+
| `bg3_core.manage_saves` | Savegame discovery, backup, retention purging, and rescue | `discover_save_games()`, `handle_clean_missing()` |
|
|
35
|
+
| `bg3_core.env_paths` | Cross-platform state, cache, config, and game path detection | `get_xdg_config_home()`, `get_xdg_cache_home()`, `get_xdg_data_home()`, `detect_bg3_appdata_dirs()`, `detect_bg3_install_dir()` |
|
|
36
|
+
| `bg3_core.cli_common` | Stream separation, POSIX exit codes, and JSON envelopes | `OutputEmitter`, `ConsoleColor`, `ExitCode`, `format_json_response()`, `confirm_action()` |
|
|
37
|
+
| `bg3_core.modio_client` | Authenticated mod.io v1 REST client | `ModioClient`, `ModioError`, `resolve_modio_token()` |
|
|
38
|
+
| `bg3_core.tools` | Dynamic CLI argument parser introspection and registry | `build_parser_ast_registry()`, `search_tools()`, `verify_registry()`, `audit_skills()` |
|
|
39
|
+
| `bg3_core.cli` | Central unified router for all workspace domains | `main()`, `build_parser()`, `dispatch_command()` |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## CLI Console Entrypoint
|
|
44
|
+
|
|
45
|
+
This package registers the primary console script for the entire studio:
|
|
46
|
+
|
|
47
|
+
```toml
|
|
48
|
+
[project.scripts]
|
|
49
|
+
bg3 = "bg3_core.cli:main"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Common Commands Driven by `bg3-core`:
|
|
53
|
+
- `bg3 tools list` — Discover all registered tools across packages.
|
|
54
|
+
- `bg3 tools search <query>` — Search tools by semantic intent.
|
|
55
|
+
- `bg3 tools info <tool>` — Inspect tool arguments and schemas.
|
|
56
|
+
- `bg3 tools verify --audit-skills` — Programmatic health audit of agent skills.
|
|
57
|
+
- `bg3 modio search <query>` — Search remote community mods on mod.io.
|
|
58
|
+
- `bg3 modio sync` — Synchronize subscribed mods and download `.pak` packages.
|
|
59
|
+
- `bg3 loadorder list` — Inspect active `modsettings.lsx` load order.
|
|
60
|
+
- `bg3 save inspect latest` — Inspect save metadata and audit mod requirements.
|
|
61
|
+
- `bg3 save clean-missing` — Prune missing mods to rescue corrupted saves.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Cross-Platform Storage Architecture
|
|
66
|
+
|
|
67
|
+
All state directories comply strictly with the XDG Base Directory specification on Linux/macOS and APPDATA specifications on Windows:
|
|
68
|
+
|
|
69
|
+
- **Configuration**: `$XDG_CONFIG_HOME/bg3/env` (or `%APPDATA%\bg3\env`)
|
|
70
|
+
- **Cache**: `$XDG_CACHE_HOME/bg3/` (or `%LOCALAPPDATA%\bg3\`)
|
|
71
|
+
- **User Content**: `$XDG_DOCUMENTS_DIR/BG3_Mod_Studio/Mods/`
|
|
72
|
+
- **Game VFS**: Automatically detects Steam Native, Proton (`compatdata/1086940/pfx`), WSL, and Windows installations.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# bg3-core
|
|
2
|
+
|
|
3
|
+
Foundational runtime, binary parsers, cross-platform path resolution, and mod.io REST API client for the **Baldur's Gate 3 Mod Studio** workspace.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Domain Architecture & Scope
|
|
8
|
+
|
|
9
|
+
`bg3-core` serves as the foundational, leaf-level dependency package in the monorepo DAG. It is designed to be completely independent of higher-level domain packages (`bg3-authoring` and `bg3-theorycraft`), providing low-level binary engines and runtime utilities that require zero external binary tools (such as LSLib's `divine.exe`, Wine, or .NET runtimes).
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
graph TD
|
|
13
|
+
Authoring["bg3-authoring"] --> Core["bg3-core"]
|
|
14
|
+
Theorycraft["bg3-theorycraft"] --> Core["bg3-core"]
|
|
15
|
+
CLI["bg3 CLI Router"] --> Core
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Core Modules & Exported Interfaces
|
|
21
|
+
|
|
22
|
+
| Module | Core Responsibility | Key Exported Symbols |
|
|
23
|
+
| :--- | :--- | :--- |
|
|
24
|
+
| `bg3_core.larian_parsers` | Larian binary formats, version encoding, and localization | `encode_version64()`, `decode_version64()`, `parse_loca_bytes()`, `compile_xml_bytes_to_loca()`, `split_larian_items()` |
|
|
25
|
+
| `bg3_core.lspk` | Pure-Python LSPK v18 archive reader, builder, and unpacker | `LspkArchive`, `build_lspk_archive()`, `lz4_block_decompress()`, `lz4_frame_or_block_decompress()`, `decompress_pak_file_data()` |
|
|
26
|
+
| `bg3_core.save_inspector` | Save game metadata extraction, AST audit, and mod pruning | `extract_save_metadata()`, `audit_against_modsettings()`, `prune_missing_mods_from_save()` |
|
|
27
|
+
| `bg3_core.manage_saves` | Savegame discovery, backup, retention purging, and rescue | `discover_save_games()`, `handle_clean_missing()` |
|
|
28
|
+
| `bg3_core.env_paths` | Cross-platform state, cache, config, and game path detection | `get_xdg_config_home()`, `get_xdg_cache_home()`, `get_xdg_data_home()`, `detect_bg3_appdata_dirs()`, `detect_bg3_install_dir()` |
|
|
29
|
+
| `bg3_core.cli_common` | Stream separation, POSIX exit codes, and JSON envelopes | `OutputEmitter`, `ConsoleColor`, `ExitCode`, `format_json_response()`, `confirm_action()` |
|
|
30
|
+
| `bg3_core.modio_client` | Authenticated mod.io v1 REST client | `ModioClient`, `ModioError`, `resolve_modio_token()` |
|
|
31
|
+
| `bg3_core.tools` | Dynamic CLI argument parser introspection and registry | `build_parser_ast_registry()`, `search_tools()`, `verify_registry()`, `audit_skills()` |
|
|
32
|
+
| `bg3_core.cli` | Central unified router for all workspace domains | `main()`, `build_parser()`, `dispatch_command()` |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## CLI Console Entrypoint
|
|
37
|
+
|
|
38
|
+
This package registers the primary console script for the entire studio:
|
|
39
|
+
|
|
40
|
+
```toml
|
|
41
|
+
[project.scripts]
|
|
42
|
+
bg3 = "bg3_core.cli:main"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Common Commands Driven by `bg3-core`:
|
|
46
|
+
- `bg3 tools list` — Discover all registered tools across packages.
|
|
47
|
+
- `bg3 tools search <query>` — Search tools by semantic intent.
|
|
48
|
+
- `bg3 tools info <tool>` — Inspect tool arguments and schemas.
|
|
49
|
+
- `bg3 tools verify --audit-skills` — Programmatic health audit of agent skills.
|
|
50
|
+
- `bg3 modio search <query>` — Search remote community mods on mod.io.
|
|
51
|
+
- `bg3 modio sync` — Synchronize subscribed mods and download `.pak` packages.
|
|
52
|
+
- `bg3 loadorder list` — Inspect active `modsettings.lsx` load order.
|
|
53
|
+
- `bg3 save inspect latest` — Inspect save metadata and audit mod requirements.
|
|
54
|
+
- `bg3 save clean-missing` — Prune missing mods to rescue corrupted saves.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Cross-Platform Storage Architecture
|
|
59
|
+
|
|
60
|
+
All state directories comply strictly with the XDG Base Directory specification on Linux/macOS and APPDATA specifications on Windows:
|
|
61
|
+
|
|
62
|
+
- **Configuration**: `$XDG_CONFIG_HOME/bg3/env` (or `%APPDATA%\bg3\env`)
|
|
63
|
+
- **Cache**: `$XDG_CACHE_HOME/bg3/` (or `%LOCALAPPDATA%\bg3\`)
|
|
64
|
+
- **User Content**: `$XDG_DOCUMENTS_DIR/BG3_Mod_Studio/Mods/`
|
|
65
|
+
- **Game VFS**: Automatically detects Steam Native, Proton (`compatdata/1086940/pfx`), WSL, and Windows installations.
|
|
File without changes
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
bump_mod_version.py
|
|
4
|
+
-------------------
|
|
5
|
+
Inspects and updates semantic version numbers and Larian Version64 integers
|
|
6
|
+
atomically across `Mods/<UUID>/meta.lsx` (and legacy `Projects/<UUID>/meta.lsx`),
|
|
7
|
+
and keeps `my_mods/<slug>/README.md` in sync so documentation and metadata never drift.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
- Encodes and decodes 64-bit integer versions: (major << 55) | (minor << 47) | (revision << 31) | build
|
|
11
|
+
- Supports semantic increments: --build, --patch, --minor, --major, or explicit --set
|
|
12
|
+
- Atomic update across meta.lsx and README.md
|
|
13
|
+
- Clean regex-based replacement preserving exact XML and Markdown formatting
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import List, Tuple
|
|
22
|
+
|
|
23
|
+
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
24
|
+
if str(_REPO_ROOT) not in sys.path:
|
|
25
|
+
sys.path.insert(0, str(_REPO_ROOT))
|
|
26
|
+
|
|
27
|
+
from bg3_core.env_paths import MY_MODS_DIR, REPO_ROOT, resolve_mod_dir
|
|
28
|
+
from bg3_core.larian_parsers import (
|
|
29
|
+
calculate_version64,
|
|
30
|
+
decode_version64,
|
|
31
|
+
format_version_tuple,
|
|
32
|
+
parse_version_str,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
WORKSPACE_ROOT = REPO_ROOT
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_mod_directories(mod_query: str = "") -> List[Path]:
|
|
39
|
+
"""Find mod directories in my_mods matching query or return all."""
|
|
40
|
+
if not mod_query:
|
|
41
|
+
if not MY_MODS_DIR.exists():
|
|
42
|
+
return []
|
|
43
|
+
return sorted([d for d in MY_MODS_DIR.iterdir() if d.is_dir()])
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
target = resolve_mod_dir(mod_query)
|
|
47
|
+
if target and target.is_dir():
|
|
48
|
+
return [target]
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
if not MY_MODS_DIR.exists():
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
query_norm = mod_query.strip().lower().replace("-", "").replace("_", "")
|
|
56
|
+
matches = []
|
|
57
|
+
for d in MY_MODS_DIR.iterdir():
|
|
58
|
+
if d.is_dir():
|
|
59
|
+
name_norm = d.name.lower().replace("-", "").replace("_", "")
|
|
60
|
+
if query_norm in name_norm:
|
|
61
|
+
matches.append(d)
|
|
62
|
+
return sorted(matches)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_meta_files(mod_dir: Path) -> List[Path]:
|
|
66
|
+
"""Find meta.lsx files in Mods/ (and legacy Projects/ with ModuleInfo) to update Version64."""
|
|
67
|
+
files = []
|
|
68
|
+
# Always include runtime module descriptors in Mods/
|
|
69
|
+
mods_dir = mod_dir / "Mods"
|
|
70
|
+
if mods_dir.exists():
|
|
71
|
+
for p in mods_dir.glob("*/meta.lsx"):
|
|
72
|
+
files.append(p)
|
|
73
|
+
|
|
74
|
+
# Only include Projects/ if it contains ModuleInfo (legacy scaffold)
|
|
75
|
+
proj_dir = mod_dir / "Projects"
|
|
76
|
+
if proj_dir.exists():
|
|
77
|
+
for p in proj_dir.glob("*/meta.lsx"):
|
|
78
|
+
try:
|
|
79
|
+
if '<node id="ModuleInfo">' in p.read_text(encoding="utf-8"):
|
|
80
|
+
files.append(p)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
return files
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def read_mod_version(meta_path: Path) -> Tuple[int, int, int, int, int]:
|
|
87
|
+
"""Extract Version64 integer and decoded tuple from a meta.lsx file."""
|
|
88
|
+
content = meta_path.read_text(encoding="utf-8")
|
|
89
|
+
|
|
90
|
+
# Locate ModuleInfo node first to avoid matching dependencies or PublishVersion
|
|
91
|
+
mod_info_match = re.search(r'<node\s+id="ModuleInfo">(.*?)</node>', content, re.DOTALL)
|
|
92
|
+
if mod_info_match:
|
|
93
|
+
target_text = mod_info_match.group(1)
|
|
94
|
+
else:
|
|
95
|
+
target_text = content
|
|
96
|
+
|
|
97
|
+
ver_match = re.search(r'<attribute\s+id="Version64"\s+type="int64"\s+value="(\d+)"\s*/>', target_text)
|
|
98
|
+
if not ver_match:
|
|
99
|
+
raise ValueError(f"Could not find Version64 attribute under ModuleInfo in {meta_path}")
|
|
100
|
+
|
|
101
|
+
raw_val = int(ver_match.group(1))
|
|
102
|
+
major, minor, rev, bld = decode_version64(raw_val)
|
|
103
|
+
return major, minor, rev, bld, raw_val
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def update_meta_version(meta_path: Path, new_v64: int, dry_run: bool = False) -> bool:
|
|
107
|
+
"""Updates Version64 under ModuleInfo in meta.lsx while preserving formatting."""
|
|
108
|
+
content = meta_path.read_text(encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
def replacer(match: re.Match[str]) -> str:
|
|
111
|
+
node_content = match.group(0)
|
|
112
|
+
updated_node = re.sub(
|
|
113
|
+
r'(<attribute\s+id="Version64"\s+type="int64"\s+value=")\d+("\s*/>)',
|
|
114
|
+
rf"\g<1>{new_v64}\g<2>",
|
|
115
|
+
node_content,
|
|
116
|
+
count=1,
|
|
117
|
+
)
|
|
118
|
+
return updated_node
|
|
119
|
+
|
|
120
|
+
new_content, count = re.subn(
|
|
121
|
+
r'<node\s+id="ModuleInfo">.*?</node>',
|
|
122
|
+
replacer,
|
|
123
|
+
content,
|
|
124
|
+
count=1,
|
|
125
|
+
flags=re.DOTALL,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if count == 0:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
if not dry_run:
|
|
132
|
+
meta_path.write_text(new_content, encoding="utf-8")
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def update_readme_version(mod_dir: Path, new_version_str: str, dry_run: bool = False) -> bool:
|
|
137
|
+
"""Update version line in README.md if present to avoid documentation drift."""
|
|
138
|
+
readme_path = mod_dir / "README.md"
|
|
139
|
+
if not readme_path.is_file():
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
content = readme_path.read_text(encoding="utf-8")
|
|
143
|
+
pattern = r'(?i)(-\s*\*\*Version:\*\*\s*[`"]?|\*{0,2}Version:?\*{0,2}\s*[:=]?\s*[`"]?)(\d+(?:\.\d+)*)([`"]?)'
|
|
144
|
+
new_content, count = re.subn(
|
|
145
|
+
pattern,
|
|
146
|
+
rf"\g<1>{new_version_str}\g<3>",
|
|
147
|
+
content,
|
|
148
|
+
count=1,
|
|
149
|
+
)
|
|
150
|
+
if count == 0 or new_content == content:
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
if not dry_run:
|
|
154
|
+
readme_path.write_text(new_content, encoding="utf-8")
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def bump_version(
|
|
159
|
+
mod_name: str,
|
|
160
|
+
action: str = "show",
|
|
161
|
+
set_value: str = "",
|
|
162
|
+
dry_run: bool = False,
|
|
163
|
+
as_json: bool = False,
|
|
164
|
+
) -> dict:
|
|
165
|
+
matched = find_mod_directories(mod_name)
|
|
166
|
+
if not matched:
|
|
167
|
+
raise ValueError(f"No mod found in my_mods/ matching query '{mod_name}'.")
|
|
168
|
+
|
|
169
|
+
target_mod = matched[0]
|
|
170
|
+
meta_files = get_meta_files(target_mod)
|
|
171
|
+
if not meta_files:
|
|
172
|
+
raise ValueError(f"No meta.lsx files found in {target_mod} under Projects/ or Mods/.")
|
|
173
|
+
|
|
174
|
+
# Read current from first file
|
|
175
|
+
major, minor, rev, bld, raw_val = read_mod_version(meta_files[0])
|
|
176
|
+
old_version_str = format_version_tuple((major, minor, rev, bld))
|
|
177
|
+
|
|
178
|
+
new_major, new_minor, new_rev, new_bld = major, minor, rev, bld
|
|
179
|
+
|
|
180
|
+
if action == "build":
|
|
181
|
+
new_bld += 1
|
|
182
|
+
elif action == "patch":
|
|
183
|
+
new_rev += 1
|
|
184
|
+
new_bld = 0
|
|
185
|
+
elif action == "minor":
|
|
186
|
+
new_minor += 1
|
|
187
|
+
new_rev = 0
|
|
188
|
+
new_bld = 0
|
|
189
|
+
elif action == "major":
|
|
190
|
+
new_major += 1
|
|
191
|
+
new_minor = 0
|
|
192
|
+
new_rev = 0
|
|
193
|
+
new_bld = 0
|
|
194
|
+
elif action == "set":
|
|
195
|
+
new_major, new_minor, new_rev, new_bld = parse_version_str(set_value)
|
|
196
|
+
|
|
197
|
+
new_v64 = calculate_version64(new_major, new_minor, new_rev, new_bld)
|
|
198
|
+
new_version_str = format_version_tuple((new_major, new_minor, new_rev, new_bld))
|
|
199
|
+
|
|
200
|
+
def _rel_path(p: Path, base: Path) -> str:
|
|
201
|
+
try:
|
|
202
|
+
return str(p.relative_to(base))
|
|
203
|
+
except ValueError:
|
|
204
|
+
return str(p)
|
|
205
|
+
|
|
206
|
+
updated_files = []
|
|
207
|
+
if action != "show":
|
|
208
|
+
for mf in meta_files:
|
|
209
|
+
if update_meta_version(mf, new_v64, dry_run=dry_run):
|
|
210
|
+
updated_files.append(_rel_path(mf, WORKSPACE_ROOT))
|
|
211
|
+
if update_readme_version(target_mod, new_version_str, dry_run=dry_run):
|
|
212
|
+
updated_files.append(_rel_path(target_mod / "README.md", WORKSPACE_ROOT))
|
|
213
|
+
|
|
214
|
+
result = {
|
|
215
|
+
"mod_name": target_mod.name,
|
|
216
|
+
"mod_dir": _rel_path(target_mod, WORKSPACE_ROOT),
|
|
217
|
+
"action": action,
|
|
218
|
+
"previous_version": old_version_str,
|
|
219
|
+
"previous_version64": raw_val,
|
|
220
|
+
"new_version": new_version_str,
|
|
221
|
+
"new_version64": new_v64,
|
|
222
|
+
"updated_files": updated_files,
|
|
223
|
+
"dry_run": dry_run,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if as_json:
|
|
227
|
+
print(json.dumps(result, indent=2))
|
|
228
|
+
else:
|
|
229
|
+
if action == "show":
|
|
230
|
+
print(f"Mod '{target_mod.name}' version: {old_version_str} (Version64: {raw_val})")
|
|
231
|
+
for mf in meta_files:
|
|
232
|
+
print(f" └ {_rel_path(mf, WORKSPACE_ROOT)}")
|
|
233
|
+
else:
|
|
234
|
+
action_desc = "[DRY-RUN] Would update" if dry_run else "Successfully updated"
|
|
235
|
+
print(f"{action_desc} '{target_mod.name}': {old_version_str} -> {new_version_str} (Version64: {new_v64})")
|
|
236
|
+
for f in updated_files:
|
|
237
|
+
print(f" ✓ Updated {f}")
|
|
238
|
+
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def main() -> None:
|
|
243
|
+
parser = argparse.ArgumentParser(
|
|
244
|
+
description="Auto-generate and bump semantic versions and Larian Version64 integers in BG3 mods."
|
|
245
|
+
)
|
|
246
|
+
parser.add_argument("--mod", required=True, help="Mod name or folder in my_mods/ (e.g. paladins-call)")
|
|
247
|
+
group = parser.add_mutually_exclusive_group()
|
|
248
|
+
group.add_argument("--show", action="store_true", help="Display current version without modifying")
|
|
249
|
+
group.add_argument("--build", "-b", action="store_true", help="Increment build number (+1)")
|
|
250
|
+
group.add_argument("--patch", "-p", action="store_true", help="Increment patch/revision (+1) and reset build")
|
|
251
|
+
group.add_argument("--minor", "-m", action="store_true", help="Increment minor version (+1) and reset patch/build")
|
|
252
|
+
group.add_argument("--major", action="store_true", help="Increment major version (+1) and reset minor/patch/build")
|
|
253
|
+
group.add_argument("--set", type=str, help="Explicitly set version (e.g. 1.2.0.0)")
|
|
254
|
+
|
|
255
|
+
parser.add_argument("--dry-run", action="store_true", help="Preview version changes without writing to disk")
|
|
256
|
+
parser.add_argument("--json", action="store_true", help="Output result as JSON")
|
|
257
|
+
|
|
258
|
+
args = parser.parse_args()
|
|
259
|
+
|
|
260
|
+
action = "show"
|
|
261
|
+
set_val = ""
|
|
262
|
+
if args.build:
|
|
263
|
+
action = "build"
|
|
264
|
+
elif args.patch:
|
|
265
|
+
action = "patch"
|
|
266
|
+
elif args.minor:
|
|
267
|
+
action = "minor"
|
|
268
|
+
elif args.major:
|
|
269
|
+
action = "major"
|
|
270
|
+
elif args.set:
|
|
271
|
+
action = "set"
|
|
272
|
+
set_val = args.set
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
bump_version(
|
|
276
|
+
mod_name=args.mod,
|
|
277
|
+
action=action,
|
|
278
|
+
set_value=set_val,
|
|
279
|
+
dry_run=args.dry_run,
|
|
280
|
+
as_json=args.json,
|
|
281
|
+
)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
284
|
+
sys.exit(1)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
main()
|