Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0506aa58ed | |||
| 91def20e61 | |||
| 339b126a50 | |||
| 05e47e1499 | |||
| 4ee661d349 | |||
| cdf280acbf | |||
| bf70965548 | |||
| 3783abaa3d | |||
| 01c3333cb2 | |||
| 72d79af391 |
@@ -0,0 +1,20 @@
|
|||||||
|
name: Validate Repo
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
push:
|
||||||
|
branches: [main, dev]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate repo structure
|
||||||
|
run: python3 validate.py
|
||||||
|
|
||||||
|
- name: Validate repo structure + check URLs
|
||||||
|
run: python3 validate.py --check-urls
|
||||||
|
continue-on-error: true # URL checks are best-effort
|
||||||
@@ -1,75 +1,94 @@
|
|||||||
# Wii U Mod Store: Repository Template
|
# CupStore Repo Template
|
||||||
|
|
||||||
This is the official template for hosting mods on the **Wii U Mod Store** app.
|
This is the official template for hosting mods on [CupStore](https://github.com/timkicker/cupstore) - a mod manager for the Wii U.
|
||||||
|
|
||||||
## How to use
|
## Quick Start
|
||||||
|
|
||||||
1. Fork this repository
|
1. Fork this repository
|
||||||
2. Edit `repo.json` with your repo name and author
|
2. Edit `repo.json` (name, author)
|
||||||
3. Add your games under `games/[game-id]/game.json`
|
3. Add your game folders under `games/`
|
||||||
4. Host your mod ZIPs anywhere (GitHub Releases, direct links, etc.)
|
4. Add your mod ZIPs as GitHub Release assets
|
||||||
5. Add your raw `repo.json` URL to the Mod Store app
|
5. Share your `repo.json` URL with users
|
||||||
|
|
||||||
## Repository URL format
|
## Structure
|
||||||
|
|
||||||
After forking, your repo index URL will look like:
|
|
||||||
```
|
```
|
||||||
https://raw.githubusercontent.com/YOUR-USERNAME/YOUR-REPO/main/repo.json
|
repo.json ← repo index
|
||||||
|
games/
|
||||||
|
mario-kart-8/
|
||||||
|
game.json ← game + mod definitions
|
||||||
|
your-game/
|
||||||
|
game.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Add this to `sd:/wiiu/apps/modstore/config.json` on your Wii U SD card:
|
## repo.json
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"repos": [
|
"formatVersion": 1,
|
||||||
"https://raw.githubusercontent.com/YOUR-USERNAME/YOUR-REPO/main/repo.json"
|
"name": "My Mod Repo",
|
||||||
|
"description": "...",
|
||||||
|
"author": "your-name",
|
||||||
|
"version": "1",
|
||||||
|
"games": [
|
||||||
|
{ "path": "games/mario-kart-8/game.json" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Folder structure
|
## game.json
|
||||||
|
|
||||||
```
|
```json
|
||||||
repo/
|
{
|
||||||
├── repo.json ← Repo index (required)
|
"name": "Mario Kart 8",
|
||||||
└── games/
|
"icon": "https://...",
|
||||||
└── [game-id]/
|
"titleIds": [
|
||||||
├── game.json ← Game info + mod list
|
"000500001010EB00",
|
||||||
└── assets/
|
"000500001010EC00",
|
||||||
└── cover.png ← Game cover image (optional)
|
"000500001010ED00"
|
||||||
|
],
|
||||||
|
"mods": [ ... ]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mod types
|
## Mod Fields
|
||||||
|
|
||||||
- `mod`: A single standalone mod
|
|
||||||
- `modpack`: A bundle of multiple mods installed together
|
|
||||||
|
|
||||||
## game.json fields
|
|
||||||
|
|
||||||
| Field | Required | Description |
|
| Field | Required | Description |
|
||||||
|-------|----------|-------------|
|
|-------|----------|-------------|
|
||||||
| `name` | yes | Display name of the game |
|
| `id` | ✅ | Unique ID, only `a-z 0-9 - _` |
|
||||||
| `cover` | no | Relative path to cover image |
|
| `name` | ✅ | Display name |
|
||||||
| `title_ids` | yes | Array of Wii U Title IDs (all regions) |
|
| `author` | ✅ | Author name |
|
||||||
| `mods` | yes | Array of mod objects |
|
| `version` | ✅ | Version string e.g. `1.0.0` |
|
||||||
|
| `description` | ✅ | Short description |
|
||||||
|
| `download` | ✅ | Direct URL to `.zip` file |
|
||||||
|
| `type` | ✅ | `"mod"` or `"modpack"` |
|
||||||
|
| `thumbnail` | ☑️ | Preview image URL (400x225) |
|
||||||
|
| `screenshots` | ☑️ | List of screenshot URLs (800x450) |
|
||||||
|
| `includes` | ☑️ | For modpacks: list of mod IDs |
|
||||||
|
| `releaseDate` | ☑️ | e.g. `"2024-03-15"` |
|
||||||
|
| `license` | ☑️ | e.g. `"CC BY-NC"` |
|
||||||
|
| `tags` | ☑️ | e.g. `["skin", "music", "course"]` |
|
||||||
|
| `fileSize` | ☑️ | ZIP size in bytes |
|
||||||
|
| `requirements` | ☑️ | List of required mods/patches |
|
||||||
|
| `changelog` | ☑️ | Free text changelog |
|
||||||
|
|
||||||
## Mod fields
|
## Validation
|
||||||
|
|
||||||
| Field | Required | Description |
|
```bash
|
||||||
|-------|----------|-------------|
|
python3 validate.py # check structure
|
||||||
| `id` | yes | Unique identifier (no spaces) |
|
python3 validate.py --check-urls # also verify download URLs
|
||||||
| `name` | yes | Display name |
|
```
|
||||||
| `type` | yes | `mod` or `modpack` |
|
|
||||||
| `version` | yes | Semantic version string |
|
|
||||||
| `author` | yes | Author name |
|
|
||||||
| `description` | yes | Description text |
|
|
||||||
| `thumbnail` | no | URL to thumbnail image (400x225 recommended) |
|
|
||||||
| `screenshots` | no | Array of screenshot URLs |
|
|
||||||
| `download` | yes | Direct URL to ZIP file |
|
|
||||||
| `modpack_path` | yes | SDCafiine path (`content/` or `aoc/`) |
|
|
||||||
| `includes` | modpack only | Array of mod IDs included in bundle |
|
|
||||||
|
|
||||||
## Notes
|
Validation runs automatically on every PR via GitHub Actions.
|
||||||
|
|
||||||
- Title IDs can be found at [wiiubrew.org/wiki/Title_database](https://wiiubrew.org/wiki/Title_database)
|
## Mod ZIP Structure
|
||||||
- Mod ZIPs must match the SDCafiine folder structure (`content/` at root of ZIP)
|
|
||||||
- The app does **not** verify mod safety! only distribute mods you trust
|
Your ZIP must extract to a folder matching the SDCafiine layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
your-mod.zip
|
||||||
|
└── content/
|
||||||
|
└── ...game files...
|
||||||
|
```
|
||||||
|
|
||||||
|
CupStore installs to: `SD:/wiiu/sdcafiine/<titleId>/<modId>/`
|
||||||
|
|||||||
|
After Width: | Height: | Size: 442 KiB |
|
After Width: | Height: | Size: 484 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 200 KiB |
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "The Legend of Zelda: Breath of the Wild",
|
||||||
|
"titleIds": ["00050000101C9300", "00050000101C9400", "00050000101C9500"],
|
||||||
|
"icon": "https://cdn2.steamgriddb.com/icon_thumb/ed54e9a013d6bbc378503bdb4ca43c27.png",
|
||||||
|
"mods": [
|
||||||
|
{
|
||||||
|
"id": "botw-amiibo-outfits",
|
||||||
|
"name": "Zelda's Ballad Amiibo Outfits",
|
||||||
|
"author": "Amiibolad, YamGaming",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "modpack",
|
||||||
|
"releaseDate": "2021-07-18",
|
||||||
|
"description": "All Amiibo armor sets redesigned to reflect Zelda's previous incarnations. Includes icons and physics. Outfits are mix-and-match compatible. Do not combine the full pack with separate downloads.",
|
||||||
|
"tags": ["armor", "outfits", "amiibo"],
|
||||||
|
"license": "Custom",
|
||||||
|
"requirements": ["Zelda's Ballad"],
|
||||||
|
"download": "https://placeholder.example.com/botw-amiibo-outfits.zip",
|
||||||
|
"thumbnail": "https://gittea.dev/anononon/do-not-use/raw/branch/main/games/breath-of-the-wild/assets/amiibo-thumb.jpg",
|
||||||
|
"screenshots": [
|
||||||
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/breath-of-the-wild/assets/amiibo-screen1.jpg",
|
||||||
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/breath-of-the-wild/assets/amiibo-screen2.jpg",
|
||||||
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/breath-of-the-wild/assets/amiibo-screen3.jpg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "botw-mirror-shield",
|
||||||
|
"name": "Mirror Shield",
|
||||||
|
"author": "Lylari uPic, IssueLink",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"type": "mod",
|
||||||
|
"releaseDate": "2019-12-23",
|
||||||
|
"description": "Standalone Mirror Shield implemented as a new actor with unique properties. Includes an alternate version with the crescent moon symbol from Ocarina of Time 1.0.",
|
||||||
|
"tags": ["weapon", "shield", "model"],
|
||||||
|
"license": "Custom",
|
||||||
|
"download": "https://gamebanana.com/mods/download/49623#FileInfo_442793",
|
||||||
|
"thumbnail": "https://gittea.dev/anononon/do-not-use/raw/branch/main/games/breath-of-the-wild/assets/mirror-shield-thumb.jpg",
|
||||||
|
"screenshots": [
|
||||||
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/breath-of-the-wild/assets/mirror-shield-screen1.jpg",
|
||||||
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/breath-of-the-wild/assets/mirror-shield-screen2.jpg"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 586 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 126 KiB |
@@ -1,75 +1,58 @@
|
|||||||
{
|
{
|
||||||
"name": "Mario Kart 8",
|
"name": "Mario Kart 8",
|
||||||
"cover": "assets/cover.png",
|
"titleIds": ["000500001010EB00", "000500001010EC00", "000500001010ED00"],
|
||||||
"title_ids": [
|
"icon": "https://cdn2.steamgriddb.com/icon_thumb/ac1ad983e08ad3304a97e147f522747e.png",
|
||||||
"000500001010EB00",
|
|
||||||
"000500001010EC00",
|
|
||||||
"000500001010ED00"
|
|
||||||
],
|
|
||||||
"mods": [
|
"mods": [
|
||||||
{
|
{
|
||||||
"id": "hd-ui-pack",
|
"id": "mk8-galaxy-modpack",
|
||||||
"name": "HD UI Pack",
|
"name": "Mario Kart Galaxy Modpack",
|
||||||
"type": "mod",
|
"author": "Fuffina, FunkyRacer, Squadaloo & more",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"author": "ExampleUser",
|
|
||||||
"description": "Replaces the default UI textures with high-resolution alternatives. Includes improved fonts, icons, and menu backgrounds.",
|
|
||||||
"thumbnail": "https://placehold.co/400x225/1a1a2e/ffffff?text=HD+UI+Pack",
|
|
||||||
"screenshots": [
|
|
||||||
"https://placehold.co/800x450/1a1a2e/ffffff?text=Screenshot+1",
|
|
||||||
"https://placehold.co/800x450/16213e/ffffff?text=Screenshot+2"
|
|
||||||
],
|
|
||||||
"download": "https://example.com/mods/hd-ui-pack.zip",
|
|
||||||
"modpack_path": "content/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "custom-music-pack",
|
|
||||||
"name": "Custom Music Pack",
|
|
||||||
"type": "mod",
|
|
||||||
"version": "2.1.0",
|
|
||||||
"author": "MusicModder",
|
|
||||||
"description": "Replaces all in-game music with custom tracks. Over 40 songs included.",
|
|
||||||
"thumbnail": "https://placehold.co/400x225/0f3460/ffffff?text=Music+Pack",
|
|
||||||
"screenshots": [
|
|
||||||
"https://placehold.co/800x450/0f3460/ffffff?text=Screenshot+1"
|
|
||||||
],
|
|
||||||
"download": "https://example.com/mods/custom-music-pack.zip",
|
|
||||||
"modpack_path": "content/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "retro-track-pack",
|
|
||||||
"name": "Retro Track Textures",
|
|
||||||
"type": "mod",
|
|
||||||
"version": "1.3.2",
|
|
||||||
"author": "RetroFan",
|
|
||||||
"description": "Gives all retro cups a classic low-poly aesthetic inspired by older Mario Kart titles.",
|
|
||||||
"thumbnail": "https://placehold.co/400x225/533483/ffffff?text=Retro+Tracks",
|
|
||||||
"screenshots": [
|
|
||||||
"https://placehold.co/800x450/533483/ffffff?text=Screenshot+1",
|
|
||||||
"https://placehold.co/800x450/2d132c/ffffff?text=Screenshot+2",
|
|
||||||
"https://placehold.co/800x450/1b1b2f/ffffff?text=Screenshot+3"
|
|
||||||
],
|
|
||||||
"download": "https://example.com/mods/retro-track-pack.zip",
|
|
||||||
"modpack_path": "content/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ultimate-bundle",
|
|
||||||
"name": "Ultimate MK8 Bundle",
|
|
||||||
"type": "modpack",
|
"type": "modpack",
|
||||||
"version": "1.0.0",
|
"releaseDate": "2026-02-22",
|
||||||
"author": "ExampleUser",
|
"description": "51 custom tracks across 12 cups - 4 Nitro-only, 4 Retro-only, 4 mixed. Replaces the entire roster except Female Villager. Includes custom vehicle parts, items and music. Supports English and Latin American Spanish.",
|
||||||
"description": "A curated bundle combining the HD UI Pack, Custom Music Pack, and Retro Track Textures into one install.",
|
"tags": ["tracks", "music", "skins", "items"],
|
||||||
"thumbnail": "https://placehold.co/400x225/e94560/ffffff?text=Ultimate+Bundle",
|
"license": "Custom",
|
||||||
|
"download": "https://placeholder.example.com/mk8-galaxy-modpack.zip",
|
||||||
|
"thumbnail": "https://gittea.dev/anononon/do-not-use/raw/branch/main/games/mario-kart-8/assets/galaxy-modpack-thumb.jpg",
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
"https://placehold.co/800x450/e94560/ffffff?text=Bundle+Preview"
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/mario-kart-8/assets/galaxy-modpack-screen1.jpg",
|
||||||
],
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/mario-kart-8/assets/galaxy-modpack-screen2.jpg"
|
||||||
"includes": [
|
]
|
||||||
"hd-ui-pack",
|
},
|
||||||
"custom-music-pack",
|
{
|
||||||
"retro-track-pack"
|
"id": "mk8-minecart-skin",
|
||||||
],
|
"name": "Minecart Skin",
|
||||||
"download": "https://example.com/mods/ultimate-bundle.zip",
|
"author": "Radnos",
|
||||||
"modpack_path": "content/"
|
"version": "1.1.0",
|
||||||
|
"type": "mod",
|
||||||
|
"releaseDate": "2020-07-25",
|
||||||
|
"description": "Replaces the Tanooki Kart with Steve's Minecart from Minecraft. Includes a matching UI skin.",
|
||||||
|
"tags": ["kart", "skin"],
|
||||||
|
"license": "Custom",
|
||||||
|
"download": "https://gamebanana.com/dl/542942",
|
||||||
|
"thumbnail": "https://gittea.dev/anononon/do-not-use/raw/branch/main/games/mario-kart-8/assets/minecart-thumb.jpg",
|
||||||
|
"screenshots": [
|
||||||
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/mario-kart-8/assets/minecart-screen1.jpg",
|
||||||
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/mario-kart-8/assets/minecart-screen2.jpg"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mk8-shrek-skin",
|
||||||
|
"name": "Shrek Skin",
|
||||||
|
"author": "largescale",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "mod",
|
||||||
|
"releaseDate": "2018-04-15",
|
||||||
|
"description": "Ogres have layers. Replaces a character model with Shrek. The rig is a bit rough due to poly count.",
|
||||||
|
"tags": ["character", "skin"],
|
||||||
|
"license": "Custom",
|
||||||
|
"download": "https://gamebanana.com/dl/379569",
|
||||||
|
"thumbnail": "https://gittea.dev/anononon/do-not-use/raw/branch/main/games/mario-kart-8/assets/shrek-thumb.jpg",
|
||||||
|
"screenshots": [
|
||||||
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/mario-kart-8/assets/shrek-screen1.jpg",
|
||||||
|
"https://gittea.dev/anononon/do-not-use/raw/branch/main/games/mario-kart-8/assets/shrek-screen2.jpg"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "Wii U Mod Store Template Repo",
|
"formatVersion": 1,
|
||||||
"description": "Official template repository for the Wii U Mod Store. Fork this to host your own mods.",
|
"name": "Tim's Mod Repo",
|
||||||
"author": "your-name",
|
"description": "A personal CupStore mod repository.",
|
||||||
"version": "1",
|
|
||||||
"games": [
|
"games": [
|
||||||
{
|
{ "path": "games/mario-kart-8/game.json" },
|
||||||
"id": "mario-kart-8",
|
{ "path": "games/breath-of-the-wild/game.json" }
|
||||||
"path": "games/mario-kart-8/game.json"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
CupStore repo validation script.
|
||||||
|
Usage: python3 validate.py [--check-urls]
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
REQUIRED_MOD_FIELDS = ["id", "name", "author", "version", "description", "download", "type"]
|
||||||
|
REQUIRED_GAME_FIELDS = ["name", "titleIds"]
|
||||||
|
VALID_TYPES = ["mod", "modpack"]
|
||||||
|
ID_PATTERN = re.compile(r'^[a-z0-9\-_]+$')
|
||||||
|
URL_PATTERN = re.compile(r'^https?://.+\..+')
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
def err(msg): errors.append(msg)
|
||||||
|
def warn(msg): warnings.append(msg)
|
||||||
|
|
||||||
|
def validate_url(url, field, context):
|
||||||
|
if not URL_PATTERN.match(url):
|
||||||
|
err(f"{context}: {field} is not a valid URL: {url!r}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_url_reachable(url, context):
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(url, method='HEAD')
|
||||||
|
urllib.request.urlopen(req, timeout=10)
|
||||||
|
except Exception as e:
|
||||||
|
warn(f"{context}: URL not reachable: {url} ({e})")
|
||||||
|
|
||||||
|
def validate_mod(mod, game_name, seen_ids, check_urls):
|
||||||
|
ctx = f"Game '{game_name}' / Mod '{mod.get('id', '?')}'"
|
||||||
|
|
||||||
|
# Required fields
|
||||||
|
for f in REQUIRED_MOD_FIELDS:
|
||||||
|
if f not in mod:
|
||||||
|
err(f"{ctx}: missing required field '{f}'")
|
||||||
|
|
||||||
|
mod_id = mod.get("id", "")
|
||||||
|
|
||||||
|
# ID format
|
||||||
|
if mod_id and not ID_PATTERN.match(mod_id):
|
||||||
|
err(f"{ctx}: id '{mod_id}' contains invalid characters (only a-z, 0-9, - _ allowed)")
|
||||||
|
|
||||||
|
# ID uniqueness (global)
|
||||||
|
if mod_id in seen_ids:
|
||||||
|
err(f"{ctx}: duplicate id '{mod_id}' (also in '{seen_ids[mod_id]}')")
|
||||||
|
else:
|
||||||
|
seen_ids[mod_id] = game_name
|
||||||
|
|
||||||
|
# Type
|
||||||
|
if mod.get("type") not in VALID_TYPES:
|
||||||
|
err(f"{ctx}: type must be one of {VALID_TYPES}, got {mod.get('type')!r}")
|
||||||
|
|
||||||
|
# URLs
|
||||||
|
if "download" in mod: validate_url(mod["download"], "download", ctx)
|
||||||
|
if "thumbnail" in mod: validate_url(mod["thumbnail"], "thumbnail", ctx)
|
||||||
|
for i, s in enumerate(mod.get("screenshots", [])):
|
||||||
|
validate_url(s, f"screenshots[{i}]", ctx)
|
||||||
|
|
||||||
|
if check_urls:
|
||||||
|
if "download" in mod: check_url_reachable(mod["download"], ctx)
|
||||||
|
|
||||||
|
# Optional field types
|
||||||
|
if "fileSize" in mod and not isinstance(mod["fileSize"], int):
|
||||||
|
err(f"{ctx}: fileSize must be an integer (bytes)")
|
||||||
|
if "tags" in mod and not isinstance(mod["tags"], list):
|
||||||
|
err(f"{ctx}: tags must be a list of strings")
|
||||||
|
if "requirements" in mod and not isinstance(mod["requirements"], list):
|
||||||
|
err(f"{ctx}: requirements must be a list of strings")
|
||||||
|
|
||||||
|
def validate_game(game_data, game_path, seen_ids, check_urls):
|
||||||
|
name = game_data.get("name", game_path)
|
||||||
|
|
||||||
|
# titleIds (support both camelCase and snake_case)
|
||||||
|
if "titleIds" not in game_data and "title_ids" not in game_data:
|
||||||
|
err(f"Game '{name}': missing 'titleIds'")
|
||||||
|
else:
|
||||||
|
tids = game_data.get("titleIds", game_data.get("title_ids", []))
|
||||||
|
if not tids:
|
||||||
|
err(f"Game '{name}': titleIds is empty")
|
||||||
|
|
||||||
|
if "mods" not in game_data or not game_data["mods"]:
|
||||||
|
warn(f"Game '{name}': no mods defined")
|
||||||
|
return
|
||||||
|
|
||||||
|
for mod in game_data["mods"]:
|
||||||
|
validate_mod(mod, name, seen_ids, check_urls)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Validate a CupStore repo")
|
||||||
|
parser.add_argument("--check-urls", action="store_true", help="Check if download URLs are reachable")
|
||||||
|
parser.add_argument("--repo", default="repo.json", help="Path to repo.json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.exists(args.repo):
|
||||||
|
print(f"ERROR: {args.repo} not found")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
with open(args.repo) as f:
|
||||||
|
try:
|
||||||
|
repo = json.load(f)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
print(f"ERROR: repo.json is invalid JSON: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Repo-level fields
|
||||||
|
for f in ["name", "formatVersion", "games"]:
|
||||||
|
if f not in repo:
|
||||||
|
err(f"repo.json: missing required field '{f}'")
|
||||||
|
|
||||||
|
seen_ids = {}
|
||||||
|
|
||||||
|
for entry in repo.get("games", []):
|
||||||
|
if "path" not in entry:
|
||||||
|
err(f"repo.json: game entry missing 'path'")
|
||||||
|
continue
|
||||||
|
game_path = entry["path"]
|
||||||
|
if not os.path.exists(game_path):
|
||||||
|
err(f"repo.json: game file not found: {game_path!r}")
|
||||||
|
continue
|
||||||
|
with open(game_path) as f:
|
||||||
|
try:
|
||||||
|
game_data = json.load(f)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
err(f"{game_path}: invalid JSON: {e}")
|
||||||
|
continue
|
||||||
|
validate_game(game_data, game_path, seen_ids, args.check_urls)
|
||||||
|
|
||||||
|
# Report
|
||||||
|
if warnings:
|
||||||
|
print(f"\n⚠ {len(warnings)} warning(s):")
|
||||||
|
for w in warnings: print(f" {w}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print(f"\n✗ {len(errors)} error(s):")
|
||||||
|
for e in errors: print(f" {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"✓ Repo is valid ({len(seen_ids)} mods across {len(repo.get('games', []))} game(s))")
|
||||||
|
if warnings:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||