initial commit: reforgermods.net launcher 0.1.0
A desktop server browser for Arma Reforger, built on the reforgermods.net v2 API. Live server list with search, sort and filters; a detail inspector with population history and mod readiness; measured ICMP latency; favorites, recents and settings; and direct joining through Steam. Tauri 2 and Rust, with a Preact frontend that builds to 80 KB of JavaScript in one chunk. Logic lives in rfm-core, which has no Tauri dependency and is tested directly; src-tauri is the window, the command surface and the wiring. Packaged for the public beta: a per-user NSIS installer, a portable .exe and an Arch package, with signature-verified background updates on Windows and a release pipeline that builds from a tag. The artifacts are not yet Authenticode-signed — that is wired and waiting on an Azure Trusted Signing identity, and docs/distribution.md says so plainly rather than letting anyone discover it at the SmartScreen dialog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
91 files changed
+24994
No files matched your search
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Derive the launcher's icons from the reforgermods.net brand assets.
|
||||
|
||||
No image libraries, so this is reproducible from a clean checkout with nothing
|
||||
but a Python interpreter. Run from the repo root:
|
||||
|
||||
python3 scripts/gen_icons.py
|
||||
|
||||
Sources (from the reforgermods.net site assets, copied into brand/):
|
||||
|
||||
reforger-mods-app-icon.png -> application / taskbar icon, all sizes
|
||||
reforger-mods-icon-dark.png -> transparent mark used in the app header
|
||||
|
||||
Downsampling is a box filter on premultiplied alpha. Premultiplying matters:
|
||||
averaging straight RGBA makes fully transparent pixels drag their colour into
|
||||
the edge, which shows up as a dark halo around the mark at 16px.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
BRAND = ROOT / "brand"
|
||||
ICONS = ROOT / "src-tauri" / "icons"
|
||||
UI_ASSETS = ROOT / "src" / "assets"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- decoding
|
||||
|
||||
def read_png(path):
|
||||
"""Decodes an 8-bit RGBA PNG into (width, height, rows of bytes)."""
|
||||
data = path.read_bytes()
|
||||
if data[:8] != b"\x89PNG\r\n\x1a\n":
|
||||
raise ValueError(f"{path} is not a PNG")
|
||||
|
||||
width = height = None
|
||||
idat = bytearray()
|
||||
pos = 8
|
||||
while pos < len(data):
|
||||
length, tag = struct.unpack_from(">I4s", data, pos)
|
||||
body = data[pos + 8 : pos + 8 + length]
|
||||
if tag == b"IHDR":
|
||||
width, height, depth, colour = struct.unpack(">IIBB", body[:10])
|
||||
if depth != 8 or colour != 6:
|
||||
raise ValueError(f"{path}: expected 8-bit RGBA, got depth={depth} colour={colour}")
|
||||
elif tag == b"IDAT":
|
||||
idat += body
|
||||
elif tag == b"IEND":
|
||||
break
|
||||
pos += 12 + length
|
||||
|
||||
raw = zlib.decompress(bytes(idat))
|
||||
stride = width * 4
|
||||
rows = []
|
||||
previous = bytearray(stride)
|
||||
pos = 0
|
||||
for _ in range(height):
|
||||
filter_type = raw[pos]
|
||||
pos += 1
|
||||
line = bytearray(raw[pos : pos + stride])
|
||||
pos += stride
|
||||
unfilter(filter_type, line, previous, 4)
|
||||
rows.append(bytes(line))
|
||||
previous = line
|
||||
return width, height, rows
|
||||
|
||||
|
||||
def unfilter(filter_type, line, previous, bpp):
|
||||
"""Reverses one PNG scanline filter in place (RFC 2083 section 6)."""
|
||||
if filter_type == 0:
|
||||
return
|
||||
for i in range(len(line)):
|
||||
a = line[i - bpp] if i >= bpp else 0
|
||||
b = previous[i]
|
||||
if filter_type == 1:
|
||||
line[i] = (line[i] + a) & 0xFF
|
||||
elif filter_type == 2:
|
||||
line[i] = (line[i] + b) & 0xFF
|
||||
elif filter_type == 3:
|
||||
line[i] = (line[i] + ((a + b) >> 1)) & 0xFF
|
||||
elif filter_type == 4:
|
||||
c = previous[i - bpp] if i >= bpp else 0
|
||||
p = a + b - c
|
||||
pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
|
||||
pred = a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
|
||||
line[i] = (line[i] + pred) & 0xFF
|
||||
else:
|
||||
raise ValueError(f"unknown filter {filter_type}")
|
||||
|
||||
|
||||
# --------------------------------------------------------------- resampling
|
||||
|
||||
def resize(width, height, rows, size):
|
||||
"""Box-filter downsample to size x size, averaging premultiplied alpha."""
|
||||
out = []
|
||||
for oy in range(size):
|
||||
y0, y1 = oy * height // size, max(oy * height // size + 1, (oy + 1) * height // size)
|
||||
row = bytearray()
|
||||
for ox in range(size):
|
||||
x0, x1 = ox * width // size, max(ox * width // size + 1, (ox + 1) * width // size)
|
||||
r = g = b = a = 0
|
||||
count = 0
|
||||
for y in range(y0, y1):
|
||||
line = rows[y]
|
||||
for x in range(x0, x1):
|
||||
i = x * 4
|
||||
alpha = line[i + 3]
|
||||
# Premultiply so transparent pixels contribute no colour.
|
||||
r += line[i] * alpha
|
||||
g += line[i + 1] * alpha
|
||||
b += line[i + 2] * alpha
|
||||
a += alpha
|
||||
count += 1
|
||||
if a == 0:
|
||||
row += bytes(4)
|
||||
else:
|
||||
row += bytes((r // a, g // a, b // a, a // count))
|
||||
out.append(bytes(row))
|
||||
return out
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- encoding
|
||||
|
||||
def write_png(rows, size):
|
||||
def chunk(tag, body):
|
||||
payload = tag + body
|
||||
return struct.pack(">I", len(body)) + payload + struct.pack(">I", zlib.crc32(payload))
|
||||
|
||||
raw = b"".join(b"\x00" + r for r in rows)
|
||||
return (
|
||||
b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 6, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(raw, 9))
|
||||
+ chunk(b"IEND", b"")
|
||||
)
|
||||
|
||||
|
||||
def write_ico(entries):
|
||||
"""Packs PNG-encoded images into an .ico container."""
|
||||
header = struct.pack("<HHH", 0, 1, len(entries))
|
||||
offset = 6 + 16 * len(entries)
|
||||
directory, blobs = b"", b""
|
||||
for size, blob in entries:
|
||||
directory += struct.pack(
|
||||
"<BBBBHHII", size % 256, size % 256, 0, 0, 1, 32, len(blob), offset
|
||||
)
|
||||
blobs += blob
|
||||
offset += len(blob)
|
||||
return header + directory + blobs
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- main
|
||||
|
||||
def main():
|
||||
ICONS.mkdir(parents=True, exist_ok=True)
|
||||
UI_ASSETS.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
app_w, app_h, app_rows = read_png(BRAND / "reforger-mods-app-icon.png")
|
||||
cache = {}
|
||||
|
||||
def app_at(size):
|
||||
if size not in cache:
|
||||
cache[size] = write_png(resize(app_w, app_h, app_rows, size), size)
|
||||
return cache[size]
|
||||
|
||||
for name, size in [
|
||||
("32x32.png", 32),
|
||||
("128x128.png", 128),
|
||||
("128x128@2x.png", 256),
|
||||
("icon.png", 512),
|
||||
]:
|
||||
(ICONS / name).write_bytes(app_at(size))
|
||||
print(f"wrote src-tauri/icons/{name} ({size}x{size})")
|
||||
|
||||
(ICONS / "icon.ico").write_bytes(write_ico([(s, app_at(s)) for s in (16, 32, 48, 256)]))
|
||||
print("wrote src-tauri/icons/icon.ico (16, 32, 48, 256)")
|
||||
|
||||
# Transparent mark for the in-app header. 64px covers an 18px slot at 3x.
|
||||
mark_w, mark_h, mark_rows = read_png(BRAND / "reforger-mods-icon-dark.png")
|
||||
blob = write_png(resize(mark_w, mark_h, mark_rows, 64), 64)
|
||||
(UI_ASSETS / "brand-mark.png").write_bytes(blob)
|
||||
print(f"wrote src/assets/brand-mark.png (64x64, {len(blob)} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user