mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
ci: build the PC and Quest clients on Jenkins
A Jenkinsfile builds, on push (only what changed) or on demand: - the PC patcher zip (MelonLoader, the mod, config template, Steam stub), - the full PC client (stock build + patcher + config), - the Quest APK and .xapk. Stock builds come out of the restic build archive on the FTP: restic through its rclone backend, picked by snapshot tag per run and cached. The FTP login and restic password are Jenkins credentials, and the repository folders are environment settings, so nothing about the archive is fixed in the repo. ci/jenkins/Dockerfile is the Jenkins image with the toolchain added (.NET SDK, Android NDK and build-tools, apktool, Python with LIEF, restic, rclone), for the existing Jenkins service. The build scripts move from PowerShell to Python so they run the same on Windows, Linux and in CI: - DorkNet.Quest/scripts/build.py replaces build.ps1 + package.ps1; it can use a committed hook table (checked against the APK's libil2cpp.so) instead of dump.cs, and takes a persistent keystore. - tools/build_pc.py replaces build-patcher-zip.ps1 (now with the 2023 install layout) and adds the full-client build. Setup steps are in ci/jenkins/README.md.
This commit is contained in:
@@ -1,157 +0,0 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Builds the per-version dorknet-clientpatch-{VersionKey}.zip the
|
||||
DorkNet launcher expects to find as a GitHub Release asset.
|
||||
|
||||
.DESCRIPTION
|
||||
Output zip layout (see launcher/RELEASES.md on the public main
|
||||
branch for the full contract):
|
||||
|
||||
manifest.json (schema v1, describes the install)
|
||||
MelonLoader.zip (downloaded from melonloader github)
|
||||
DorkNet.ClientMod.dll (built from this repo)
|
||||
dorknet-clientmod.json.template (copied from DorkNet.ClientMod/)
|
||||
|
||||
The launcher reads manifest.json, unzips MelonLoader on top of the
|
||||
user's Rec Room install root, copies the DLL into Mods/, then
|
||||
renders the config template into UserData/.
|
||||
|
||||
Self-contained: downloads MelonLoader before the dotnet build and
|
||||
feeds its DLLs into the build as references, so no local Rec Room
|
||||
install is required to compile DorkNet.ClientMod. (The mod resolves
|
||||
every game-side type at runtime via AccessTools.TypeByName.)
|
||||
|
||||
.PARAMETER VersionKey
|
||||
The version key matching versions.json on the public main branch
|
||||
(e.g. december_2020_12_18 or march_2020_03_10).
|
||||
|
||||
.PARAMETER MelonLoaderVersion
|
||||
MelonLoader release tag to bundle. Defaults to the 0.6.x stable
|
||||
target — that's the loader DorkNet.ClientMod compiled against.
|
||||
|
||||
.PARAMETER OutputDir
|
||||
Where the final zip lands. Defaults to repo-root/dist.
|
||||
|
||||
.EXAMPLE
|
||||
.\tools\build-patcher-zip.ps1 -VersionKey december_2020_12_18
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$VersionKey,
|
||||
[string]$MelonLoaderVersion = "v0.6.6",
|
||||
[string]$OutputDir = $null
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = Resolve-Path "$PSScriptRoot\.."
|
||||
if (-not $OutputDir) { $OutputDir = Join-Path $repoRoot 'dist' }
|
||||
New-Item -ItemType Directory -Force $OutputDir | Out-Null
|
||||
|
||||
$stage = Join-Path $env:TEMP "dorknet-patcher-$VersionKey-$([guid]::NewGuid().Guid.Substring(0,8))"
|
||||
New-Item -ItemType Directory -Force $stage | Out-Null
|
||||
try {
|
||||
# ── 1. Fetch MelonLoader from upstream releases ──────────────────────
|
||||
# We download it first so the DLLs inside can serve as compile-time
|
||||
# references for DorkNet.ClientMod — keeps the build self-contained
|
||||
# (no local Rec Room install required to compile the mod).
|
||||
# x64 .NET 6 build targets 2020-era Rec Room. The asset name pattern
|
||||
# is stable across 0.6.x: MelonLoader.x64.zip.
|
||||
$mlAsset = "MelonLoader.x64.zip"
|
||||
$mlUrl = "https://github.com/LavaGang/MelonLoader/releases/download/$MelonLoaderVersion/$mlAsset"
|
||||
$mlZip = "$stage\MelonLoader.zip"
|
||||
$mlExtract = "$stage\melonloader-extracted"
|
||||
Write-Host "[fetch] downloading $mlUrl"
|
||||
Invoke-WebRequest -Uri $mlUrl -OutFile $mlZip
|
||||
if (-not (Test-Path $mlZip) -or (Get-Item $mlZip).Length -lt 1MB) {
|
||||
throw "MelonLoader download failed or produced an unexpectedly small file"
|
||||
}
|
||||
Expand-Archive -Path $mlZip -DestinationPath $mlExtract -Force
|
||||
# The 0.6.x release zip layout has a top-level `MelonLoader` folder
|
||||
# containing net6, net35, Dependencies, Documentation. Point the
|
||||
# csproj at that inner folder.
|
||||
$mlRefRoot = Join-Path $mlExtract 'MelonLoader'
|
||||
if (-not (Test-Path "$mlRefRoot\net6\MelonLoader.dll")) {
|
||||
throw "Extracted MelonLoader is missing net6\MelonLoader.dll — release layout may have changed"
|
||||
}
|
||||
|
||||
# ── 2. Build DorkNet.ClientMod (no Rec Room install needed) ─────────
|
||||
Write-Host "[build] dotnet build DorkNet.ClientMod (Release)"
|
||||
dotnet build "$repoRoot\DorkNet.ClientMod\DorkNet.ClientMod.csproj" `
|
||||
-c Release --nologo `
|
||||
"-p:MelonLoaderDir=$mlRefRoot" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "ClientMod build failed" }
|
||||
$dll = Get-ChildItem "$repoRoot\DorkNet.ClientMod\bin\Release" -Recurse `
|
||||
-Filter "DorkNet.ClientMod.dll" |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
if (-not $dll) { throw "DorkNet.ClientMod.dll not found post-build" }
|
||||
Copy-Item $dll.FullName "$stage\DorkNet.ClientMod.dll"
|
||||
Write-Host "[build] copied $($dll.FullName)"
|
||||
|
||||
# ── 3. Copy the config template ──────────────────────────────────────
|
||||
$template = "$repoRoot\DorkNet.ClientMod\dorknet-clientmod.json.template"
|
||||
if (-not (Test-Path $template)) {
|
||||
throw "Config template missing: $template"
|
||||
}
|
||||
Copy-Item $template "$stage\dorknet-clientmod.json.template"
|
||||
|
||||
# ── 4. Copy the Steam stub (Goldberg) + appid ────────────────────────
|
||||
# Replaces the Valve steam_api64.dll so the unwrapped exe can run
|
||||
# without Steam loaded. App ID 471710 is Rec Room — same across all
|
||||
# versions we ship for.
|
||||
$stubDll = "$repoRoot\DorkNet.ClientMod\steam-stub\steam_api64.dll"
|
||||
$stubAppid = "$repoRoot\DorkNet.ClientMod\steam-stub\steam_appid.txt"
|
||||
if (-not (Test-Path $stubDll)) {
|
||||
throw "Steam stub DLL missing: $stubDll"
|
||||
}
|
||||
if (-not (Test-Path $stubAppid)) {
|
||||
throw "Steam appid file missing: $stubAppid"
|
||||
}
|
||||
Copy-Item $stubDll "$stage\steam_api64.dll"
|
||||
Copy-Item $stubAppid "$stage\steam_appid.txt"
|
||||
|
||||
# ── 5. Write manifest.json ──────────────────────────────────────────
|
||||
$manifest = @{
|
||||
'$schema_version' = 1
|
||||
'loader_archive' = 'MelonLoader.zip'
|
||||
'plugin_dll' = 'DorkNet.ClientMod.dll'
|
||||
'plugin_dest' = 'Mods'
|
||||
'config_template' = 'dorknet-clientmod.json.template'
|
||||
'config_dest' = 'MelonLoader/UserData/dorknet-clientmod.json'
|
||||
'old_plugin_paths' = @(
|
||||
'BepInEx/plugins/DorkNet.ClientPatch.dll'
|
||||
)
|
||||
'steam_stub' = @{
|
||||
# 2020.12+ is an EAC build: the real Unity binary is
|
||||
# RecRoom.exe (loads RecRoom_Data/Plugins/x86_64/steam_api64.dll),
|
||||
# NOT Recroom_Release.exe (which is the EAC launcher wrapper
|
||||
# that loads Recroom_Release_Data/Plugins/steam_api64.dll).
|
||||
# Patching the wrapper's DLL did nothing because the
|
||||
# launcher's data dir is never opened by the real game
|
||||
# process — symptom was "Failed to initialize Steam
|
||||
# platform" because the unpatched real Steam DLL in
|
||||
# RecRoom_Data tried to handshake.
|
||||
'api_dll' = 'steam_api64.dll'
|
||||
'api_dest' = 'RecRoom_Data/Plugins/x86_64/steam_api64.dll'
|
||||
'api_backup_suffix' = '.steam-original'
|
||||
'appid_file' = 'steam_appid.txt'
|
||||
'appid_dest' = 'steam_appid.txt'
|
||||
}
|
||||
} | ConvertTo-Json -Depth 4
|
||||
Set-Content -LiteralPath "$stage\manifest.json" -Value $manifest
|
||||
|
||||
# ── 6. Bundle into the final zip ────────────────────────────────────
|
||||
# Strip the extracted MelonLoader scratch dir before zipping so it
|
||||
# doesn't bloat the artifact. The MelonLoader.zip itself stays.
|
||||
Remove-Item -Recurse -Force $mlExtract
|
||||
$outZip = Join-Path $OutputDir "dorknet-clientpatch-$VersionKey.zip"
|
||||
if (Test-Path $outZip) { Remove-Item $outZip -Force }
|
||||
Compress-Archive -Path "$stage\*" -DestinationPath $outZip
|
||||
$sizeMb = [math]::Round((Get-Item $outZip).Length / 1MB, 1)
|
||||
Write-Host ""
|
||||
Write-Host "Built $outZip ($sizeMb MB)" -ForegroundColor Green
|
||||
Write-Host " Upload this to the v1-* GitHub Release on this branch."
|
||||
}
|
||||
finally {
|
||||
if (Test-Path $stage) { Remove-Item -Recurse -Force $stage }
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the DorkNet PC client packages.
|
||||
|
||||
patcher dorknet-clientpatch-<VersionKey>.zip, what the DorkNet launcher
|
||||
installs: MelonLoader.zip, DorkNet.ClientMod.dll (built from this
|
||||
repo), the config template, the Goldberg Steam stub and a
|
||||
manifest.json saying where each piece goes. No game files.
|
||||
|
||||
client a ready-to-run client: a stock Rec Room build with a patcher zip
|
||||
applied the way the launcher applies it, plus your config. It
|
||||
contains the game's own files, so keep it private.
|
||||
|
||||
Runs on Windows, Linux and macOS. `patcher` needs the .NET SDK (the mod
|
||||
builds against MelonLoader's own DLLs, so no Rec Room install is needed).
|
||||
|
||||
py -3 tools/build_pc.py patcher --version-key march_2023_03_21
|
||||
py -3 tools/build_pc.py client --base-client RecRoom-2023-03-21.zip \\
|
||||
--patcher-zip dist/dorknet-clientpatch-march_2023_03_21.zip \\
|
||||
--config dorknet-clientmod.json
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CLIENT_MOD = REPO / "DorkNet.ClientMod"
|
||||
|
||||
# Where each build expects the mod's config and the Steam DLL the game actually
|
||||
# loads (see the per-branch build layout notes in the README):
|
||||
# 2020.03 Recroom_Release.exe is the game; Steam DLL in its Data/Plugins.
|
||||
# 2020.12 RecRoom.exe is the game (Recroom_Release.exe is the EAC wrapper);
|
||||
# MelonLoader keeps UserData under MelonLoader/.
|
||||
# 2023.03 RecRoom.exe + RecRoom_Data; MelonLoader 0.6 keeps UserData at the
|
||||
# install root.
|
||||
LAYOUTS = {
|
||||
"march_2020_03_10": ("MelonLoader/UserData/dorknet-clientmod.json", "Recroom_Release_Data/Plugins/steam_api64.dll"),
|
||||
"december_2020_12_18": ("MelonLoader/UserData/dorknet-clientmod.json", "RecRoom_Data/Plugins/x86_64/steam_api64.dll"),
|
||||
"march_2023_03_21": ("UserData/dorknet-clientmod.json", "RecRoom_Data/Plugins/x86_64/steam_api64.dll"),
|
||||
}
|
||||
|
||||
|
||||
def step(msg: str) -> None:
|
||||
print(f"==> {msg}", flush=True)
|
||||
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
raise SystemExit(f"error: {msg}")
|
||||
|
||||
|
||||
def need(path: Path | str, what: str) -> Path:
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
fail(f"{what} not found: {p}")
|
||||
return p
|
||||
|
||||
|
||||
def zip_dir(src: Path, out: Path) -> None:
|
||||
"""Zip the contents of src (not src itself) into out."""
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.unlink(missing_ok=True)
|
||||
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, allowZip64=True) as z:
|
||||
for f in sorted(src.rglob("*")):
|
||||
if f.is_file():
|
||||
z.write(f, f.relative_to(src).as_posix())
|
||||
|
||||
|
||||
# ---- patcher --------------------------------------------------------------
|
||||
|
||||
def cmd_patcher(a: argparse.Namespace) -> None:
|
||||
if a.version_key not in LAYOUTS:
|
||||
fail(f"unknown version key '{a.version_key}'. Known: {', '.join(LAYOUTS)}")
|
||||
config_dest, steam_dest = LAYOUTS[a.version_key]
|
||||
out_dir = Path(a.output_dir) if a.output_dir else REPO / "dist"
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix=f"dorknet-patcher-{a.version_key}-") as tmp:
|
||||
stage = Path(tmp) / "stage"
|
||||
stage.mkdir()
|
||||
|
||||
# 1. MelonLoader. Downloaded first so its DLLs can serve as the mod's
|
||||
# compile-time references: no local Rec Room install needed.
|
||||
url = f"https://github.com/LavaGang/MelonLoader/releases/download/{a.melonloader_version}/MelonLoader.x64.zip"
|
||||
ml_zip = stage / "MelonLoader.zip"
|
||||
step(f"downloading {url}")
|
||||
with urllib.request.urlopen(url) as r, open(ml_zip, "wb") as f:
|
||||
shutil.copyfileobj(r, f)
|
||||
if ml_zip.stat().st_size < 1 << 20:
|
||||
fail("MelonLoader download failed or produced an unexpectedly small file")
|
||||
ml_extract = Path(tmp) / "melonloader"
|
||||
with zipfile.ZipFile(ml_zip) as z:
|
||||
z.extractall(ml_extract)
|
||||
# The 0.6.x release zip has a top-level MelonLoader/ folder holding
|
||||
# net6, net35, Dependencies, Documentation.
|
||||
ml_ref = ml_extract / "MelonLoader"
|
||||
if not (ml_ref / "net6" / "MelonLoader.dll").exists():
|
||||
fail("extracted MelonLoader is missing net6/MelonLoader.dll; the release layout may have changed")
|
||||
|
||||
# 2. the mod
|
||||
step("dotnet build DorkNet.ClientMod (Release)")
|
||||
r = subprocess.run(["dotnet", "build", str(CLIENT_MOD / "DorkNet.ClientMod.csproj"),
|
||||
"-c", "Release", "--nologo", f"-p:MelonLoaderDir={ml_ref}"])
|
||||
if r.returncode:
|
||||
fail("ClientMod build failed")
|
||||
dlls = sorted((CLIENT_MOD / "bin" / "Release").rglob("DorkNet.ClientMod.dll"),
|
||||
key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if not dlls:
|
||||
fail("DorkNet.ClientMod.dll not found after the build")
|
||||
shutil.copy2(dlls[0], stage / "DorkNet.ClientMod.dll")
|
||||
print(f" built {dlls[0]}")
|
||||
|
||||
# 3. config template + 4. Goldberg Steam stub (App ID 471710, the same
|
||||
# for every build). The stub replaces Valve's steam_api64.dll so the
|
||||
# game runs without Steam.
|
||||
for src, what in ((CLIENT_MOD / "dorknet-clientmod.json.template", "config template"),
|
||||
(CLIENT_MOD / "steam-stub" / "steam_api64.dll", "Steam stub DLL"),
|
||||
(CLIENT_MOD / "steam-stub" / "steam_appid.txt", "Steam appid file")):
|
||||
shutil.copy2(need(src, what), stage / src.name)
|
||||
|
||||
# 5. manifest.json, read by the launcher and by `client` below.
|
||||
manifest = {
|
||||
"$schema_version": 1,
|
||||
"loader_archive": "MelonLoader.zip",
|
||||
"plugin_dll": "DorkNet.ClientMod.dll",
|
||||
"plugin_dest": "Mods",
|
||||
"config_template": "dorknet-clientmod.json.template",
|
||||
"config_dest": config_dest,
|
||||
"old_plugin_paths": ["BepInEx/plugins/DorkNet.ClientPatch.dll"],
|
||||
"steam_stub": {
|
||||
# 2020.12+ is an EAC build: the real Unity binary is RecRoom.exe
|
||||
# (it loads RecRoom_Data/Plugins/x86_64/steam_api64.dll), NOT
|
||||
# Recroom_Release.exe (the EAC launcher wrapper, whose data dir
|
||||
# the real game never opens).
|
||||
"api_dll": "steam_api64.dll",
|
||||
"api_dest": steam_dest,
|
||||
"api_backup_suffix": ".steam-original",
|
||||
"appid_file": "steam_appid.txt",
|
||||
"appid_dest": "steam_appid.txt",
|
||||
},
|
||||
}
|
||||
(stage / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
||||
|
||||
# 6. bundle
|
||||
out = out_dir / f"dorknet-clientpatch-{a.version_key}.zip"
|
||||
zip_dir(stage, out)
|
||||
print()
|
||||
print(f"Built {out} ({out.stat().st_size / 2**20:.1f} MB)")
|
||||
|
||||
|
||||
# ---- client ---------------------------------------------------------------
|
||||
|
||||
def cmd_client(a: argparse.Namespace) -> None:
|
||||
base = need(a.base_client, "stock client")
|
||||
patcher_zip = need(a.patcher_zip, "patcher zip")
|
||||
config = need(a.config, "client config") if a.config else None
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="dorknet-pc-client-") as tmp:
|
||||
tmp = Path(tmp)
|
||||
install, patcher = tmp / "install", tmp / "patcher"
|
||||
patcher.mkdir()
|
||||
|
||||
# 1. stock client: a zip or a folder, the install at its root or
|
||||
# inside one wrapping folder.
|
||||
step(f"unpacking stock client {base}")
|
||||
if base.is_dir():
|
||||
unpacked = base
|
||||
else:
|
||||
unpacked = tmp / "base"
|
||||
with zipfile.ZipFile(base) as z:
|
||||
z.extractall(unpacked)
|
||||
root = unpacked
|
||||
top = list(root.iterdir())
|
||||
if len(top) == 1 and top[0].is_dir():
|
||||
root = top[0]
|
||||
games = [p.name for p in root.glob("*.exe") if (root / f"{p.stem}_Data").is_dir()]
|
||||
if not games:
|
||||
fail(f"no game exe with a matching _Data folder at the root of {base}")
|
||||
print(f" game: {', '.join(games)}")
|
||||
shutil.copytree(root, install, ignore=shutil.ignore_patterns(".DepotDownloader"))
|
||||
|
||||
# 2. apply the patcher the way the launcher does, from its manifest.
|
||||
step(f"applying {patcher_zip}")
|
||||
with zipfile.ZipFile(patcher_zip) as z:
|
||||
z.extractall(patcher)
|
||||
m = json.loads(need(patcher / "manifest.json", "patcher manifest.json").read_text(encoding="utf-8"))
|
||||
|
||||
with zipfile.ZipFile(need(patcher / m["loader_archive"], "MelonLoader archive")) as z:
|
||||
z.extractall(install)
|
||||
print(" MelonLoader -> install root")
|
||||
|
||||
plugin_dir = install / m["plugin_dest"]
|
||||
plugin_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(patcher / m["plugin_dll"], plugin_dir / m["plugin_dll"])
|
||||
print(f" {m['plugin_dll']} -> {m['plugin_dest']}/")
|
||||
|
||||
config_dest = install / m["config_dest"]
|
||||
config_dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(config or patcher / m["config_template"], config_dest)
|
||||
print(f" config -> {m['config_dest']}{'' if config else ' (template, unedited)'}")
|
||||
|
||||
stub = m.get("steam_stub")
|
||||
if stub:
|
||||
api_dest = install / stub["api_dest"]
|
||||
if api_dest.exists():
|
||||
backup = api_dest.with_name(api_dest.name + stub["api_backup_suffix"])
|
||||
if not backup.exists():
|
||||
shutil.copy2(api_dest, backup)
|
||||
api_dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(patcher / stub["api_dll"], api_dest)
|
||||
shutil.copy2(patcher / stub["appid_file"], install / stub["appid_dest"])
|
||||
print(f" Steam stub -> {stub['api_dest']}")
|
||||
|
||||
for old in m.get("old_plugin_paths") or []:
|
||||
p = install / old
|
||||
if p.exists():
|
||||
p.unlink()
|
||||
print(f" removed old {old}")
|
||||
|
||||
# 3. zip
|
||||
key = patcher_zip.stem.removeprefix("dorknet-clientpatch-")
|
||||
out = Path(a.output_zip) if a.output_zip else REPO / "dist" / f"dorknet-client-{key}.zip"
|
||||
step(f"zipping -> {out}")
|
||||
zip_dir(install, out)
|
||||
print()
|
||||
print(f"Built {out} ({out.stat().st_size / 2**20:.1f} MB). It contains game files: keep it private.")
|
||||
|
||||
|
||||
# ---- cli ------------------------------------------------------------------
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser(description="Build the DorkNet PC client packages.")
|
||||
sub = p.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
pz = sub.add_parser("patcher", help="dorknet-clientpatch-<VersionKey>.zip (no game files)")
|
||||
pz.add_argument("--version-key", required=True, choices=sorted(LAYOUTS),
|
||||
help="the build the zip is for; picks the install layout")
|
||||
pz.add_argument("--melonloader-version", default="v0.6.6",
|
||||
help="MelonLoader release tag to bundle (default: v0.6.6, what the mod compiles against)")
|
||||
pz.add_argument("--output-dir", help="where the zip goes (default: <repo>/dist)")
|
||||
pz.set_defaults(func=cmd_patcher)
|
||||
|
||||
c = sub.add_parser("client", help="stock build + patcher zip + config -> ready-to-run client zip")
|
||||
c.add_argument("--base-client", required=True, help="the stock client, as a .zip or a folder")
|
||||
c.add_argument("--patcher-zip", required=True, help="dorknet-clientpatch-<VersionKey>.zip from `patcher`")
|
||||
c.add_argument("--config", help="dorknet-clientmod.json to install (default: the unedited template)")
|
||||
c.add_argument("--output-zip", help="default: <repo>/dist/dorknet-client-<VersionKey>.zip")
|
||||
c.set_defaults(func=cmd_client)
|
||||
|
||||
a = p.parse_args()
|
||||
a.func(a)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user