# Faded Local Mod Installer

Faded Local Mod Installer is a Windows, macOS, and Linux/Steam Deck desktop app for downloading local, non-Workshop Project Zomboid mods from a public `manifest.json` file and installing them into the current user's local mods folder.

Default install path:

```text
Windows: %USERPROFILE%\Zomboid\mods
macOS/Linux: ~/Zomboid/mods
```

The app is intentionally simple for the MVP:

1. The app loads a public `manifest.json` URL.
2. The app displays each mod's name, version, server Mod ID, and description.
3. The user selects one or more mods.
4. The app downloads ZIP files from each manifest entry's `download_url`.
5. The app extracts and installs them to `%USERPROFILE%\Zomboid\mods`.

Normal users do not need API keys, login, OAuth, or cloud setup.

## Terms And Conditions

The installer requires users to accept the FadedMods Terms and Conditions before use:

```text
https://fadedmods.com/?page_id=68
```

The Settings tab also includes a link to reopen the terms later.

## Valid Mod ZIP Structure

Each ZIP should contain one proper Project Zomboid mod folder. Older/simple mods can place `mod.info` directly inside the mod folder:

```text
YourModFolder/
  mod.info
  media/
  poster.png
```

The final installed structure should look like this:

```text
C:\Users\<USERNAME>\Zomboid\mods\YourModFolder\mod.info
C:\Users\<USERNAME>\Zomboid\mods\YourModFolder\media\...
```

Build-specific Project Zomboid 42 mods are also supported:

```text
YourModFolder/
  42/
    mod.info
    media/
  common/
```

Those install as:

```text
C:\Users\<USERNAME>\Zomboid\mods\YourModFolder\42\mod.info
```

The installer is designed to avoid broken nested installs like:

```text
mods\YourModFolder\YourModFolder\mod.info
```

## Tech Stack

This project uses Python, PySide6, and PyInstaller.

That stack keeps the source readable while PyInstaller produces self-contained native packages without requiring users to install Python.

## Project Structure

```text
app/
  app_config.py       App settings and default paths
  manifest_client.py  Public manifest loading and ZIP downloading
  installer.py        Safe zip extraction, validation, backup, and install logic
  java_loader.py      Signed FJL release staging, CLI execution, and JAR verification
  java_mod_installer.py Transactional Java-enabled mod installation
  remote_client.py    SFTP/FTP/FTPS server upload helpers
  logger.py           Log file setup
  ui.py               PySide6 desktop app
  version.py          App version
examples/
  mod_manifest.example.json
installer/
  FadedLocalModInstaller.iss
scripts/
  build_exe.ps1
  package_zip.ps1
tests/
  test_installer.py
  test_manifest_client.py
main.py
requirements.txt
README.md
```

## How Players Use It

1. Open the app.
2. Go to **Settings**.
3. Paste the public `manifest.json` URL into **Manifest URL**.
4. Confirm the install path. The default is usually correct:

   ```text
   %USERPROFILE%\Zomboid\mods
   ```

5. Go back to **Player Install**.
6. Click **Refresh Mod List**.
7. Use search or badges to find the mods you need.
8. Select one or more mods.
9. Click **Download & Install**.

If a mod is already installed, the app offers:

- Backup Then Replace
- Replace Existing Mod
- Skip Existing Mod

Backups are stored here:

```text
%APPDATA%\FadedLocalModInstaller\backups
```

Logs are stored here:

```text
%APPDATA%\FadedLocalModInstaller\logs\installer.log
```

Player quality-of-life features:

- Search mods by name, version, description, or server Mod ID.
- See badges for `Installed`, `Not Installed`, `Update Available`, and `Latest Update`.
- Select mods and click **Copy Mods= Line** for server config, for example:

  ```text
  Mods=ExampleMod;AnotherMod
  ```

- Click **Copy Mod IDs** to copy only the selected semicolon-separated IDs.
- Use the details panel to inspect the selected mod's Mod ID, install status, installed version, and description.
- Install and upload operations show progress bars.
- Use **Verify Installed Mods**, **Repair Missing Files**, or **Reinstall Selected Fresh** to recover from missing files or a bad cached download.
- Use **Clear Installer Cache** when a user may have an interrupted, stale, or quarantined temporary download.
- Settings includes **Copy Diagnostics** for easier support reports.
- Settings can optionally launch the installer at OS startup and auto-update the latest highlighted manifest mods. Both options are off by default.

## Java-Enabled Mods

Manifest mods can declare either a hybrid Faded Java Loader (FJL) plugin under `42/java`, another numeric build folder's `java` directory, or `common/java`, or a standalone plugin JAR. For those entries the installer:

1. Selects the Windows client, Windows dedicated-server, or Linux dedicated-server FJL package.
2. Downloads it over HTTPS and checks its exact size and SHA-256 hash.
3. Verifies a detached Ed25519 signature against a publisher key pinned in this app release.
4. Runs only the verified FJL package and validates its JSON output and process exit code.
5. Installs or repairs FJL before copying the mod.
6. Copies `FadedJavaLoaderBridge` from the signed FJL package and enables it in the client `mods/default.txt` or the selected dedicated-server `Server/<name>.ini` preset.
7. Validates `META-INF/faded-java-mod.json`. Hybrid JARs remain under the mod's build folder; standalone JARs are installed only through FJL's `install-plugin --json` command.
8. Adds the plugin id to `FadedJavaLoader/profiles/default.json`, removes a matching disabled entry, and runs FJL `verify` again.
9. Restores the previous mod/plugin, bridge, preset, and profile if any post-copy verification fails.

The **Java Loader** tab reports **Not installed**, **Installed**, **Update available**, or **Repair required**, shows the installed version, and provides install/update, repair, verify, redacted diagnostics, and uninstall actions. For a dedicated server, set **Server Preset Name** to the INI filename without `.ini` (the default is `servertest`). Linux Project Zomboid clients are not supported by FJL; Linux dedicated servers are supported. A complete Project Zomboid or dedicated-server restart is required after installing or updating FJL or a Java plugin.

For multiplayer, required or transforming `BOTH` plugins must have the exact same id, version, and SHA-256 on client and server. The installer never transfers executable JARs from a game server to clients. Server operators can run the local CLI preflight with `--java-multiplayer-preflight`, both mods roots, a dedicated-server `--java-target`, and `--java-root`; every mismatch is reported by plugin name. The preflight also fails unless FJL verifies the server startup hook, so an unpatchable startup command cannot pass.

Java-enabled mods cannot be pushed blindly through the FTP/SFTP **Server Upload** tab. That connection cannot safely stop the server, execute and verify FJL on the host, and restart the server. Run this app or its CLI on the dedicated-server machine instead.

## Server Upload

The **Server Upload** tab lets server owners upload the same manifest mods directly to a dedicated server over:

- SFTP
- FTP
- FTPS

Use the tab to connect, browse remote folders, detect or create a Project Zomboid mods folder, then upload selected mods to the server.

The folder detector looks for common dedicated-server layouts such as:

```text
Zomboid/mods
ProjectZomboid/mods
/home/container/Zomboid/mods
```

If the folder does not exist, the installer shows the exact remote folders it wants to create before creating them. Passwords are not saved in the app config.

Server upload quality-of-life features:

- The server upload page scrolls on smaller screens and has draggable dividers for the remote browser, mod list, and log.
- Search/filter the server upload mod list.
- Save named server profiles for host, port, username, protocol, key path, and remote folder.
- Type a new profile name directly in the profile box, or choose a saved profile to load it.
- Remote folder selection is shown under the browser before uploading.
- Passwords are not stored in server profiles.
- Copy `Mods=` lines from selected server upload mods.
- Upload progress is shown during download, validation, replacement, and remote upload.

## manifest.json Setup

Host a public `manifest.json` file anywhere that can be reached by a normal browser. GitHub Pages, a static web host, your own website, or a public object storage URL are all fine.

Example:

```json
{
  "manifest_version": 1,
  "app_name": "Faded Local Mod Installer",
  "mods": [
    {
      "id": "your-first-mod-id",
      "name": "Your First Mod Name",
      "version": "1.0.0",
      "description": "Short plain-language description of this mod.",
      "updated_at": "2026-06-22",
      "server_mod_id": "YourFirstModId",
      "download_url": "https://github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME/releases/download/your-first-mod-id-v1.0.0/YOUR_FIRST_MOD_ZIP.zip",
      "zip_name": "YOUR_FIRST_MOD_ZIP.zip"
    },
    {
      "id": "your-second-mod-id",
      "name": "Your Second Mod Name",
      "version": "1.0.0",
      "description": "Short plain-language description of this mod.",
      "updated_at": "2026-06-22",
      "server_mod_id": "YourSecondModId",
      "download_url": "https://github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME/releases/download/your-second-mod-id-v1.0.0/YOUR_SECOND_MOD_ZIP.zip",
      "zip_name": "YOUR_SECOND_MOD_ZIP.zip"
    }
  ]
}
```

Rules:

- `manifest_version` must be `1`.
- `app_name` should be `Faded Local Mod Installer`.
- `mods` must be an array.
- Each mod must have a unique `id`.
- Each mod must have a readable `name`.
- Each mod must have a `version`.
- Each mod must have a short `description`.
- Each mod can include optional `updated_at` in `YYYY-MM-DD` format. The newest date is highlighted in the app, and the **Update** button installs only those highlighted mods.
- Each mod can include optional `server_mod_id` for the exact `id=` value from `mod.info`. This is shown in bold in the installer for server config use. If omitted, the app falls back to the manifest `id`.
- Each mod must have a public direct `download_url`.
- Each normal or hybrid mod must have a `zip_name` ending in `.zip`; a standalone FJL plugin uses `.jar`.
- Each `download_url` should download a ZIP file without requiring login.
- Users should not need API keys, login, OAuth, or cloud setup.
- Large mods can use `download_parts` instead of one `download_url`.
- Each `download_parts` entry must include a public direct `download_url` and `.zip` `zip_name`.
- A Java-enabled mod sets `requiresJavaLoader` to `true` and includes `minimumJavaLoaderVersion`, `javaEnvironment` (`CLIENT`, `SERVER`, or `BOTH`), `javaPluginId`, `javaPluginVersion`, and optional `javaPluginMode` (`HYBRID` by default or `STANDALONE`).
- Every ZIP or multipart ZIP belonging to a Java-enabled mod must include exact `sha256` and `size_bytes` values. The installed hybrid JAR is then hashed again and compared byte-for-byte with the verified source copy.
- A manifest containing Java-enabled mods must include a top-level signed `java_loader` release. See `docs/JAVA_LOADER_RELEASES.md` for its schema and publishing procedure.

An example file is included at:

```text
examples\mod_manifest.example.json
```

## GitHub Mod Host Repo

This project includes a starter repo template here:

```text
github-mod-host-repo\
```

Copy that folder into its own GitHub repository.

Recommended hosted layout:

```text
manifest.json
releases/
  README.md
```

For small ZIP files, you can put ZIPs in `releases/` and use raw GitHub URLs in `download_url`.

For large ZIP files, use GitHub Release assets instead of committing ZIPs into the repository.

GitHub blocks normal repository files larger than 100 MiB. GitHub Releases allow release assets under 2 GiB. A large mod ZIP should use a release asset URL like:

```text
https://github.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME/releases/download/MOD_ID-vVERSION/YOUR_MOD_ZIP.zip
```

The app does not require the GitHub API. It only needs a public raw `manifest.json` URL and public ZIP download URLs.

After pushing the GitHub mod host repo, use this kind of URL in the installer's **Manifest URL** setting:

```text
https://raw.githubusercontent.com/YOUR_GITHUB_USERNAME/YOUR_REPO_NAME/main/manifest.json
```

If you accidentally use a GitHub `blob` URL for a ZIP, the app converts it to a raw GitHub URL internally.

## Server Owner Instructions

The app includes a **Server Owner Instructions** tab. It does not automatically install to servers yet.

Windows dedicated server path:

```text
C:\Users\<SERVER_USER>\Zomboid\mods
```

Linux dedicated server path:

```text
~/Zomboid/mods
```

Hosted servers usually require the host's File Manager or FTP/SFTP. Look for a `Zomboid`, server profile, or `mods` folder and upload the mod folder so `mod.info` is directly inside the mod folder:

```text
Zomboid/mods/ExampleMod/mod.info
Zomboid/mods/ExampleMod/media/...
```

Local non-Workshop mods must exist on the server and on every client. Server owners may still need to add the mod ID to the server config. The mod ID is usually inside `mod.info`. This tool does not replace proper server config setup.

## Run Locally

From this project folder:

```powershell
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements.txt
python main.py
```

## Run Tests

```powershell
python -m unittest discover -s tests
```

The tests cover manifest parsing, safe zip extraction, `mod.info` validation, duplicate nested folder avoidance, existing mod backup, signed FJL staging, hybrid plugin validation, and default install path behavior.

## Build The Windows EXE

Manual build:

```powershell
python -m venv .venv
.\.venv\Scripts\activate
pip install -r requirements.txt
pyinstaller --onefile --windowed --name "Faded Local Mod Installer" main.py
```

Or run:

```powershell
.\scripts\build_exe.ps1
```

If PowerShell blocks scripts on your machine, use:

```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\build_exe.ps1
```

The built app will be:

```text
dist\Faded Local Mod Installer.exe
```

## Create A Downloadable Zip Package

After building the `.exe`:

```powershell
.\scripts\package_zip.ps1
```

If PowerShell blocks scripts on your machine, use:

```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\package_zip.ps1
```

The package will be created in:

```text
release\FadedLocalModInstaller-<version>-windows.zip
```

Include:

- `Faded Local Mod Installer.exe`
- `README.md`

Do not include:

- `.venv`
- `build`
- private `config.json`
- `.env`
- secrets
- tokens
- logs
- cache folders

## Build The Linux / Steam Deck Package

The Linux build is separate from the Windows `.exe`. Build it on Linux, Steam Deck Desktop Mode, or WSL after WSL has been installed and initialized.

On Linux:

```bash
sudo apt install python3 python3-venv python3-pip binutils file coreutils findutils libxcb-cursor0
chmod +x scripts/build_linux.sh scripts/package_linux.sh
./scripts/build_linux.sh
./scripts/package_linux.sh
```

The package will be created in:

```text
release/FadedLocalModInstaller-<version>-linux-<arch>.tar.gz
```

The package includes:

- `faded-local-mod-installer`
- `install.sh`
- `README-LINUX.md`
- desktop launcher metadata

The Linux tarball is built from the same app source as the Windows release. If a Steam Deck still shows an old app version, run the `install.sh` from the newer tarball so it overwrites the previous files in `~/.local/share/FadedLocalModInstaller`.

Linux and Steam Deck defaults:

```text
Install path: ~/Zomboid/mods
Config: ~/.config/FadedLocalModInstaller/config.json
Logs: ~/.local/state/FadedLocalModInstaller/logs/installer.log
Backups: ~/.local/share/FadedLocalModInstaller/backups
```

From Windows after WSL is installed and the PC has rebooted:

```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build_linux_wsl.ps1
```

## Optional Windows Installer

Install Inno Setup, build the `.exe`, then compile:

```text
installer\FadedLocalModInstaller.iss
```

That creates an installer in:

```text
release\
```

## Updating Mods Later

To update mod files for players:

1. Create a new valid zip for the mod.
2. Upload the zip to your hosting location.
3. Update the mod's `version` in `manifest.json`.
4. Update the mod's `updated_at` date in `YYYY-MM-DD` format so the app highlights it.
5. Update the `download_url` or `zip_name` in `manifest.json` if either changed.
6. Keep the zip structure valid: one mod folder with `mod.info` directly inside.
7. Players click **Refresh Mod List**, then click **Update** to install the highlighted latest mod(s).
8. If the mod already exists, players can backup and replace it.

To update the app:

1. Edit the source.
2. Update `APP_VERSION` in `app/version.py`.
3. Update the version in `scripts/package_zip.ps1` and `installer/FadedLocalModInstaller.iss`.
4. Run tests.
5. Build a new `.exe`.
6. Package a new release zip or installer.

## Security Notes

- Normal mods are data-only and are never executed by the installer.
- FJL packages are executable code and are run only after HTTPS download, exact size, SHA-256, and a detached Ed25519 signature all verify against a key pinned in the app.
- A remote manifest cannot add or replace trusted publisher keys.
- Zip extraction blocks path traversal attempts.
- FJL extraction also rejects symbolic links.
- Existing mods are not overwritten without a user choice.
- Backups use timestamped folders.
- FJL subprocesses use argument arrays, JSON result validation, deterministic exit-code checks, timeouts, and diagnostic redaction.
- No credentials are required for normal users.
- The app does not collect user data.
- There is no telemetry.

## Troubleshooting

If the mod list does not load:

- Confirm the Manifest URL opens in a browser.
- Confirm the manifest is valid JSON.
- Confirm `manifest_version` is `1`.
- Confirm each manifest entry has `id`, `name`, `version`, `description`, `download_url`, and `zip_name`.
- Confirm each `download_url` is public and does not require login.
- Check the log file.

If a download fails:

- Confirm the ZIP file still exists at the `download_url`.
- Confirm the link downloads the ZIP directly.
- If using GitHub Release assets, confirm the release is public.
- If using a ZIP committed into `releases/`, confirm it is under GitHub's normal repository file limit.
- If the app says the download is a web page instead of a ZIP, GitHub is returning a web page, permission page, or missing-file page instead of the ZIP.
- Try downloading the file manually from the browser.

If install fails:

- Confirm the zip contains exactly one `mod.info`.
- Confirm `mod.info` is directly inside the real mod folder.
- Confirm Project Zomboid is not locking files.
- Confirm the app has permission to write to the selected install path.

If the app says multiple mod folders were found:

- The zip likely contains more than one `mod.info`.
- Split those into separate mod zip files.

If the mods folder does not exist:

- Let the app create it, or create it manually at:

  ```text
  %USERPROFILE%\Zomboid\mods
  ```
