438b1f7c65
Adds removal-detection: when --previous-manifest <path> is given, the converter diffs the previous publish against current packwiz state and emits modify[].type=remove entries for mods/resourcepacks/etc that disappeared, using simple-mod-sync's on-disk naming convention as the regex pattern. Reverse-engineered from upstream source: - simple-mod-sync writes <sanitized_name>-<sanitized_version>.<ext> - StringUtils.sanitize strips [^a-zA-Z0-9.\-_] - GetOlderVersion() finds files starting with <name>- and auto-deletes on version bumps. So version upgrades need no converter handling; only full removals do. 8 new tests including end-to-end CLI verification with a synthetic previous manifest. 23/23 pass.
99 lines
4.3 KiB
Markdown
99 lines
4.3 KiB
Markdown
# packwiz-to-sms
|
|
|
|
Convert a [packwiz](https://github.com/packwiz/packwiz) pack to a [simple-mod-sync](https://github.com/oxydien/simple-mod-sync) manifest (`sync_version: 3`).
|
|
|
|
## Why
|
|
|
|
Use **packwiz** to author the modpack (Modrinth/CurseForge integration, git-friendly TOML, optional/side-aware mods, `.mrpack` export for free), and **simple-mod-sync** for delivery to clients that can't or won't use Prism/MMC pre-launch hooks (vanilla launcher, TLauncher, cracked players).
|
|
|
|
One source of truth, two distribution channels — `.mrpack` export and simple-mod-sync manifest are both produced from the same packwiz repo.
|
|
|
|
## Install
|
|
|
|
Requires Python 3.11+ (uses `tomllib`). Zero runtime deps.
|
|
|
|
```bash
|
|
git clone https://git.timemachine.center/Timemachine/packwiz-to-sms.git
|
|
cd packwiz-to-sms
|
|
python3 packwiz_to_sms.py --help
|
|
```
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
# Minimal — emit manifest to stdout
|
|
python3 packwiz_to_sms.py /path/to/packwiz/pack
|
|
|
|
# Write to file
|
|
python3 packwiz_to_sms.py /path/to/packwiz/pack -o manifest.json
|
|
|
|
# Bundle non-mod files (config/, options.txt, servers.dat) into a zip
|
|
# and add a 'packed' entry pointing at where you'll host it
|
|
python3 packwiz_to_sms.py /path/to/packwiz/pack \
|
|
-o manifest.json \
|
|
--bundle-non-mods overrides.zip \
|
|
--bundle-url https://packs.example.com/overrides.zip
|
|
|
|
# Generate removal entries for mods dropped since the previous publish
|
|
python3 packwiz_to_sms.py /path/to/packwiz/pack \
|
|
-o manifest.json \
|
|
--previous-manifest /path/to/last-published-manifest.json
|
|
```
|
|
|
|
## Mod removal
|
|
|
|
Two cases, handled differently:
|
|
|
|
| What changed | Who handles cleanup |
|
|
|---|---|
|
|
| **Mod version bump** (Sodium 0.5 → 0.6) | simple-mod-sync itself — it writes downloaded files as `<sanitized_name>-<sanitized_version>.<ext>` and on update looks for any prior file starting with `<name>-` and deletes it. No converter intervention needed. |
|
|
| **Mod removed entirely** (no longer in pack) | Converter emits an explicit `modify[].type=remove` entry with a regex matching simple-mod-sync's on-disk naming. Triggered by `--previous-manifest <path>` flag. |
|
|
|
|
Without `--previous-manifest`, removed mods stay on player disk forever. In CI, keep the last-published manifest and feed it in as the previous one on every run.
|
|
|
|
The regex follows simple-mod-sync's `StringUtils.sanitize()` rules: `[^a-zA-Z0-9.\-_]` characters are stripped. So `"Fabric API (Old)"` → pattern `^mods/FabricAPIOld-.*\.jar$`.
|
|
|
|
## What gets emitted
|
|
|
|
| Packwiz path | Becomes simple-mod-sync `type` |
|
|
|---|---|
|
|
| `mods/*.pw.toml` (side=client or both) | `mod` |
|
|
| `mods/*.pw.toml` (side=server) | dropped |
|
|
| `resourcepacks/*.pw.toml` | `resourcepack` |
|
|
| `shaderpacks/*.pw.toml` | `shader` |
|
|
| `**/datapacks/*.pw.toml` | `datapack` |
|
|
| Any non-metafile (e.g. `options.txt`, `config/*`) | bundled into zip via `--bundle-non-mods`, emitted as one `packed` entry pointing at `directory: "."` |
|
|
|
|
CurseForge mods that use `mode = "metadata:curseforge"` (no direct URL) are skipped with a warning. Either switch to Modrinth equivalents or run `packwiz cf reexport` first to inline resolved URLs.
|
|
|
|
## What's not handled
|
|
|
|
- **Optional mods** — simple-mod-sync has no per-client toggle UI. All non-server mods are emitted unconditionally. Ship two manifests (with/without optional) if you need this.
|
|
- **Rename / regex transforms** — packwiz has no equivalent concept, so we don't generate `modify.rename` entries.
|
|
|
|
## How it works
|
|
|
|
1. Reads `pack.toml` + `index.toml`.
|
|
2. For each entry marked `metafile = true`: reads the `.pw.toml`, pulls `download.url`, picks the simple-mod-sync `type` from the parent directory.
|
|
3. Drops `side = "server"`.
|
|
4. Drops entries where `download.url` is missing (CF metadata mode).
|
|
5. Optionally zips non-metafile files into a single archive for `packed` distribution.
|
|
|
|
Output schema matches `sync_version: 3` exactly (see [simple-mod-sync DOCS.md](https://github.com/oxydien/simple-mod-sync/blob/main/DOCS.md)).
|
|
|
|
## Tests
|
|
|
|
```bash
|
|
python3 -m pytest tests/
|
|
```
|
|
|
|
Covers conversion logic + CLI + bundle pipeline + a network integration test that converts the official [`packwiz-example-pack`](https://github.com/packwiz/packwiz-example-pack) (auto-skipped if offline).
|
|
|
|
## Upstream contribution
|
|
|
|
This tool fits the model used by other [simple-mod-sync translators](https://github.com/oxydien/simple-mod-sync/tree/main/translators). It can be PR'd upstream as `translators/packwiz.py`.
|
|
|
|
## License
|
|
|
|
MIT.
|