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
140 changes: 30 additions & 110 deletions .github/workflows/docs-check.yaml
Original file line number Diff line number Diff line change
@@ -1,140 +1,60 @@
name: Documentation Check

# Trigger conditions for documentation workflow
on:
# Push events to docs-related paths on active branches
push:
branches:
- main
- develop
- documentation
- 'release/**'
- 'releases/**'
paths:
- '**.md'
- 'docs/**'
- '.github/workflows/docs-check.yaml'
# Pull requests with docs-related changes
- 'tools/sync-doc-metadata.ps1'
pull_request:
branches:
- main
- develop
- documentation
- 'release/**'
- 'releases/**'
paths:
- '**.md'
- 'docs/**'
- '.github/workflows/docs-check.yaml'
- 'tools/sync-doc-metadata.ps1'

env:
DOTNET_VERSION: '9.0.x'
PROJECT_PATH: 'src/'

jobs:
docs-check:
name: Documentation Check
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check for build-affecting changes in documentation branch
if: |
(github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'documentation') ||
(github.event_name == 'push' && github.ref == 'refs/heads/documentation')
run: |
echo "🔍 Checking for build-affecting changes in documentation branch..."
echo ""

# Determine the base reference for comparison
if [ "${{ github.event_name }}" == "pull_request" ]; then
# For pull requests, compare PR head with base
BASE_REF="${{ github.event.pull_request.base.sha }}"
HEAD_REF="${{ github.event.pull_request.head.sha }}"
echo "📋 Comparing PR changes:"
echo " Base: $BASE_REF"
echo " Head: $HEAD_REF"
else
# For push events, compare with the previous commit
BASE_REF="HEAD^"
HEAD_REF="HEAD"
echo "📋 Comparing push changes:"
echo " Base: $BASE_REF"
echo " Head: $HEAD_REF"
fi
echo ""

# Get all changed files
CHANGED_FILES=$(git diff --name-only "$BASE_REF" "$HEAD_REF" 2>&1)

if [ $? -ne 0 ]; then
echo "⚠️ Warning: git diff command failed"
echo "$CHANGED_FILES"
exit 1
fi

if [ -z "$CHANGED_FILES" ]; then
echo "ℹ️ No files changed"
exit 0
fi

echo "📝 Changed files:"
echo "$CHANGED_FILES"
echo ""

# Define patterns for build-affecting files
# These are files that should NOT be changed in a documentation-only PR
BUILD_AFFECTING_PATTERNS=(
"^src/"
"^GitVersion\.yml$"
)

BLOCKED_FILES=""
while IFS= read -r file; do
[ -z "$file" ] && continue
is_blocked=false
for pattern in "${BUILD_AFFECTING_PATTERNS[@]}"; do
if echo "$file" | grep -qE "$pattern"; then
is_blocked=true
break
fi
done

if echo "$file" | grep -qE "^\.github/workflows/" && ! echo "$file" | grep -qE "^\.github/workflows/docs-"; then
is_blocked=true
fi

if [ "$is_blocked" = true ]; then
BLOCKED_FILES="$BLOCKED_FILES$file"$'\n'
fi
done <<< "$CHANGED_FILES"

if [ -n "$BLOCKED_FILES" ]; then
echo "❌ ERROR: Found build-affecting changes in documentation branch!"
echo ""
echo "The following files affect the build and should not be modified in PRs to the documentation branch:"
echo "$BLOCKED_FILES"
echo ""
echo "The documentation branch should only contain:"
echo " - Markdown files (*.md)"
echo " - Files in docs/ directory"
echo " - Documentation workflow files (docs-*.yaml)"
echo ""
exit 1
fi

echo "✅ All changes are documentation-only"

- name: Documentation validation
run: |
echo "📚 Documentation Check"
echo "Branch: ${{ github.ref }}"
echo "Event: ${{ github.event_name }}"
echo ""
echo "✅ Documentation files detected and validated"
echo ""
echo "Future enhancements:"
echo "- Markdown lint validation"
echo "- Link validation"
echo "- Documentation coverage checks"
echo "- Build documentation site"
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Restore dependencies
run: dotnet restore ${{ env.PROJECT_PATH }}

- name: Check docs metadata sync
shell: pwsh
run: ./tools/sync-doc-metadata.ps1 -Mode check

- name: Documentation validation summary
run: |
echo "📚 Documentation Check"
echo "Branch: ${{ github.ref }}"
echo "Event: ${{ github.event_name }}"
echo ""
echo "✅ Documentation metadata is in sync"
25 changes: 7 additions & 18 deletions GitVersion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ next-version: 'v0.2.1'

# Branch-specific configuration
branches:
# Main branch: Production releases
# Main branch: Stable promotions
main:
# Prevent version jumping when merging from develop
# Version increments based on main's own version, not the merged branch
Expand All @@ -38,7 +38,7 @@ branches:
regex: ^master$|^main$
source-branches: ['develop', 'hotfix']

# Develop branch: Integration branch for ongoing development
# Develop branch: Daily integration
develop:
# Allow normal version increment from merged branches
prevent-increment:
Expand All @@ -52,33 +52,22 @@ branches:
source-branches: ['feature', 'bugfix', 'hotfix']
track-merge-target: false

# Documentation branch: Documentation maintenance
documentation:
# Don't affect main version when merging documentation
prevent-increment:
when-branch-merged: true
increment: Patch
# Docs tag for documentation releases
label: docs
regex: ^docs?$|^documentation$
source-branches: []

# Feature branches: New features under development
# Feature branches: Optional short-lived feature work
feature:
increment: Minor
# Alpha tag for feature development
label: 'alpha.{BranchName}.{number}'
regex: ^features?[/-](?<number>\d+)
source-branches: ['develop']

# Bugfix branches: Bug fixes under development
# Bugfix branches: Optional short-lived bugfix work
bugfix:
increment: Patch
label: 'bugfix.{BranchName}'
regex: ^bug(fix)?[/-]
source-branches: ['develop']

# Hotfix branches: Emergency fixes from production
# Hotfix branches: Exceptional fixes based on main
hotfix:
increment: Patch
label: hotfix
Expand All @@ -93,12 +82,12 @@ branches:
# Use PR number in version (e.g., 1.1.0-pr.123)
regex: ^(pull|pull\-requests|pr)[/-](?<number>\d+)

# Release branches: Release preparation
# Release branches: Release execution from main
release:
increment: None
label: rc
regex: ^releases?[/-]
source-branches: ['develop', 'main']
source-branches: ['main']
is-main-branch: false
prevent-increment:
when-branch-merged: true
Expand Down
49 changes: 35 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</p>

<p align="center">
<em>Host multiple Terraria worlds in one launcher process,<br>keep worlds isolated, and keep extending behavior with plugins and publisher tooling on OTAPI USP.</em>
<em>Run multiple Terraria worlds from one launcher,<br>keep each world in its own context, and handle routing, data sharing, and plugin-driven extension inside the same OTAPI USP runtime.</em>
</p>

---
Expand Down Expand Up @@ -51,15 +51,14 @@

UnifierTSL wraps [OTAPI Unified Server Process](https://github.com/CedaryCat/OTAPI.UnifiedServerProcess) into a runtime you can run directly to host **multiple Terraria worlds in one launcher process**.

The launcher handles world lifecycle, player join routing, and spins up a dedicated console client per world context so each world's I/O stays separate.
Those console sessions now use an ANSI-safe prompt/status protocol, so launcher and per-world consoles can keep semantic readline state, replay status frames, and recover cleanly after reconnects instead of acting like plain text pipes.
Compared with classic single-world servers or packet-routed multi-process world stacks, Unifier keeps join routing, world handoff, and extension hooks in one runtime surface instead of scattering that logic across process boundaries.
In traditional multi-process multi-world stacks, building a cluster of cooperating worlds usually means extra cross-process routing, state synchronization, and serialization design. Moving players between instances often relies on packet relays and side channels; when plugin-attached data, temporary state, or runtime objects need to cross worlds, problems that could otherwise stay in-process often have to be rewritten as protocols and synchronization flows.

Compared with approaches that push this coordination outside process boundaries, Unifier, based on OTAPI USP, keeps join routing, world switching, and extension hooks inside the same runtime plane and treats cross-world coordination as a first-class concern from the start. The launcher manages multi-world lifecycle centrally, lets each world run independently and in parallel in its own `ServerContext`, and provides a dedicated console per world so I/O stays isolated.
`UnifiedServerCoordinator` handles coordination, `UnifierApi.EventHub` carries event traffic, and `PluginHost.PluginOrchestrator` runs plugin hosting.
With shared connection and state surfaces, you can operate worlds together and build tighter cross-world interactions, while policy-based routing and transfer hooks still leave room for world-level fallback behavior.
This shared listener-and-coordination model reduces the extra overhead and complexity introduced by cross-process relays, making cross-world interaction, data interchange, and unified operations easier while still leaving enough routing control to define the default join target and take over later world-switch flows.

If you push this model further, you can build more gameplay-driven setups: fully connected multi-instance world clusters, elastic worlds that load or unload region-sized shards on demand, or private worlds tuned per player for logic and resource budgets.
These are achievable directions, not out-of-the-box defaults.
Some heavier implementations may stay outside launcher core, but you can expect practical sample plugins for these patterns to land over time in the `plugins/` ecosystem.
From the player's side, this still behaves like a normal Terraria entry point: clients connect to one shared listener port, and `UnifiedServerCoordinator` routes each connection to the selected world inside the same process. If you push this model further, you can build more gameplay-driven setups: fully connected multi-instance world clusters, elastic worlds that load or unload region-sized shards on demand, or private worlds tuned per player for logic and resource budgets.
These are reachable directions, even though the launcher does not currently ship them as default out-of-the-box features, and heavier implementations like these may stay out of the launcher core itself; you can still expect usable example plugins to land under `plugins/` over time.

---

Expand All @@ -75,7 +74,7 @@ Some heavier implementations may stay outside launcher core, but you can expect
| 📦 **Collectible module contexts** | `ModuleLoadContext` gives you unloadable plugin domains and staged dependency handling |
| 📝 **Shared logging pipeline** | `UnifierApi.LogCore` supports custom filters, writers, and metadata injectors |
| 🛡 **Bundled TShock port** | Ships with a USP-adapted TShock baseline ready for use |
| 💻 **Per-context console isolation** | Named-pipe console sessions with ANSI-safe logs, semantic readline prompts, and live status bars per world context |
| 💻 **Per-context console isolation** | Independent, auto-reconnecting console I/O windows for each world context, plus semantic readline prompts and live status bars |
| 🚀 **RID-targeted publishing** | Publisher produces reproducible, runtime-specific directory trees |

---
Expand Down Expand Up @@ -124,7 +123,7 @@ Additional dependency baselines:
<img src="./docs/assets/readme/arch-flow.svg" alt="Architecture flow" width="100%">
</p>

Actual runtime startup flow:
If you want the real boot order, it looks like this:

1. `Program.Main` initializes assembly resolver, applies pre-run CLI language overrides, and prints runtime version details.
2. `Initializer.Initialize()` prepares Terraria/USP runtime state and loads core hooks (`UnifiedNetworkPatcher`, `UnifiedServerCoordinator`, `ServerContext` setup).
Expand All @@ -148,6 +147,8 @@ Actual runtime startup flow:

### Pick Your Path

If you already know why you're here, jump in from the track that matches your role:

| Role | Start Here | Why |
|:--|:--|:--|
| 🖥 Server operator | [Quick Start ↓](#quick-start) | Bring up a usable multi-world host with minimal setup |
Expand All @@ -158,6 +159,8 @@ Actual runtime startup flow:
<a id="quick-start"></a>
## 🚀 Quick Start

If your main goal is "get a launcher up and see worlds come online," start here.

### Prerequisites

Choose the requirement set that matches how you plan to run UnifierTSL:
Expand All @@ -169,6 +172,8 @@ Choose the requirement set that matches how you plan to run UnifierTSL:

### Option A: Use a Release Bundle

If you just want to run it, this is the shortest path.

**1.** Download the release asset that matches your platform from [GitHub Releases](https://github.com/CedaryCat/UnifierTSL/releases):

| Platform | File pattern |
Expand Down Expand Up @@ -209,7 +214,7 @@ chmod +x UnifierTSL

### Option B: Run from Source

Use this path for local debugging, CI integration, or custom bundle output.
Take this path if you want local debugging, CI integration, or your own Publisher output.

**1.** Clone and restore:

Expand Down Expand Up @@ -253,6 +258,23 @@ dotnet run --project src/UnifierTSL/UnifierTSL.csproj -- \
3. Switch startup project to `UnifierTSL`, choose the `Executable` launch profile, and start debugging.
4. That profile runs the published launcher from `utsl-publish` and debugs the published program directly.

### What Happens on First Boot

- On the first successful launch, `config/config.json` is created automatically and stores the effective launcher startup snapshot. CLI arguments still win for the launch you are doing right now.
- Plugin configs live under `config/<PluginName>/`. For the bundled TShock port, that root is `config/TShockAPI/`; it also stores other TShock runtime files such as `tshock.sqlite` when SQLite is enabled, so in practice it fills the same role as the standalone TShock `tshock/` directory.
- Published bundles start with a flat `plugins/` directory. During startup, the module loader may reshuffle modules into subfolders when dependency or core-module metadata says it should.
- If everything went well, you should see the shared listener bind, the configured worlds start, the launcher status output begin updating, and, under the default console I/O implementation, one dedicated console window appear for each world.

### Bundled TShock Notes

- The bundled TShock here is a migration for the UnifierTSL / OTAPI USP runtime. Its lower-level logic is reimplemented by prioritizing UTSL/USP-native runtime APIs, event surfaces, packet models, and similar built-in capabilities, without maintaining an extra compatibility layer, while still aiming to keep the behavior and operator experience of TShock's higher-level features as close to upstream TShock as possible within a multi-world, single-process runtime model.
- This port is maintained to keep tracking upstream TShock. You can inspect the current migration baseline directly in `src/Plugins/TShockAPI/TShockAPI.csproj`, especially `MainlineSyncBranch`, `MainlineSyncCommit`, and `MainlineVersion`.
- Launcher settings stay in `config/config.json`, while the bundled TShock uses its own config-and-data root under `config/TShockAPI/`, separate from the launcher root config. This is also where other TShock runtime files live, such as `tshock.sqlite` when SQLite is enabled, so this directory effectively plays the same role as the standalone TShock `tshock/` folder.
- `config/TShockAPI/config.json` holds global TShock defaults, while `config/TShockAPI/config.override.json` stores per-server override patches keyed by configured server name, for example `"S1": { "MaxSlots": 16 }`. `config/TShockAPI/sscconfig.json` remains a separate file for SSC settings.
- Because the runtime hosts multiple worlds at once, some TShock data access that is usually implicit in a single-world flow becomes explicit here; for example, warp-related code paths resolve entries with an explicit `worldId` instead of only relying on the current global world state.
- Editing `config.json` or `config.override.json` externally updates the watched config handles and reapplies runtime TShock server settings. `/reload` still matters because it additionally refreshes permissions, regions, bans, whitelist-backed state, and the classic TShock reload flow. Some changes still require a restart.
- Finally, thanks to the TShock project and its contributors for the functionality, design work, and ecosystem this migration builds upon.

---

<a id="launcher-reference"></a>
Expand Down Expand Up @@ -533,6 +555,8 @@ This table reflects the currently maintained/documented packaging targets, not e
|:--|:--|
| Developer Overview | [docs/dev-overview.md](./docs/dev-overview.md) |
| Plugin Development Guide | [docs/dev-plugin.md](./docs/dev-plugin.md) |
| Branch Workflow Guide | [docs/branch-setup-guide.md](./docs/branch-setup-guide.md) |
| Branch Workflow Quick Reference | [docs/branch-strategy-quick-reference.md](./docs/branch-strategy-quick-reference.md) |
| OTAPI Unified Server Process | [GitHub](https://github.com/CedaryCat/OTAPI.UnifiedServerProcess) |
| Upstream TShock | [GitHub](https://github.com/Pryaxis/TShock) |
| DeepWiki AI Analysis | [deepwiki.com](https://deepwiki.com/CedaryCat/UnifierTSL) *(reference only)* |
Expand All @@ -542,6 +566,3 @@ This table reflects the currently maintained/documented packaging targets, not e
<p align="center">
<sub>Made with ❤️ by the UnifierTSL contributors · Licensed under GPL-3.0</sub>
</p>



Loading
Loading