Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: zone-relay
name: modbrew
path: staging
retention-days: 1

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: zone-relay
name: modbrew
path: artifact

- name: Copy files
Expand All @@ -42,7 +42,7 @@ jobs:
cp -a resources/ release

- name: Create archive
run: zip -r ZoneRelay-${{ inputs.build-ver }}.zip .
run: zip -r Modbrew-${{ inputs.build-ver }}.zip .
working-directory: release

- name: Create release
Expand All @@ -52,4 +52,4 @@ jobs:
name: ${{ inputs.build-ver }}
body_path: ./CHANGELOG.md
prerelease: false
files: release/ZoneRelay-${{ inputs.build-ver }}.zip
files: release/Modbrew-${{ inputs.build-ver }}.zip
2 changes: 1 addition & 1 deletion .idea/runConfigurations/Build___Copy___Run.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/runConfigurations/Copy_Resources.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Copyright (c) 2026 Tosox
Project: Modbrew

This project is licensed under the Creative Commons
Attribution-NonCommercial-NoDerivatives 4.0 International License
(CC BY-NC-ND 4.0).

Full license text:
https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode

Summary:
- You may use and view this repository.
- You may share unmodified copies of this work for non-commercial purposes,
provided you give appropriate credit and include a link to the license.
- You may not share or distribute modified versions of this work.
- You may not use this work for commercial purposes.

Additional Permissions:
1. Contribution Permission
You may create local modifications solely to submit patches or pull requests
to this repository, and may share those modifications only with the project
owner (Tosox) or through the repository’s normal contribution workflow.

2. Maintainer Use of Contributions
By submitting a contribution, you agree that it is provided under the same
project license and these additional permissions, and you grant the project
owner (Tosox) a perpetual, irrevocable, worldwide right to use, modify, and
incorporate it into this project.

3. Explicit Exception by Permission
Uses outside this license may be allowed only with prior explicit written
permission from the project owner (Tosox).
109 changes: 108 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,108 @@
# ZoneRelay
# Modbrew

A declarative mod installer for PC games. You write a `modbrew.yaml` describing what to download and where to put it — Modbrew handles the rest.

## 🍺 How it works

Modbrew reads a `modbrew.yaml` file (the *brewfile*) authored by a modlist maintainer. It downloads each mod, extracts it, copies files to the right locations, and sets up a Mod Organizer 2 profile — all in one click.

Users never touch the YAML. Maintainers write it once; everyone brews from it.

## 📋 Requirements

- [Mod Organizer 2](https://github.com/ModOrganizer2/modorganizer) installed and launched at least once
- Modbrew placed in a `.Modbrew/` subfolder inside the MO2 directory
- A `data/modbrew.yaml` brewfile alongside the installer

## 🚀 Getting started

1. Create a `.Modbrew/` folder inside your MO2 directory (next to `ModOrganizer.exe`) and place `Modbrew.exe` and the accompanying `data/`, `locales/`, and `resources/` folders inside it.
2. Run `Modbrew.exe`.
3. Click **Brew**.

To reinstall everything from scratch, tick **Full brew** before clicking Brew.

## 📝 Writing a brewfile

The brewfile is `data/modbrew.yaml`. It is human-readable, Git-friendly, and maintained by the modlist author.

```yaml
profileName: "My Modlist"
shortcutName: "My Game — Modded"

mods:
- id: "mod-config-menu"
name: "Mod Configuration Menu"
url: "https://www.moddb.com/..."
hash: "abc123" # optional SHA-256, validated on download
recipe:
- "gamedata" # copy the gamedata/ folder from the archive

patches:
- id: "my-patch"
name: "Compatibility Patch"
url: "https://github.com/..."
recipe:
- "bin" # patches extract directly to the game root
- "db"

separators:
- id: "frameworks-sep"
name: "Frameworks" # creates a visual separator in MO2
```

### 🔑 Entry fields

| Field | Required | Description |
|---|---|---|
| `id` | ✓ | Unique identifier used for progress tracking and hash storage |
| `name` | ✓ | Display name shown in the installer UI |
| `url` | ✓ | Direct download URL or ModDB addon page URL |
| `hash` | — | Expected SHA-256 of the archive. Modbrew warns on mismatch and can update it automatically. |
| `recipe` | — | List of paths inside the extracted archive to copy to the destination |

### 📦 Recipe syntax

Each `recipe` entry is a path relative to the extracted archive root.

```yaml
recipe:
- "gamedata" # copy the gamedata/ folder as-is
- "MyMod-main/gamedata" # copy a nested folder
- "config/settings.ltx -> gamedata/config/settings.ltx" # explicit destination
- "bin/engine.exe -> bin/" # copy a file into a folder
```

The `->` operator maps a source path to a custom destination. Without it, the entry is copied to the mod folder root.

## ⚙️ User configuration

`user_config.yaml` (created next to the installer on first run) controls personal preferences:

```yaml
language: en-US
logLevel: INFO
logRetentionCount: 5
archiveCleanupStrategy: KEEP_ALL # KEEP_ALL | KEEP_LATEST_ONLY | DELETE_ALL
```

## 🗂️ Directory layout

```
ModOrganizer.exe ← MO2 lives here
.Modbrew/
├── Modbrew.exe
├── data/
│ ├── modbrew.yaml ← the brewfile
│ └── assets/
│ ├── icon.ico
│ └── splash.png
├── locales/
├── resources/
├── logs/
└── user_config.yaml
```

## 📄 License

Modbrew is licensed under [CC BY-NC-ND 4.0](https://creativecommons.org/licenses/by-nc-nd/4.0/) — free to use and share with attribution, but not for commercial purposes or redistribution of modified versions.
6 changes: 3 additions & 3 deletions data/modlist.yaml → data/modbrew.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ mods:
- id: "mcm"
name: "Mod Configuration Menu"
url: "https://www.moddb.com/mods/stalker-anomaly/addons/anomaly-mod-configuration-menu"
setup:
recipe:
- "gamedata"
- id: "fdda"
name: "Food, Drug and Drink Animations"
url: "https://github.com/Grokitach/anomaly_fdda/archive/refs/heads/main.zip"
setup:
recipe:
- "anomaly_fdda-main/gamedata"
patches:
- id: "modded-exes"
name: "Modded Exes"
url: "https://github.com/themrdemonized/xray-monolith/releases/download/2024.12.9/STALKER-Anomaly-modded-exes_2024.12.9.zip"
setup:
recipe:
- "db"
- "bin"
separators:
Expand Down
33 changes: 16 additions & 17 deletions locales/en-US.json
Original file line number Diff line number Diff line change
@@ -1,50 +1,49 @@
{
"GUI_INSTALLER_VERSION": "Installer version: {0}",
"GUI_INSTALL": "Install",
"GUI_INSTALL": ">>> Brew <<<",
"GUI_FULL_INSTALLATION": "Full installation",
"GUI_LAUNCH_MO2": "Launch",
"GUI_CURRENT_PROGRESS": "Current progress",
"GUI_TOTAL_PROGRESS": "Total progress",
"GUI_DOWNLOADING": "Downloading...",

"MSG_WELCOME_MESSAGE": "Welcome to {0}",
"MSG_STARTING_INSTALLATION": "Starting installation",
"MSG_COMPLETE_INSTALLATION": "\nInstallation complete. You can now launch the MO2 instance of the modpack from the desktop",
"MSG_READ_MODLIST_CFG": "Reading mod list configuration",
"MSG_STARTING_BREW": "Starting brew",
"MSG_COMPLETE_BREW": "\nBrew complete. You can now launch the MO2 instance of the modpack from the desktop",
"MSG_READ_BREWFILE_CFG": "Reading brewfile configuration",
"MSG_TITLE_CONFIGENTRY": "\n_______________ {0} _______________",
"MSG_CREATE_SEPARATOR": " Creating separator in {0}",
"MSG_GENERATE_META": " Generating meta.ini file",
"MSG_ADDON_DELETE_OLD_VERSION": " Deleting contents of the previous version",
"MSG_ADDON_ALREADY_UP_TO_DATE": " Installed version matches archive — skipping reinstall",
"MSG_HASH_MISMATCH_SUMMARY": "\nHash mismatches detected for the following mods:",
"MSG_HASH_MISMATCH_ENTRY": " {0}: expected {1}, got {2}",
"MSG_READ_SETUP" : " Reading setup instructions",
"MSG_READ_RECIPE": " Reading recipe",
"MSG_COPY_TO": " Copying {0} ==> {1}",
"MSG_COPY": " Copying {0}",
"MSG_EXTRACT_TO": " Extracting to {0}",
"MSG_INSTALLATION_MO2_SETUP": " MO2 Setup",
"MSG_INSTALLATION_CLEANUP": " Cleaning Workspace",
"MSG_CREATE_CUSTOM_PROFILE": " Creating custom MO2 profile",
"MSG_CREATE_SHORTCUT": " Creating shortcut to the modpack MO2 instance on the desktop",
"MSG_SETUP_FAILURE_SUMMARY": "\nRecipe path failures detected for the following mods:",

"ERR_INVALID_INSTALL_DIR": "Please move the installer into the MO2 directory",
"ERR_LAUNCH_MO2_FAIL": "An error occurred while trying to run MO2",
"ERR_LAUNCH_MO2": "Please launch MO2 once first",
"ERR_ALREADY_INSTALLING": "Installation already in progress",
"ERR_CONFIG_INVALID": "Failed to load or validate the modlist configuration",
"ERR_INSTALLATION_FAILED": "An error occurred during installation: {0}",
"ERR_ALREADY_BREWING": "Brew already in progress",
"ERR_CONFIG_INVALID": "Failed to load or validate the brewfile configuration",
"ERR_INSTALLATION_FAILED": "An error occurred during brew: {0}",
"ERR_SETUP_PATH_BLANK": "The following paths must not be blank: {0}",

"DLG_RESUME_TITLE": "Resume Installation",
"DLG_RESUME_MESSAGE": "Previous installation was interrupted at \"{0}\". Resume from there?",
"DLG_RESUME_TITLE": "Resume Brew",
"DLG_RESUME_MESSAGE": "Previous brew was interrupted at \"{0}\". Resume from there?",
"DLG_STALE_PROGRESS_TITLE": "Progress Discarded",
"DLG_STALE_PROGRESS_MESSAGE": "The saved progress file refers to a mod that no longer exists in the modlist. Press OK to start a full installation, or Cancel to abort.",
"DLG_STALE_PROGRESS_MESSAGE": "The saved progress file refers to a mod that no longer exists in the brewfile. Press OK to start a full brew, or Cancel to abort.",

"DLG_HASH_MISMATCH_TITLE": "Hash Mismatch",
"DLG_HASH_MISMATCH_UPDATE_TITLE": "Update Hashes",
"DLG_HASH_MISMATCH_UPDATE_MESSAGE": "Would you like to update the mismatched hashes in modlist.yaml with the actual values?",

"MSG_SETUP_FAILURE_SUMMARY": "\nSetup path failures detected for the following mods:",
"DLG_SETUP_FAILURE_TITLE": "Setup Path Failure",
"DLG_SETUP_FAILURE_DESCRIPTION": "<html>Some setup paths were not found in the extracted archive.<br>Correct them below, or dismiss to leave affected mods as empty folders (red X in MO2).</html>",
"ERR_SETUP_PATH_BLANK": "The following paths must not be blank: {0}"
"DLG_HASH_MISMATCH_UPDATE_MESSAGE": "Would you like to update the mismatched hashes in modbrew.yaml with the actual values?",
"DLG_SETUP_FAILURE_TITLE": "Recipe Path Failure",
"DLG_SETUP_FAILURE_DESCRIPTION": "<html>Some recipe paths were not found in the extracted archive.<br>Correct them below, or dismiss to leave affected mods as empty folders (red X in MO2).</html>"
}
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<modelVersion>4.0.0</modelVersion>

<groupId>de.tosox</groupId>
<artifactId>zonerelay</artifactId>
<artifactId>modbrew</artifactId>
<version>0.1.0</version>
<name>ZoneRelay</name>
<name>Modbrew</name>

<properties>
<maven.compiler.source>17</maven.compiler.source>
Expand Down Expand Up @@ -96,7 +96,7 @@
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>de.tosox.zonerelay.Main</mainClass>
<mainClass>de.tosox.modbrew.Main</mainClass>
</transformer>
</transformers>
</configuration>
Expand All @@ -120,7 +120,7 @@
<outfile>target/${project.name}-v${project.version}.exe</outfile>
<dontWrapJar>false</dontWrapJar>
<classPath>
<mainClass>de.tosox.zonerelay.Main</mainClass>
<mainClass>de.tosox.modbrew.Main</mainClass>
</classPath>
<jre>
<requiresJdk>true</requiresJdk>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package de.tosox.zonerelay;
package de.tosox.modbrew;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.name.Names;
import de.tosox.zonerelay.shared.config.AppPaths;
import de.tosox.zonerelay.ui.CrashHandler;
import de.tosox.zonerelay.ui.MainFrame;
import de.tosox.zonerelay.shared.i18n.Localizer;
import de.tosox.zonerelay.shared.logging.Logger;
import de.tosox.modbrew.shared.config.AppPaths;
import de.tosox.modbrew.ui.CrashHandler;
import de.tosox.modbrew.ui.MainFrame;
import de.tosox.modbrew.shared.i18n.Localizer;
import de.tosox.modbrew.shared.logging.Logger;

public class Application {
private final MainFrame mainFrame;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
package de.tosox.zonerelay;
package de.tosox.modbrew;

import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import de.tosox.zonerelay.infrastructure.mo2.Mo2ConfigReader;
import de.tosox.zonerelay.shared.config.UserSettings;
import de.tosox.zonerelay.infrastructure.config.UserSettingsManager;
import de.tosox.zonerelay.domain.port.*;
import de.tosox.zonerelay.infrastructure.download.HttpArchiveDownloader;
import de.tosox.zonerelay.infrastructure.download.source.DirectUrlSource;
import de.tosox.zonerelay.infrastructure.download.source.ModDbUrlSource;
import de.tosox.zonerelay.infrastructure.download.source.UrlSource;
import de.tosox.zonerelay.infrastructure.extraction.SevenZipExtractor;
import de.tosox.zonerelay.infrastructure.install.ModEntryInstaller;
import de.tosox.zonerelay.infrastructure.install.SeparatorInstaller;
import de.tosox.zonerelay.infrastructure.mo2.*;
import de.tosox.zonerelay.infrastructure.persistence.FileInstallProgressStore;
import de.tosox.zonerelay.infrastructure.persistence.YamlModlistRepository;
import de.tosox.zonerelay.shared.i18n.Localizer;
import de.tosox.zonerelay.shared.logging.LogManager;
import de.tosox.zonerelay.shared.logging.Logger;
import de.tosox.zonerelay.shared.config.AppPaths;
import de.tosox.zonerelay.ui.util.ImageLoader;
import de.tosox.modbrew.infrastructure.mo2.Mo2ConfigReader;
import de.tosox.modbrew.shared.config.UserSettings;
import de.tosox.modbrew.infrastructure.config.UserSettingsManager;
import de.tosox.modbrew.domain.port.*;
import de.tosox.modbrew.infrastructure.download.HttpArchiveDownloader;
import de.tosox.modbrew.infrastructure.download.source.DirectUrlSource;
import de.tosox.modbrew.infrastructure.download.source.ModDbUrlSource;
import de.tosox.modbrew.infrastructure.download.source.UrlSource;
import de.tosox.modbrew.infrastructure.extraction.SevenZipExtractor;
import de.tosox.modbrew.infrastructure.install.ModEntryInstaller;
import de.tosox.modbrew.infrastructure.install.SeparatorInstaller;
import de.tosox.modbrew.infrastructure.mo2.*;
import de.tosox.modbrew.infrastructure.persistence.FileInstallProgressStore;
import de.tosox.modbrew.infrastructure.persistence.YamlBrewfileRepository;
import de.tosox.modbrew.shared.i18n.Localizer;
import de.tosox.modbrew.shared.logging.LogManager;
import de.tosox.modbrew.shared.logging.Logger;
import de.tosox.modbrew.shared.config.AppPaths;
import de.tosox.modbrew.ui.util.ImageLoader;

import javax.swing.*;
import java.io.IOException;
Expand All @@ -37,7 +37,7 @@ protected void configure() {
bind(ArchiveExtractor.class).to(SevenZipExtractor.class);
bind(InstallProgressStore.class).to(FileInstallProgressStore.class);
bind(MetaIniWriter.class).to(IniMetaWriter.class);
bind(ModlistRepository.class).to(YamlModlistRepository.class);
bind(BrewfileRepository.class).to(YamlBrewfileRepository.class);
bind(ProfileSetup.class).to(Mo2ProfileSetup.class);
bind(ShortcutCreator.class).to(Mo2ShortcutCreator.class);
bind(SplashImageCopier.class).to(Mo2SplashImageCopier.class);
Expand Down
Loading
Loading