Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
734dbc7
Add experimental RimWorld 1.7 support
mkiplm-svg Oct 19, 2026
c3bfada
Normalize line endings in RimWorld 1.7 port
mkiplm-svg Oct 19, 2026
fb025c5
Avoid CoreCLR stack trace false desyncs
mkiplm-svg Oct 20, 2026
cc02dfd
Fix RimWorld 1.7 reload and runtime state
romangr Oct 25, 2026
2330d38
Remove redundant RimWorld 1.7 build overrides
romangr Oct 25, 2026
1fc7441
Fix RimWorld 1.7 compatibility hashes and solution build
romangr Oct 26, 2026
b8f1268
Restore pawn bio hashing with Waylith compatibility comment
romangr Oct 26, 2026
b014fbf
Fix collection hashing and multiplayer reload state
romangr Oct 26, 2026
6956d17
WIP: checkpoint RimWorld 1.6 and 1.7 compatibility changes
romangr Oct 26, 2026
b9fbdce
Support RimWorld 1.6 and 1.7 with versioned builds
romangr Oct 27, 2026
cad0a8b
Resolve reflection-only Unity dependencies on RimWorld 1.6
romangr Oct 27, 2026
d938e40
Build versioned mod and server artifacts for pull requests
romangr Oct 27, 2026
3c4604a
Add local runtime test runner and centralize .NET SDK setup
romangr Oct 27, 2026
30db9b3
Remove workshop bundler and package supported versions in CI
romangr Oct 27, 2026
b4f32b5
Share mod packaging across GitHub Actions workflows
romangr Oct 27, 2026
ad89d64
Target RimWorld 1.7 only and bundle legacy builds in CI
romangr Oct 28, 2026
d902f2d
Bundle all supported RimWorld versions through a CI matrix
romangr Oct 28, 2026
34c6a6a
Give CI jobs and artifact steps descriptive names
romangr Oct 28, 2026
cf163a2
Separate fast context tracing from slow managed stack capture
romangr Oct 28, 2026
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
122 changes: 122 additions & 0 deletions .github/actions/package-mod/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: Package Multiplayer mod
description: Assemble any number of independently built game versions.
inputs:
destination:
description: Destination mod directory.
required: true
builds-root:
description: Directory containing one build per version, each with About and files.
required: true
versions:
description: JSON list of all required game versions; missing builds fail packaging.
required: true
current-version:
description: Version whose About metadata supplies the package name and description.
required: true
display-name:
description: Optional package display name.
default: ''
stamp-version:
description: Stamp the release version and current source commit.
default: 'false'
runs:
using: composite
steps:
- name: Assemble versioned mod
shell: bash
env:
DESTINATION: ${{ inputs.destination }}
BUILDS_ROOT: ${{ inputs.builds-root }}
VERSIONS: ${{ inputs.versions }}
CURRENT_VERSION: ${{ inputs.current-version }}
DISPLAY_NAME: ${{ inputs.display-name }}
STAMP_VERSION: ${{ inputs.stamp-version }}
run: |
python3 - <<'PYTHON'
from copy import deepcopy
from pathlib import Path
import os
import json
import re
import shutil
import subprocess
import xml.etree.ElementTree as ET

destination = Path(os.environ['DESTINATION'])
builds_root = Path(os.environ['BUILDS_ROOT'])
sources = {p.name: p for p in builds_root.iterdir() if p.is_dir()}
current = os.environ['CURRENT_VERSION']
required = json.loads(os.environ['VERSIONS'])
if len(set(required)) != len(required) or set(sources) != set(required) or current not in sources or any(not re.fullmatch(r'\d+\.\d+', v) for v in sources):
raise SystemExit('Expected version-named build directories including the current version')
versions = sorted(sources, key=lambda value: tuple(map(int, value.split('.'))))
# Validate every independent build before copying any files.
metadata = {}
for version, source in sources.items():
metadata[version] = ET.parse(source / 'About/About.xml').getroot()
if version not in [item.text for item in metadata[version].findall('supportedVersions/li')]:
raise SystemExit(f'Source metadata does not support {version}')
for relative in ('Assemblies/MultiplayerLoader.dll', 'AssembliesCustom/Multiplayer.dll', 'AssembliesCustom/MultiplayerCommon.dll'):
path = source / 'files' / relative
if not path.is_file():
raise SystemExit(f'Missing {version} build output: {path}')
for dll in (source / 'files').rglob('*.dll'):
name = dll.name.lower()
dependency_api = name == '0prepatcherapi.dll'
if name in ('0harmony.dll', 'assembly-csharp.dll', 'verse.dll', 'unitystubs.dll') or name.startswith(('unityengine', 'mono.cecil')) or (name.startswith(('0prepatcher', 'prepatcher')) and not dependency_api):
raise SystemExit(f'Unexpected bundled game or dependency DLL: {dll}')
destination.mkdir(parents=True, exist_ok=False)
shutil.copytree(sources[current] / 'About', destination / 'About')
root = deepcopy(metadata[current])
for tag in ('supportedVersions', 'modDependencies', 'modDependenciesByVersion'):
for old in list(root.findall(tag)):
root.remove(old)
supported = ET.SubElement(root, 'supportedVersions')
dependencies = ET.SubElement(root, 'modDependenciesByVersion')
folders = ET.Element('loadFolders')
for version in versions:
shutil.copytree(sources[version] / 'files', destination / version, ignore=shutil.ignore_patterns('.git'))
ET.SubElement(supported, 'li').text = version
selected = metadata[version].find(f'modDependenciesByVersion/v{version}')
if selected is None:
selected = metadata[version].find('modDependencies')
entry = ET.SubElement(dependencies, f'v{version}')
if selected is not None:
entry.extend(deepcopy(list(selected)))
config_path = sources[version] / 'source.json'
config = json.loads(config_path.read_text()) if config_path.exists() else {}
identifiers = {item.findtext('packageId') for item in entry}
for dependency in config.get('extra-dependencies', []):
if dependency['packageId'] not in identifiers:
item = ET.SubElement(entry, 'li')
for key, value in dependency.items():
ET.SubElement(item, key).text = value
identifiers.add(dependency['packageId'])
folder = ET.SubElement(folders, f'v{version}')
ET.SubElement(folder, 'li').text = '/'
ET.SubElement(folder, 'li').text = version
# Preserve all source ordering requirements and any release dependency additions.
for tag in ('loadAfter', 'loadBefore'):
ordering = root.find(tag)
if ordering is None:
ordering = ET.SubElement(root, tag)
existing = {item.text for item in ordering}
values = [item.text for version in versions for item in metadata[version].findall(f'{tag}/li')]
if tag == 'loadAfter':
values += [item.findtext('packageId') for entry in dependencies for item in entry]
for value in values:
if value and value not in existing:
ET.SubElement(ordering, 'li').text = value
existing.add(value)
if os.environ['DISPLAY_NAME']:
root.find('name').text = os.environ['DISPLAY_NAME']
if os.environ['STAMP_VERSION'] == 'true':
source = Path('Source/Common/Version.cs').read_text(encoding='utf-8')
version = re.search(r'\bSimpleVersion\s*=\s*"([^"]+)"', source)
if version is None:
raise SystemExit('Cannot find SimpleVersion')
commit = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], text=True).strip()
root.find('modVersion').text = f'{version[1]} ({commit})'
ET.ElementTree(root).write(destination / 'About/About.xml', encoding='utf-8', xml_declaration=True)
ET.ElementTree(folders).write(destination / 'LoadFolders.xml', encoding='utf-8', xml_declaration=True)
PYTHON
21 changes: 21 additions & 0 deletions .github/package-versions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"current": "1.7",
"releases": [
{
"version": "1.5",
"branch": "rw-1.5",
"extra-dependencies": [
{
"packageId": "zetrith.prepatcher",
"displayName": "Prepatcher",
"steamWorkshopUrl": "steam://url/CommunityFilePage/2934420800"
}
]
},
{
"version": "1.6",
"branch": "rw-1.6",
"fallback": "9df94bd04ea9a94c00bd39a276e58c4c2f965740"
}
]
}
38 changes: 4 additions & 34 deletions .github/workflows/alpha-notes.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,5 @@
### This 'alpha' release is an automatically generated snapshot of the current state of development. It is continuously updated with work-in-progress changes that may be broken, incomplete, or incompatible.
Development build for RimWorld 1.7, bundled with independently built 1.5 and 1.6 versions.

#### Supported RimWorld versions: latest 1.6

#### Installation
1. Download the `Multiplayer-beta.zip` file below.
2. Open your RimWorld installation directory
* **Steam**: Right-click RimWorld in your Steam library → `Manage` → `Browse local Files`
* Or navigate directly to:
- Windows: `C:\Program Files (x86)\Steam\steamapps\common\RimWorld\Mods`
- Mac: `~/Library/Application Support/Steam/steamapps/common/RimWorld/RimWorldMac.app/Mods` (Right-click on `RimWorldMac.app`, then click `Show Package Contents` to see the `Mods` folder)
- Linux: `~/.steam/steam/steamapps/common/RimWorld/Mods`
3. Extract the zip file into the `Mods` folder.
* You should have a `Multiplayer` folder in the `Mods` folder (`Mods/Multiplayer`)
* Make sure you do not have this directory structure: `Mods/Multiplayer-beta/Multiplayer`. If you do, move the `Multiplayer` folder to the parent directory.

#### Standalone server

Download `Server-beta.zip` if you want to host a dedicated standalone server for testing.

**Setup**
1. Download and extract `Server-beta.zip`.
2. Open the folder for your platform:
- `Server/Windows`
- `Server/Linux`
3. Start the server using:
- Windows: `Server.exe`
- Linux: `./Server.sh`
4. The server will start and wait for the first connection.

**First-time configuration (bootstrap)**
No manual configuration files are required.
The **first player to connect** via the Multiplayer mod client will be prompted to perform the initial setup:
they can configure the game world, scenario, and other options directly from within RimWorld.
Subsequent players connect to the already-running session.
Install the dependencies matching your game version. The 1.5 and 1.6 packages
require Prepatcher; 1.7 requires Harmony 3. Extract the mod ZIP to Mods;
LoadFolders.xml selects matching assets and assemblies. See README.md for source selection.
98 changes: 35 additions & 63 deletions .github/workflows/build-beta.yml
Original file line number Diff line number Diff line change
@@ -1,79 +1,51 @@
name: Build beta

env:
SLN_PATH: Source/

on:
workflow_dispatch:
push:
branches:
- dev
paths-ignore:
- 'README.md'

branches: [dev]
paths-ignore: ['README.md']
permissions:
contents: read
jobs:
build:
name: Build beta
name: Build beta mod and server packages
uses: ./.github/workflows/build-packages.yml
with:
artifact-suffix: beta-${{ github.run_id }}
display-name: Multiplayer [Continuous]
release:
name: Publish beta release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ github.token }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Check out release metadata
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: recursive

- name: Setup Dotnet
uses: actions/setup-dotnet@v4
persist-credentials: false
- name: Download combined mod package
uses: actions/download-artifact@v4
with:
dotnet-version: 9.x

- name: Install Mod Dependencies
run: dotnet restore ${{ env.SLN_PATH }}

- name: Build Mod
run: dotnet build ${{ env.SLN_PATH }} --configuration Release --no-restore

- name: Publish Server (Windows)
run: dotnet publish ${{ env.SLN_PATH }}Server/Server.csproj --configuration Release --runtime win-x64 --self-contained false -p:UseAppHost=true -o output/Server/Windows

- name: Publish Server (Linux)
run: dotnet publish ${{ env.SLN_PATH }}Server/Server.csproj --configuration Release --runtime linux-x64 --self-contained false -p:UseAppHost=true -o output/Server/Linux

- name: Add Server Launchers
run: |
cat > output/Server/Linux/Server.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
exec dotnet "$SCRIPT_DIR/Server.dll" "$@"
EOF

chmod +x output/Server/Linux/Server.sh

- name: Package files
name: ${{ needs.build.outputs.mod-artifact }}
path: output
- name: Download combined server package
uses: actions/download-artifact@v4
with:
name: ${{ needs.build.outputs.server-artifact }}
path: output
- name: Package release ZIPs
working-directory: output
run: |
sed -i "s/<name>\(.*\)<\/name>\$/<name>\1 [Continuous]<\/name>/" About/About.xml
mkdir -p output/Multiplayer
mv About/ Assemblies/ AssembliesCustom/ Defs/ Languages/ Textures/ output/Multiplayer
cd output/
zip -r ../Multiplayer-beta.zip Multiplayer/
zip -r ../Server-beta.zip Server/
cd ../

- name: Upload Mod Artifacts
uses: actions/upload-artifact@v4
with:
name: Multiplayer-beta
path: output/

- name: Clean up last release
- name: Remove previous release
continue-on-error: true
run: gh release delete --cleanup-tag --yes continuous
- name: Publish release
run: |
gh release delete --cleanup-tag --yes "continuous"

- name: Upload new release
run: |
gh release create --target "${{ github.sha }}" --title "Continuous" --notes-file ".github/workflows/alpha-notes.md" --draft "continuous"
gh release upload "continuous" Multiplayer-beta.zip Server-beta.zip
gh release edit "continuous" --draft=false
gh release create --target "${{ github.sha }}" --title Continuous --notes-file .github/workflows/alpha-notes.md --draft continuous
gh release upload continuous Multiplayer-beta.zip Server-beta.zip
gh release edit continuous --draft=false
Loading
Loading