the files
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
@echo off
|
||||
setlocal
|
||||
title Campus launcher - debug build
|
||||
|
||||
REM Builds the DEBUG launcher for testing: bin\Debug\Campus-Debug.exe
|
||||
REM - its own settings / session / log folder (%LOCALAPPDATA%\CampusLauncher-Debug), so it never touches
|
||||
REM the real launcher's sign-in or settings
|
||||
REM - never self-updates (it says what the server has and carries on)
|
||||
REM - the window title, version line and log say DEBUG; the log also goes to Visual Studio's Output window
|
||||
REM - can run next to the real launcher
|
||||
REM - does NOT change the version, the launcher key or anything in Deploy\ - nothing here is shipped.
|
||||
REM The release is still built by build_release.bat (Release configuration) and sent out with PushUpdates.bat.
|
||||
REM Extra arguments are passed to the launcher: build_debug.bat run --server http://127.0.0.1:8080
|
||||
REM (also: --data <folder>, --page play|customize|settings, --nofind)
|
||||
|
||||
set "ROOT=%~dp0.."
|
||||
set "MSB=D:\VisualStudio\18\Enterprise\MSBuild\Current\Bin\amd64\MSBuild.exe"
|
||||
|
||||
"%MSB%" "%ROOT%\Launcher.vcxproj" /t:Build /p:Configuration=Debug /p:Platform=x64 /m /nologo /v:minimal
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo === DEBUG BUILD FAILED ===
|
||||
endlocal
|
||||
exit /b 1
|
||||
)
|
||||
echo.
|
||||
echo === DONE: %ROOT%\bin\Debug\Campus-Debug.exe ===
|
||||
|
||||
if /i "%~1"=="run" (
|
||||
shift
|
||||
start "" "%ROOT%\bin\Debug\Campus-Debug.exe" %1 %2 %3 %4 %5 %6 %7 %8 %9
|
||||
)
|
||||
endlocal
|
||||
exit /b 0
|
||||
@@ -0,0 +1,67 @@
|
||||
@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
title Campus launcher - release build
|
||||
|
||||
REM Makes the next release of bin\Campus.exe:
|
||||
REM 1. raises the version by one (src\Version.h: 1.01 -> 1.02); put back if anything fails
|
||||
REM 2. builds the exe (static C++ runtime - that one file is all anyone needs)
|
||||
REM 3. builds the Campus patch (CampusPatch2025\2025Patch.sln)
|
||||
REM 4. stages the exe in Deploy\launcher-app and the patch in Deploy\launcher-patch
|
||||
REM UploadUpdate.bat puts both on the server: everyone's launcher updates itself to the new exe, and
|
||||
REM every Play downloads the newest patch. The launcher may stay open: its exe is moved aside to
|
||||
REM bin\Campus.old.exe (the new one deletes it when it starts).
|
||||
REM Only the patch changed? "python tools\release_app.py patch" then UploadUpdate.bat is enough.
|
||||
|
||||
set "ROOT=%~dp0.."
|
||||
set "MSB=D:\VisualStudio\18\Enterprise\MSBuild\Current\Bin\amd64\MSBuild.exe"
|
||||
set "BUMPED="
|
||||
|
||||
where python >nul 2>&1 || (echo [x] python is not on PATH & goto :fail)
|
||||
REM Make a free slot for the previous build. If something is still holding Campus.old.exe (a
|
||||
REM previous auto-update's old process hasn't let go yet), slide it to Campus.old2.exe /
|
||||
REM Campus.old3.exe / ... .old9.exe - RemoveOldExe() on the next launcher start sweeps all of
|
||||
REM them. Only bail out if there is nowhere left to park the stale copy.
|
||||
if exist "%ROOT%\bin\Campus.old.exe" (
|
||||
del /q "%ROOT%\bin\Campus.old.exe" 2>nul
|
||||
if exist "%ROOT%\bin\Campus.old.exe" (
|
||||
set "PARKED="
|
||||
for %%N in (2 3 4 5 6 7 8 9) do (
|
||||
if not defined PARKED (
|
||||
if not exist "%ROOT%\bin\Campus.old%%N.exe" (
|
||||
ren "%ROOT%\bin\Campus.old.exe" "Campus.old%%N.exe" 2>nul
|
||||
if not exist "%ROOT%\bin\Campus.old.exe" set "PARKED=1"
|
||||
)
|
||||
)
|
||||
)
|
||||
if not defined PARKED (
|
||||
echo [x] bin\Campus.old.exe is locked and every parking slot is taken. Close every running Campus launcher first.
|
||||
goto :fail
|
||||
)
|
||||
)
|
||||
)
|
||||
if exist "%ROOT%\bin\Campus.exe" (
|
||||
move /y "%ROOT%\bin\Campus.exe" "%ROOT%\bin\Campus.old.exe" >nul || (echo [x] Can't move bin\Campus.exe aside - close every running Campus launcher first. & goto :fail)
|
||||
)
|
||||
python "%ROOT%\tools\release_app.py" bump || goto :fail
|
||||
set "BUMPED=1"
|
||||
REM Rotate the launcher-launch secret - every release gets its own key, so an exfiltrated one is
|
||||
REM short-lived. The server reads the same key from Data/Launcher/app/<sha>.launchkey, which
|
||||
REM "release_app.py stage" uploads next to the exe below.
|
||||
python "%ROOT%\tools\release_app.py" newlaunchkey || goto :fail
|
||||
"%MSB%" "%ROOT%\Launcher.vcxproj" /t:Rebuild /p:Configuration=Release /p:Platform=x64 /m /nologo /v:minimal || goto :fail
|
||||
"%MSB%" "%ROOT%\..\2025Patch.sln" /t:Build /p:Configuration=Release /p:Platform=x64 /m /nologo /v:minimal || goto :fail
|
||||
python "%ROOT%\tools\release_app.py" stage || goto :fail
|
||||
python "%ROOT%\tools\release_app.py" patch || goto :fail
|
||||
|
||||
echo.
|
||||
echo === DONE: %ROOT%\bin\Campus.exe - run UploadUpdate.bat to release it. ===
|
||||
endlocal
|
||||
exit /b 0
|
||||
|
||||
:fail
|
||||
if defined BUMPED python "%ROOT%\tools\release_app.py" unbump
|
||||
if not exist "%ROOT%\bin\Campus.exe" if exist "%ROOT%\bin\Campus.old.exe" move /y "%ROOT%\bin\Campus.old.exe" "%ROOT%\bin\Campus.exe" >nul
|
||||
echo.
|
||||
echo === BUILD FAILED ===
|
||||
endlocal
|
||||
exit /b 1
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Builds HeroMask.png: where Hero.jpg is backdrop or logo letters (white) - the parts the launcher
|
||||
repaints in the player's gradient (App::PaintHero). Run after changing Hero.jpg:
|
||||
|
||||
python tools/make_hero_mask.py
|
||||
|
||||
The characters' skin and red hair are as saturated as the purple->orange backdrop, so colour alone
|
||||
can't separate them. Instead:
|
||||
1. fit a smooth model of the backdrop (least squares, cubic in x/y) to clearly-backdrop pixels,
|
||||
2. a pixel is backdrop only if it matches that model in chroma AND hue, at any brightness (the
|
||||
backdrop in shadow round the logo is still backdrop; red hair is a few degrees off in hue,
|
||||
skin is off in chroma),
|
||||
3. keep only the big connected backdrop region(s) - an isolated patch inside a character (a hat
|
||||
band, a highlight) is never backdrop - and fill small specks in the backdrop,
|
||||
4. add the logo's letters: the saturated pixels its white outline encloses.
|
||||
"""
|
||||
import os
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFilter
|
||||
from scipy import ndimage
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
im = np.asarray(Image.open(os.path.join(ROOT, 'Hero.jpg')).convert('RGB')).astype(np.float32) / 255
|
||||
H, W, _ = im.shape
|
||||
ys, xs = np.mgrid[0:H, 0:W]
|
||||
X, Y = xs / W, ys / H
|
||||
|
||||
|
||||
def feats(x, y):
|
||||
return np.stack([np.ones_like(x), x, y, x * x, x * y, y * y, x ** 3, y ** 3, x * x * y, x * y * y], -1)
|
||||
|
||||
|
||||
def hue(rgb):
|
||||
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
|
||||
mx, mn = rgb.max(-1), rgb.min(-1)
|
||||
d = np.maximum(mx - mn, 1e-6)
|
||||
h = np.where(mx == r, ((g - b) / d) % 6, np.where(mx == g, (b - r) / d + 2, (r - g) / d + 4)) * 60
|
||||
return h
|
||||
|
||||
|
||||
F = feats(X, Y).reshape(-1, 10)
|
||||
P = im.reshape(-1, 3)
|
||||
mx, mn = P.max(1), P.min(1)
|
||||
s = np.where(mx > 0, (mx - mn) / np.maximum(mx, 1e-6), 0)
|
||||
sel = (s > 0.55) & (mx > 0.5) & ((Y < 0.33).reshape(-1) | ((X > 0.33) & (X < 0.70) & (Y > 0.66)).reshape(-1))
|
||||
for _ in range(5):
|
||||
coef, *_ = np.linalg.lstsq(F[sel], P[sel], rcond=None)
|
||||
err = np.abs(F @ coef - P).max(1)
|
||||
sel = (err < 0.07) & (s > 0.45)
|
||||
model = np.clip((F @ coef).reshape(H, W, 3), 0, 1)
|
||||
|
||||
# compare colour apart from brightness: the backdrop in shadow (round the logo's outline, behind
|
||||
# the characters) is still backdrop. Hue catches the red hair, chroma catches skin.
|
||||
vim, vmod = im.max(2), np.maximum(model.max(2), 1e-6)
|
||||
chroma_err = np.abs(im / np.maximum(vim, 1e-6)[..., None] - model / vmod[..., None]).max(2)
|
||||
ratio = vim / vmod
|
||||
dh = np.abs(hue(model) - hue(im))
|
||||
dh = np.minimum(dh, 360 - dh)
|
||||
bg = (1 - np.clip((chroma_err - 0.07) / 0.08, 0, 1)) * (1 - np.clip((dh - 4) / 5, 0, 1)) * np.clip((ratio - 0.30) / 0.15, 0, 1) * (1 - np.clip((ratio - 1.12) / 0.1, 0, 1))
|
||||
|
||||
# connected regions: keep the big backdrop, fill specks
|
||||
solid = bg > 0.5
|
||||
lab, n = ndimage.label(solid)
|
||||
sizes = ndimage.sum(solid, lab, range(1, n + 1))
|
||||
keep = np.isin(lab, 1 + np.nonzero(sizes > 0.02 * H * W)[0])
|
||||
holes, hn = ndimage.label(~keep)
|
||||
hsizes = ndimage.sum(~keep, holes, range(1, hn + 1))
|
||||
keep |= np.isin(holes, 1 + np.nonzero(hsizes < 400)[0])
|
||||
bg = np.where(keep, np.maximum(bg, 0.0), 0.0)
|
||||
bg = np.where(keep, np.clip(bg * 1.5, 0, 1), 0.0)
|
||||
|
||||
# the logo's letters: what its white outline encloses (filling the outline's holes), so nothing that
|
||||
# merely touches the logo from outside - the girl's red hair does, right of the "S" - counts
|
||||
smap, vmap = s.reshape(H, W), mx.reshape(H, W)
|
||||
box = (X > 0.24) & (X < 0.78) & (Y > 0.33) & (Y < 0.67)
|
||||
white = box & (smap < 0.15) & (vmap > 0.82)
|
||||
white = ndimage.binary_closing(white, iterations=2)
|
||||
inside = ndimage.binary_fill_holes(white) & ~white
|
||||
logo = np.where(inside, np.clip((smap - 0.35) / 0.25, 0, 1), 0)
|
||||
|
||||
m = np.maximum(bg, logo)
|
||||
mask = Image.fromarray((m * 255).astype(np.uint8), 'L')
|
||||
mask = mask.filter(ImageFilter.MaxFilter(3)).filter(ImageFilter.GaussianBlur(1.0)) # cover the backdrop's fringe
|
||||
mask.save(os.path.join(ROOT, 'HeroMask.png'), optimize=True)
|
||||
print('HeroMask.png', mask.size)
|
||||
@@ -0,0 +1,193 @@
|
||||
"""Campus launcher releases: the version number, the staged exe and the staged patch.
|
||||
|
||||
release_app.py bump raise src\\Version.h by one: 1.01 -> 1.02 ... 1.99 -> 2.00
|
||||
release_app.py unbump undo that (the build failed, so that number wasn't released)
|
||||
release_app.py stage copy bin\\Campus.exe to Deploy\\launcher-app\\<sha256>.exe and write
|
||||
latest.json (version, build, sha256, size) next to it
|
||||
release_app.py patch [beta|dev|2026] (the channel is for developer accounts only - PushChannel.bat; no channel = public;
|
||||
"2026" = the Rec Room 2026 client's patch, CampusPatch2025\\x64\\Release\\2026Patch.dll, with its own secret)
|
||||
copy the built CampusPatch.dll (CampusPatch2025\\x64\\Release) to
|
||||
Deploy\\launcher-patch\\<sha256>.dll and write latest.json (sha256, size)
|
||||
release_app.py newkey [2026] write a new patch secret to PatchProofKey.h (then build + "patch" stages it);
|
||||
"2026" = the 2026Patch project's own key
|
||||
release_app.py game [zip] stage the whole game for the launcher's INSTALL / UPDATE: the zip
|
||||
(default D:\\Puck\\Campus.zip) as Deploy\\launcher-game\\<sha256>.zip (a
|
||||
hard link - no 4 GB copy) + latest.json (sha256, size, unpacked)
|
||||
|
||||
UploadUpdate.bat sends what's staged (only what the VPS lacks) and then the latest.json files:
|
||||
running launchers see the higher build and update themselves, and every Play downloads the newest
|
||||
patch (src\\Update.h, src\\Game.h).
|
||||
"""
|
||||
import hashlib, json, os, re, sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # ...\CampusPatch2025\Launcher
|
||||
VERSION_H = os.path.join(ROOT, 'src', 'Version.h')
|
||||
DEPLOY = r'D:\Puck\Campus2025Server-2025\Deploy'
|
||||
APP_STAGE = os.path.join(DEPLOY, 'launcher-app')
|
||||
PATCH_STAGE = os.path.join(DEPLOY, 'launcher-patch')
|
||||
GAME_STAGE = os.path.join(DEPLOY, 'launcher-game')
|
||||
GAME_ZIP = r'D:\Puck\Campus.zip'
|
||||
PATCH_KEY_H = os.path.join(os.path.dirname(ROOT), '2025Patch', 'src', 'RR', 'Patching', 'PatchProofKey.h')
|
||||
PATCH26_KEY_H = os.path.join(os.path.dirname(ROOT), '2026Patch', 'src', 'RR', 'Patching', 'PatchProofKey.h') # the 2026 client's patch has its own secret
|
||||
LAUNCHER_KEY_H = os.path.join(ROOT, 'src', 'LauncherKey.h')
|
||||
PATCH_BUILDS =[os.path.join(os.path.dirname(ROOT), 'x64', 'Release', n) for n in ('2025Patch.dll', 'CampusPatch.dll')]
|
||||
PATCH_BUILDS_DEV = [os.path.join(os.path.dirname(ROOT), 'x64', 'Dev', '2025Patch.dll')] # the Dev configuration (the dev channel)
|
||||
PATCH_BUILDS_2026 = [os.path.join(os.path.dirname(ROOT), 'x64', 'Release', '2026Patch.dll')] # the Rec Room 2026 client's patch (the 2026 channel)
|
||||
|
||||
|
||||
def read_version():
|
||||
s = open(VERSION_H, encoding='utf-8').read()
|
||||
major = int(re.search(r'#define LV_VERSION_MAJOR (\d+)', s).group(1))
|
||||
minor = int(re.search(r'#define LV_VERSION_MINOR (\d+)', s).group(1))
|
||||
return s, major, minor
|
||||
|
||||
|
||||
def bump(step=1):
|
||||
s, major, minor = read_version()
|
||||
minor += step
|
||||
if minor > 99: major, minor = major + 1, 0
|
||||
if minor < 0: major, minor = major - 1, 99
|
||||
s = re.sub(r'#define LV_VERSION_MAJOR \d+', f'#define LV_VERSION_MAJOR {major}', s)
|
||||
s = re.sub(r'#define LV_VERSION_MINOR \d+', f'#define LV_VERSION_MINOR {minor}', s)
|
||||
s = re.sub(r'#define LV_VERSION_STR "[^"]*"', f'#define LV_VERSION_STR "{major}.{minor:02d}"', s)
|
||||
s = re.sub(r'#define LV_VERSION_BUILD \d+', f'#define LV_VERSION_BUILD {major * 100 + minor}', s)
|
||||
open(VERSION_H, 'w', encoding='utf-8', newline='').write(s)
|
||||
print(f'launcher version {major}.{minor:02d}')
|
||||
|
||||
|
||||
def put(stage, data, ext, info):
|
||||
os.makedirs(stage, exist_ok=True)
|
||||
sha = info['sha256']
|
||||
for f in os.listdir(stage): # only the newest goes up
|
||||
if f.endswith(ext) and f != sha + ext: os.remove(os.path.join(stage, f))
|
||||
open(os.path.join(stage, sha + ext), 'wb').write(data)
|
||||
open(os.path.join(stage, 'latest.json'), 'w', encoding='utf-8').write(json.dumps(info, indent=1))
|
||||
|
||||
|
||||
def read_launcher_key():
|
||||
m = re.search(r'#define CAMPUS_LAUNCHER_LAUNCH_KEY "([^"]+)"', open(LAUNCHER_KEY_H, encoding='utf-8').read())
|
||||
if not m:
|
||||
sys.exit('[x] no key in ' + LAUNCHER_KEY_H + ' - run "release_app.py newlaunchkey"')
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def newlaunchkey():
|
||||
import secrets
|
||||
key = secrets.token_urlsafe(32)
|
||||
open(LAUNCHER_KEY_H, 'w', encoding='utf-8', newline='').write(
|
||||
'#pragma once\n'
|
||||
'// GENERATED by CampusPatch2025\\Launcher\\tools\\release_app.py newlaunchkey (build_release.bat\n'
|
||||
'// makes a new one for every launcher release). The server reads the same key from\n'
|
||||
'// Data/Launcher/app/<sha256>.launchkey, uploaded next to this build (release_app.py stage).\n'
|
||||
'// Never commit or paste it.\n'
|
||||
'//\n'
|
||||
'// Used by App::MintLaunchToken() to sign the CAMPUS_LAUNCH_TOKEN env var passed to the game.\n'
|
||||
'// The patch forwards it to the server with /api/patch/proof; a login without a fresh launcher\n'
|
||||
'// proof is refused and the player\'s Steam id is yanked from the normal-player whitelist.\n'
|
||||
'#define CAMPUS_LAUNCHER_LAUNCH_KEY "' + key + '"\n')
|
||||
print('new launcher-launch secret written to LauncherKey.h (rebuild the launcher to use it)')
|
||||
|
||||
|
||||
def stage():
|
||||
_, major, minor = read_version()
|
||||
data = open(os.path.join(ROOT, 'bin', 'Campus.exe'), 'rb').read()
|
||||
sha = hashlib.sha256(data).hexdigest()
|
||||
info = {'version': f'{major}.{minor:02d}', 'build': major * 100 + minor, 'sha256': sha, 'size': len(data)}
|
||||
put(APP_STAGE, data, '.exe', info)
|
||||
# The launcher-launch secret for THIS build goes up alongside the exe. The server reads it as
|
||||
# Data/Launcher/app/<sha>.launchkey; if an older key sits next to a newer build it would be
|
||||
# tried first, so sweep every .launchkey that isn't ours.
|
||||
for f in os.listdir(APP_STAGE):
|
||||
if f.endswith('.launchkey') and f != sha + '.launchkey':
|
||||
os.remove(os.path.join(APP_STAGE, f))
|
||||
open(os.path.join(APP_STAGE, sha + '.launchkey'), 'w', encoding='utf-8', newline='').write(read_launcher_key())
|
||||
print(f'staged launcher {info["version"]} ({sha[:16]}..., {len(data)} bytes) in {APP_STAGE} + .launchkey')
|
||||
|
||||
|
||||
def read_key(key_h=PATCH_KEY_H):
|
||||
m = re.search(r'#define CAMPUS_PATCH_PROOF_KEY "([^"]+)"', open(key_h, encoding='utf-8').read())
|
||||
if not m:
|
||||
sys.exit('[x] no key in ' + key_h + ' - run "release_app.py newkey"')
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def newkey(which=''):
|
||||
import secrets
|
||||
if which not in ('', '2026'): sys.exit('[x] newkey takes no argument (the 2025 patch) or 2026')
|
||||
key_h = PATCH26_KEY_H if which == '2026' else PATCH_KEY_H
|
||||
key = secrets.token_urlsafe(32)
|
||||
open(key_h, 'w', encoding='utf-8', newline='').write(
|
||||
'#pragma once\n'
|
||||
'// GENERATED by CampusPatch2025\\Launcher\\tools\\release_app.py newkey (ReleaseGame.bat makes a new one\n'
|
||||
'// for every patch release). The server reads the same key from Data/Launcher/patch/<sha256>.key,\n'
|
||||
'// uploaded next to this build. Never commit or paste it.\n'
|
||||
'#define CAMPUS_PATCH_PROOF_KEY "' + key + '"\n')
|
||||
print(f'new patch secret written to {key_h} (rebuild the patch to use it)')
|
||||
|
||||
|
||||
def patch(channel=''):
|
||||
# the 2025Patch project builds 2025Patch.dll; older builds were renamed CampusPatch.dll - take the newest
|
||||
# The 2026 channel is the 2026Patch project's DLL (a different game client) with its own secret.
|
||||
if channel not in ('', 'beta', 'dev', '2026'): sys.exit('[x] the channel is beta, dev or 2026 (or nothing for public)')
|
||||
builds = PATCH_BUILDS_DEV if channel == 'dev' else PATCH_BUILDS_2026 if channel == '2026' else PATCH_BUILDS
|
||||
built = [p for p in builds if os.path.exists(p)]
|
||||
if not built:
|
||||
sys.exit(r'[x] no built patch in CampusPatch2025\x64\Dev - build 2025Patch.sln (Dev x64) first' if channel == 'dev'
|
||||
else r'[x] no built patch in CampusPatch2025\x64\Release - build 2026Patch.sln (Release x64) first' if channel == '2026'
|
||||
else r'[x] no built patch in CampusPatch2025\x64\Release - build 2025Patch.sln (Release x64) first')
|
||||
newest = max(built, key=os.path.getmtime)
|
||||
data = open(newest, 'rb').read()
|
||||
sha = hashlib.sha256(data).hexdigest()
|
||||
stage_dir = os.path.join(PATCH_STAGE, channel) if channel else PATCH_STAGE # beta / dev: Deploy\launcher-patch\<channel>
|
||||
put(stage_dir, data, '.dll', {'sha256': sha, 'size': len(data)})
|
||||
# this build's secret goes up with it (the server checks the patch's proofs with it)
|
||||
for f in os.listdir(stage_dir):
|
||||
if f.endswith('.key') and f != sha + '.key':
|
||||
os.remove(os.path.join(stage_dir, f))
|
||||
open(os.path.join(stage_dir, sha + '.key'), 'w', encoding='utf-8', newline='').write(read_key(PATCH26_KEY_H if channel == '2026' else PATCH_KEY_H))
|
||||
print(f'staged the {channel or "public"} Campus patch ({sha[:16]}..., {len(data)} bytes) in {stage_dir}')
|
||||
|
||||
|
||||
def game(zip_path=GAME_ZIP):
|
||||
import zipfile
|
||||
if not os.path.exists(zip_path):
|
||||
sys.exit('[x] no game zip at ' + zip_path)
|
||||
with zipfile.ZipFile(zip_path) as z:
|
||||
infos = z.infolist()
|
||||
if not any(i.filename.lower().endswith('recroom_release.exe') for i in infos):
|
||||
sys.exit('[x] ' + zip_path + " has no Recroom_Release.exe in it - that isn't the game")
|
||||
unpacked = sum(i.file_size for i in infos)
|
||||
h = hashlib.sha256()
|
||||
with open(zip_path, 'rb') as f:
|
||||
for block in iter(lambda: f.read(8 << 20), b''):
|
||||
h.update(block)
|
||||
sha = h.hexdigest()
|
||||
size = os.path.getsize(zip_path)
|
||||
os.makedirs(GAME_STAGE, exist_ok=True)
|
||||
for f in os.listdir(GAME_STAGE): # only the newest goes up
|
||||
if f.endswith('.zip') and f != sha + '.zip':
|
||||
os.remove(os.path.join(GAME_STAGE, f))
|
||||
dst = os.path.join(GAME_STAGE, sha + '.zip')
|
||||
if not os.path.exists(dst):
|
||||
try:
|
||||
os.link(zip_path, dst) # same drive: a hard link, no copy
|
||||
except OSError:
|
||||
import shutil
|
||||
shutil.copyfile(zip_path, dst)
|
||||
open(os.path.join(GAME_STAGE, 'latest.json'), 'w', encoding='utf-8').write(
|
||||
json.dumps({'sha256': sha, 'size': size, 'unpacked': unpacked}, indent=1))
|
||||
print(f'staged the game ({sha[:16]}..., {size / 1e9:.2f} GB, {unpacked / 1e9:.2f} GB unpacked) in {GAME_STAGE}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cmd = sys.argv[1]
|
||||
if cmd == 'game':
|
||||
game(sys.argv[2] if len(sys.argv) > 2 else GAME_ZIP)
|
||||
else:
|
||||
if cmd == 'patch':
|
||||
patch(sys.argv[2] if len(sys.argv) > 2 else '')
|
||||
else:
|
||||
if cmd == 'newkey':
|
||||
newkey(sys.argv[2] if len(sys.argv) > 2 else '')
|
||||
else:
|
||||
{'bump': bump, 'unbump': lambda: bump(-1), 'stage': stage, 'newlaunchkey': newlaunchkey}[cmd]()
|
||||
Reference in New Issue
Block a user