Re-package the aggregate: bundle Ward + ship the policing layer

Fold Ward into the single Institution install as its psychiatric-care
layer. package.sh now builds Ward clean and vendors Ward.dll plus its
defs and textures alongside the other layers, and the settings shim
gains a Ward (psychiatric care) checkbox that writes the shared
InstitutionSettings.ward flag.

Re-assembling also carries in Justice's now-shipping policing defs (the
constable work type and crime thoughts), so the aggregate finally
bundles colony crime and the weighed arrest as well. README updated to
five layers over one Core with policing shipped and Ward folded in;
adds the changelog, the Workshop preview, and the preview generator.
This commit is contained in:
flan
2026-07-15 20:10:56 +00:00
parent bd96c2c516
commit 2d46fef192
21 changed files with 1135 additions and 6 deletions
+626
View File
@@ -0,0 +1,626 @@
#!/usr/bin/env python3
"""
Generate the Institution suite's Workshop Preview.png images. Pure stdlib -- no Pillow, no CUDA,
no AI/diffusion, nothing ripped. Every pixel is drawn by the code below, exactly like the per-repo
Tools/make_textures.py sprite generators, so the copyright story stays clean.
Tools/make_preview.py # run from anywhere; writes into every sibling repo
Reuses the same from-scratch machinery the texture generators use: a hand-rolled RGBA PNG writer
(zlib + struct, see png()) and analytic signed-distance-field shapes supersampled for clean edges
(see _raster). Nothing beyond the standard library, so it runs on a box with no image tools at all.
Writes About/Preview.png (640x360, a safe 16:9 Workshop size) into each of:
rimworld-institution flagship: a barred cell window over the nature->nurture crime spectrum
rimworld-core the invisible engine: a nature x nurture node feeding a 0..1 spectrum bar
rimworld-contraband a filed shiv against a brick wall breached by an escape tunnel
rimworld-justice a level balance -- the corrections layer weighing what a pawn has done
rimworld-gangs a network of pawn tokens, two rival clusters bridged by a contraband line
They are one SET: shared muted institutional green-grey / tin palette, the same embossed-tin border,
the same faint grain, and a shared propensity spectrum ribbon (green -> amber -> red) along the
bottom of every image -- the suite's signature, the spectrum every layer reads from.
"""
import os
import zlib
import struct
import math
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # rimworld-institution
SUITE = os.path.dirname(REPO) # holds all the sibling repos
W, H = 640, 360
AS = W / H # aspect; everything is normalised by HEIGHT so circles stay round
SS = int(os.environ.get("SS", "3"))
# ---------------------------------------------------------------------------
# PNG encoder + supersampled rasteriser (pure stdlib) -- same as make_textures.py
# ---------------------------------------------------------------------------
def png(path, w, h, pixel_fn):
"""Write an 8-bit RGBA PNG from scratch. pixel_fn(x, y) -> (r, g, b, a)."""
raw = bytearray()
for y in range(h):
raw.append(0) # filter byte: 0 = None, once per scanline
row = bytearray()
for x in range(w):
r, g, b, a = pixel_fn(x, y)
row += bytes((r & 255, g & 255, b & 255, a & 255))
raw += row
def chunk(typ, data):
return (struct.pack(">I", len(data)) + typ + data
+ struct.pack(">I", zlib.crc32(typ + data) & 0xffffffff))
ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0) # 8-bit depth, colour type 6 = RGBA
idat = zlib.compress(bytes(raw), 9)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(b"\x89PNG\r\n\x1a\n")
f.write(chunk(b"IHDR", ihdr))
f.write(chunk(b"IDAT", idat))
f.write(chunk(b"IEND", b""))
def _raster(w, h, sample, ss=SS):
"""Wrap an analytic sample(fx, fy) -> (r, g, b, a) into a supersampled pixel_fn."""
inv = 1.0 / ss
n = ss * ss
def pixel(x, y):
pr = pg = pb = pa = 0.0
for j in range(ss):
fy = y + (j + 0.5) * inv
for i in range(ss):
r, g, b, a = sample(x + (i + 0.5) * inv, fy)
if a:
af = a * (1.0 / 255.0)
pr += r * af
pg += g * af
pb += b * af
pa += af
if pa <= 1e-6:
return (0, 0, 0, 0)
return (_cb(pr / pa), _cb(pg / pa), _cb(pb / pa), _cb(pa / n * 255.0))
return pixel
# ---------------------------------------------------------------------------
# colour + geometry helpers (normalised coords; u in 0..AS, v in 0..1)
# ---------------------------------------------------------------------------
def _cb(x):
x = int(x)
return 0 if x < 0 else (255 if x > 255 else x)
def _clamp01(x):
return 0.0 if x < 0.0 else (1.0 if x > 1.0 else x)
def _shade(c, f):
return (_cb(c[0] * f), _cb(c[1] * f), _cb(c[2] * f))
def _mix(a, b, t):
return (_cb(a[0] + (b[0] - a[0]) * t),
_cb(a[1] + (b[1] - a[1]) * t),
_cb(a[2] + (b[2] - a[2]) * t))
def _grain(ix, iy):
"""Deterministic faint speckle so fills aren't dead-flat."""
n = (ix * 374761393 + iy * 668265263) & 0xffffffff
n ^= (n >> 13)
n = (n * 1274126177) & 0xffffffff
return ((n >> 9) & 7) - 3 # -3..+4
def _grainy(c, u, v):
g = _grain(int(u * 300), int(v * 300))
return (_cb(c[0] + g), _cb(c[1] + g), _cb(c[2] + g))
def _seg(px, py, ax, ay, bx, by):
"""Distance from P to segment AB, and the clamped projection param t in [0, 1]."""
dx, dy = bx - ax, by - ay
l2 = dx * dx + dy * dy
if l2 < 1e-12:
return math.hypot(px - ax, py - ay), 0.0
t = ((px - ax) * dx + (py - ay) * dy) / l2
t = 0.0 if t < 0 else (1.0 if t > 1 else t)
cx, cy = ax + dx * t, ay + dy * t
return math.hypot(px - cx, py - cy), t
def _ellipse(u, v, cx, cy, rx, ry):
"""Approximate signed distance to an ellipse. Negative inside."""
k = math.hypot((u - cx) / rx, (v - cy) / ry)
return (k - 1.0) * min(rx, ry)
def _rrect(u, v, cx, cy, hw, hh, r):
"""Signed distance to a rounded rectangle. Negative inside."""
qx = abs(u - cx) - (hw - r)
qy = abs(v - cy) - (hh - r)
ox = qx if qx > 0 else 0.0
oy = qy if qy > 0 else 0.0
return math.hypot(ox, oy) + min(max(qx, qy), 0.0) - r
def _convex(u, v, pts):
"""Signed distance to a convex polygon (negative inside). Winding-agnostic."""
n = len(pts)
cxp = sum(p[0] for p in pts) / n
cyp = sum(p[1] for p in pts) / n
dmax = -1e9
for i in range(n):
ax, ay = pts[i]
bx, by = pts[(i + 1) % n]
ex, ey = bx - ax, by - ay
L = math.hypot(ex, ey) or 1e-9
nx, ny = ey / L, -ex / L # a normal to the edge
dp = (u - ax) * nx + (v - ay) * ny
dc = (cxp - ax) * nx + (cyp - ay) * ny # which side the interior is on
if dc > 0:
dp = -dp
if dp > dmax:
dmax = dp
return dmax
# ---------------------------------------------------------------------------
# shared palette -- muted institutional green-grey / tin, dark outline
# ---------------------------------------------------------------------------
OUTLINE = (22, 26, 20)
BG_TOP = (56, 64, 55)
BG_BOT = (31, 37, 31)
TIN = (138, 142, 126)
TIN_HI = (178, 182, 162)
TIN_DK = (92, 96, 82)
TIN_DDK = (62, 66, 56)
STEEL = (180, 184, 178)
STEEL_HI = (210, 214, 208)
STEEL_DK = (120, 124, 118)
BRICK = (94, 98, 88)
BRICK2 = (110, 114, 102)
MORTAR = (54, 58, 50)
EARTH = (46, 39, 29)
EARTH_DK = (24, 20, 14)
# the propensity spectrum: calm green (low) -> amber -> dangerous red (high)
SPEC_LOW = (104, 150, 100)
SPEC_MID = (198, 160, 74)
SPEC_HI = (176, 68, 54)
def spec(t):
"""Colour along the nature->nurture propensity spectrum, t in 0..1."""
t = _clamp01(t)
if t < 0.5:
return _mix(SPEC_LOW, SPEC_MID, t * 2.0)
return _mix(SPEC_MID, SPEC_HI, (t - 0.5) * 2.0)
# ---------------------------------------------------------------------------
# shared frame + background + spectrum ribbon (identical on every preview)
# ---------------------------------------------------------------------------
CX, CY = AS / 2.0, 0.5
FR_M = 0.050 # outer margin
FR_R = 0.038 # corner radius
FR_W = 0.026 # frame band width
FHW, FHH = CX - FR_M, CY - FR_M
IL, IR = FR_M + FR_W + 0.004, AS - FR_M - FR_W - 0.004 # inner content bounds
IT, IB = FR_M + FR_W + 0.004, 1.0 - FR_M - FR_W - 0.004
RIB_B = IB - 0.006
RIB_T = RIB_B - 0.036
RIB_L, RIB_R = IL + 0.004, IR - 0.004
def _bg(u, v):
c = _mix(BG_TOP, BG_BOT, _clamp01(v))
dx, dy = (u - CX) / CX, (v - CY) / CY
c = _shade(c, 1.0 - 0.24 * _clamp01(dx * dx + dy * dy))
return _grainy(c, u, v)
def _frame(u, v, d):
"""Embossed-tin border colour for a point whose rounded-rect distance is d."""
if d > -0.004:
return OUTLINE # crisp outer edge line
p = (-0.004 - d) / (FR_W - 0.004) # 0 outer .. 1 inner
if p > 0.90:
return OUTLINE # crisp inner edge line
nx, ny = (u - CX), (v - CY)
lit = -(nx * 0.55 + ny * 0.85) # light from the top-left
if lit > 0:
c = _mix(TIN, TIN_HI, _clamp01(lit * 2.4))
else:
c = _mix(TIN, TIN_DDK, _clamp01(-lit * 2.4))
return _grainy(c, u, v)
def _ribbon(u, v):
t = (u - RIB_L) / (RIB_R - RIB_L)
c = _shade(spec(t), 0.86)
if v - RIB_T < 0.004:
return OUTLINE # dark lintel over the ribbon
if RIB_B - v < 0.004:
c = _shade(c, 0.62)
if (t * 8.0) % 1.0 < 0.035: # eighth-tick divisions
c = _shade(c, 0.68)
return _grainy(c, u, v)
def preview(motif):
"""Compose background + shared ribbon + motif + border into one supersampled pixel_fn."""
def sample(fx, fy):
u, v = fx / H, fy / H
d = _rrect(u, v, CX, CY, FHW, FHH, FR_R)
if d > 0.004:
return _grainy(_shade(BG_BOT, 0.72), u, v) + (255,) # rounded corner falloff
if d > -FR_W:
return _frame(u, v, d) + (255,)
if RIB_T <= v <= RIB_B and RIB_L <= u <= RIB_R:
return _ribbon(u, v) + (255,)
m = motif(u, v)
if m is not None:
return m if len(m) == 4 else (m + (255,))
return _bg(u, v) + (255,)
return _raster(W, H, sample)
# ===========================================================================
# INSTITUTION -- the flagship. A barred cell window; behind the bars the whole
# nature->nurture crime spectrum glows. Prison + a spectrum of crime, in one image.
# ===========================================================================
WIN_CX, WIN_CY = CX, 0.45
WIN_HW, WIN_HH = 0.62, 0.30
NBARS = 6
def m_institution(u, v):
d = _rrect(u, v, WIN_CX, WIN_CY, WIN_HW, WIN_HH, 0.028)
if d > 0.030:
return None
if d > -0.006:
return OUTLINE # window outer edge
if d > -0.036: # tin window frame, bevelled
nx, ny = (u - WIN_CX), (v - WIN_CY)
lit = -(nx * 0.5 + ny * 0.9)
c = _mix(TIN, TIN_HI, _clamp01(lit * 3.0)) if lit > 0 else _mix(TIN, TIN_DK, _clamp01(-lit * 3.0))
return _grainy(c, u, v)
L, R = WIN_CX - WIN_HW, WIN_CX + WIN_HW
t = (u - L) / (R - L)
# bars: NBARS vertical + two horizontal, in front of the spectrum
s = (2 * WIN_HW) / NBARS
i = int((u - L) / s)
xc = L + s * (i + 0.5)
dvb = abs(u - xc)
bw = 0.016
hb = min(abs(v - (WIN_CY - 0.175)), abs(v - (WIN_CY + 0.175)))
hh = 0.014
is_v = dvb < bw
is_h = hb < hh
if is_v or is_h:
vertical = is_v and (not is_h or (dvb / bw) <= (hb / hh))
if vertical:
if dvb > bw - 0.0035:
return OUTLINE
p = (u - (xc - bw)) / (2 * bw) # 0 left .. 1 right
c = _mix(STEEL_DK, STEEL, 1.0 - p * 0.85)
c = _mix(c, STEEL_HI, _clamp01(0.5 - p) * 0.7)
return _grainy(c, u, v)
else:
if hb > hh - 0.0035:
return OUTLINE
near = WIN_CY - 0.175 if abs(v - (WIN_CY - 0.175)) < abs(v - (WIN_CY + 0.175)) else WIN_CY + 0.175
p = (v - (near - hh)) / (2 * hh)
c = _mix(STEEL_DK, STEEL, 1.0 - p * 0.85)
c = _mix(c, STEEL_HI, _clamp01(0.5 - p) * 0.7)
return _grainy(c, u, v)
# interior: the spectrum, brighter through the middle so it reads as light behind bars
base = spec(t)
gy = 1.0 - _clamp01(abs(v - WIN_CY) / WIN_HH)
base = _shade(base, 0.58 + 0.55 * gy)
if d > -0.020: # inner shadow just inside the frame
base = _shade(base, 0.80)
return _grainy(base, u, v)
# ===========================================================================
# CORE -- the invisible foundation. Two faint axes, NATURE (vertical) x NURTURE
# (horizontal), meet at the engine node, which drives the 0..1 spectrum bar below.
# Deliberately subdued: on its own it changes nothing you can see.
# ===========================================================================
NODE_X, NODE_Y = CX, 0.38
BAR_Y, BAR_HW, BAR_HH = 0.60, 0.62, 0.052
def m_core(u, v):
# 0..1 spectrum bar (the hero, low and central)
db = _rrect(u, v, CX, BAR_Y, BAR_HW, BAR_HH, 0.024)
if db <= 0.005:
if db > -0.005:
return OUTLINE
t = (u - (CX - BAR_HW)) / (2 * BAR_HW)
c = spec(t)
if (t * 10.0) % 1.0 < 0.028 and abs(v - BAR_Y) > BAR_HH - 0.016:
c = _shade(c, 0.6) # scale ticks along the rim
if abs(v - (BAR_Y - BAR_HH * 0.45)) < 0.006:
c = _mix(c, (255, 255, 255), 0.12) # a faint sheen line
return _grainy(c, u, v)
# engine node at the crossing
dn = math.hypot(u - NODE_X, v - NODE_Y)
if dn < 0.058:
if dn > 0.050:
return OUTLINE
c = _mix(TIN, STEEL_HI, _clamp01(1.0 - dn / 0.050))
return _grainy(c, u, v)
# faint NATURE x NURTURE axes behind everything
if abs(u - NODE_X) < 0.0035 and 0.14 < v < 0.62:
return _grainy(TIN_DK, u, v)
if abs(v - NODE_Y) < 0.0035 and (CX - 0.54) < u < (CX + 0.54):
return _grainy(TIN_DK, u, v)
# end caps on the axes -> small pips (nature high/low, nurture low/high)
for (px, py) in ((NODE_X, 0.15), (NODE_X, 0.61), (CX - 0.54, NODE_Y), (CX + 0.54, NODE_Y)):
if math.hypot(u - px, v - py) < 0.016:
return _grainy(TIN, u, v)
return None
# ===========================================================================
# CONTRABAND -- a filed shiv lying against a brick wall that an escape tunnel has
# breached. The physical smuggling loop: a whittled weapon and a hole under the wall.
# ===========================================================================
BX, BY, BRX, BRY = CX + 0.44, 0.54, 0.28, 0.25 # the tunnel breach
def _brick(u, v):
bh, bw = 0.078, 0.180
row = int((v - IT) / bh)
x = u + (bw * 0.5 if row % 2 else 0.0)
col = int(x / bw)
ly = (v - IT) - row * bh
lx = x - col * bw
if ly < 0.012 or lx < 0.012:
return MORTAR
h = (row * 928371 + col * 123457) & 255
return _shade(_mix(BRICK, BRICK2, h / 255.0), 0.94 + 0.12 * (((h >> 3) & 3) / 3.0))
def _breach(u, v):
ang = math.atan2(v - BY, u - BX)
wob = 0.028 * math.sin(ang * 7.0 + 0.7) + 0.020 * math.sin(ang * 3.0)
k = math.hypot((u - BX) / (BRX + wob), (v - BY) / (BRY + wob))
return (k - 1.0) * min(BRX, BRY)
def _shiv(u, v):
Bx, By = 0.30, 0.80 # cloth grip low-left
Jx, Jy = 0.44, 0.60
Tx, Ty = 0.78, 0.24 # filed point high-right
db, tb = _seg(u, v, Jx, Jy, Tx, Ty)
half = 0.030 * (1.0 - tb) + 0.004 * tb
d_blade = db - half
dg, tg = _seg(u, v, Bx, By, Jx, Jy)
d_grip = dg - 0.040
d = min(d_blade, d_grip)
if d > 0:
return None
if d > -0.006:
return OUTLINE
if d_grip <= d_blade:
wind = (110, 110, 104) if (tg * 4.3) % 1.0 < 0.34 else (150, 150, 142)
return _grainy(_shade(wind, 0.95 + 0.10 * tg), u, v)
bevel = 1.0 - _clamp01(db / max(1e-6, half))
c = _mix(STEEL_DK, STEEL, _clamp01(bevel * 1.6))
c = _mix(c, STEEL_HI, _clamp01(bevel - 0.55) * 1.4)
return _grainy(c, u, v)
def m_contraband(u, v):
s = _shiv(u, v) # the shiv sits on top of the wall
if s is not None:
return s
db = _breach(u, v)
if db < 0: # tunnel mouth: dark earth, deeper = darker
f = _clamp01(-db / BRY)
return _grainy(_mix(EARTH, EARTH_DK, f), u, v)
if db < 0.020: # broken, crumbling brick edge
return _grainy(_shade(MORTAR, 0.7), u, v)
return _grainy(_brick(u, v), u, v)
# ===========================================================================
# JUSTICE -- a level balance. Corrections weighing what a pawn has actually done:
# classification, deterrence, discipline. A gavel-less balance, held even.
# ===========================================================================
J_BEAM_Y = 0.28
J_BEAM_HW = 0.40
J_BASE_Y = 0.74
J_PAN_Y = 0.54
def _pan(u, v, px):
d = _ellipse(u, v, px, J_PAN_Y, 0.15, 0.058)
if v < J_PAN_Y: # keep the lower half -> a shallow bowl
d = max(d, (J_PAN_Y - v))
return d
def m_justice(u, v):
OW = 0.006
d_post = _rrect(u, v, CX, (J_BASE_Y + J_BEAM_Y) / 2.0, 0.018, (J_BASE_Y - J_BEAM_Y) / 2.0, 0.006)
d_base = min(_rrect(u, v, CX, J_BASE_Y, 0.150, 0.026, 0.012),
_convex(u, v, [(CX - 0.055, J_BASE_Y - 0.055), (CX + 0.055, J_BASE_Y - 0.055),
(CX + 0.100, J_BASE_Y - 0.020), (CX - 0.100, J_BASE_Y - 0.020)]))
d_beam = _rrect(u, v, CX, J_BEAM_Y, J_BEAM_HW, 0.015, 0.008)
d_fulc = _convex(u, v, [(CX, J_BEAM_Y - 0.055), (CX + 0.052, J_BEAM_Y),
(CX - 0.052, J_BEAM_Y)])
d_panL = _pan(u, v, CX - J_BEAM_HW)
d_panR = _pan(u, v, CX + J_BEAM_HW)
d_solid = min(d_post, d_base, d_beam, d_fulc, d_panL, d_panR)
# hangers: a thin line from each beam end down to its pan
dcl, _ = _seg(u, v, CX - J_BEAM_HW, J_BEAM_Y, CX - J_BEAM_HW, J_PAN_Y - 0.05)
dcr, _ = _seg(u, v, CX + J_BEAM_HW, J_BEAM_Y, CX + J_BEAM_HW, J_PAN_Y - 0.05)
d_chain = min(dcl, dcr) - 0.004
if d_chain <= 0 and d_chain < d_solid:
return _grainy(TIN_DK, u, v) if d_chain > -0.004 else _grainy(TIN, u, v)
if d_solid > 0:
return None
if d_solid > -OW:
return OUTLINE
# tin, lit from the top
lit = 1.0 - _clamp01((v - J_BEAM_Y) / (J_BASE_Y - J_BEAM_Y))
c = _mix(TIN_DK, TIN, 0.55 + 0.45 * lit)
if d_solid <= d_panL or d_solid <= d_panR or d_solid <= d_beam:
c = _mix(c, TIN_HI, 0.25 * lit)
return _grainy(c, u, v)
# ===========================================================================
# GANGS -- a contraband network. Pawn tokens band into two rival clusters (amber
# vs red) that a bright contraband line bridges through a broker in the middle.
# ===========================================================================
GANG_A, GANG_A_HI = (190, 150, 72), (216, 180, 98)
GANG_B, GANG_B_HI = (176, 70, 56), (208, 100, 84)
CONDUIT = (200, 202, 182)
NODES = [
(CX - 0.50, 0.34, 'A'), (CX - 0.62, 0.50, 'A'), (CX - 0.52, 0.66, 'A'), (CX - 0.36, 0.48, 'A'),
(CX, 0.44, 'X'),
(CX + 0.36, 0.36, 'B'), (CX + 0.54, 0.50, 'B'), (CX + 0.42, 0.66, 'B'), (CX + 0.24, 0.52, 'B'),
]
EDGES = [
(0, 1, 'A'), (1, 2, 'A'), (0, 3, 'A'), (3, 2, 'A'), (1, 3, 'A'),
(5, 6, 'B'), (6, 7, 'B'), (5, 8, 'B'), (8, 7, 'B'), (6, 8, 'B'),
(3, 4, 'X'), (4, 8, 'X'),
]
def _token(u, v, nx, ny, base, hi):
d_head = math.hypot(u - nx, v - (ny - 0.024)) - 0.020
d_body = _rrect(u, v, nx, ny + 0.014, 0.032, 0.024, 0.012)
d = min(d_head, d_body)
if d > 0:
return None
if d > -0.005:
return OUTLINE
return _grainy(hi if d_head < d_body else base, u, v)
def m_gangs(u, v):
for (nx, ny, g) in NODES: # tokens sit on top of the wires
if abs(u - nx) < 0.06 and abs(v - ny) < 0.06:
if g == 'A':
r = _token(u, v, nx, ny, GANG_A, GANG_A_HI)
elif g == 'B':
r = _token(u, v, nx, ny, GANG_B, GANG_B_HI)
else:
r = _token(u, v, nx, ny, TIN, TIN_HI)
if r is not None:
return r
best, kind, bt = 1e9, None, 0.0
for (i, j, k) in EDGES:
ax, ay = NODES[i][0], NODES[i][1]
bx, by = NODES[j][0], NODES[j][1]
dd, tt = _seg(u, v, ax, ay, bx, by)
if dd < best:
best, kind, bt = dd, k, tt
hw = 0.005
if best < hw:
if kind == 'X': # the contraband conduit: a bright dashed line
return _grainy(CONDUIT, u, v) if int(bt * 24) % 2 == 0 else None
col = _shade(GANG_A, 0.62) if kind == 'A' else _shade(GANG_B, 0.62)
if best > hw - 0.0018:
return _shade(col, 0.7)
return _grainy(col, u, v)
return None
# ===========================================================================
# verify: decode a PNG we wrote and confirm it is non-trivial (varied pixels)
# ===========================================================================
def verify(path):
data = open(path, "rb").read()
assert data[:8] == b"\x89PNG\r\n\x1a\n", "not a PNG"
i, idat, w, h = 8, b"", 0, 0
while i < len(data):
ln = struct.unpack(">I", data[i:i + 4])[0]
typ = data[i + 4:i + 8]
body = data[i + 8:i + 8 + ln]
if typ == b"IHDR":
w, h = struct.unpack(">II", body[:8])
elif typ == b"IDAT":
idat += body
i += 12 + ln
raw = zlib.decompress(idat) # rows: filter byte 0 + w*4 RGBA bytes
stride = w * 4 + 1
seen = set()
lo = [255, 255, 255]
hi = [0, 0, 0]
for y in range(0, h, 7): # sparse sample is plenty to prove variety
base = y * stride + 1
for x in range(0, w, 7):
o = base + x * 4
px = (raw[o], raw[o + 1], raw[o + 2])
seen.add(px)
for c in range(3):
lo[c] = min(lo[c], px[c])
hi[c] = max(hi[c], px[c])
spread = max(hi[c] - lo[c] for c in range(3))
return {"bytes": len(data), "w": w, "h": h, "distinct": len(seen), "spread": spread}
TARGETS = [
("institution", m_institution, "barred cell window over the nature->nurture crime spectrum"),
("core", m_core, "nature x nurture engine node driving a 0..1 spectrum bar"),
("contraband", m_contraband, "filed shiv against a brick wall breached by an escape tunnel"),
("justice", m_justice, "a level balance weighing what a pawn has done"),
("gangs", m_gangs, "two rival pawn clusters bridged by a contraband line"),
]
def main():
for name, motif, desc in TARGETS:
path = os.path.join(SUITE, "rimworld-" + name, "About", "Preview.png")
png(path, W, H, preview(motif))
info = verify(path)
ok = "OK" if info["distinct"] > 200 and info["spread"] > 80 else "?? CHECK"
print(f"{ok} {os.path.relpath(path, SUITE)} "
f"{info['w']}x{info['h']} {info['bytes']:,}B "
f"distinct~{info['distinct']} spread={info['spread']} -- {desc}")
if __name__ == "__main__":
main()
+14 -1
View File
@@ -2,7 +2,7 @@
# Assemble the shippable, drop-in "Institution" mod into this repo.
#
# The Institution suite's code lives in SEPARATE sibling repos (rimworld-core, -contraband, -justice,
# -gangs) as independent projects/DLLs -- that is what keeps it modular and splittable. For DISTRIBUTION
# -gangs, -ward) as independent projects/DLLs -- that is what keeps it modular and splittable. For DISTRIBUTION
# they are vendored into ONE mod folder (this repo) with one About.xml and a settings shim that toggles
# each layer. This script rebuilds the sibling DLLs clean (no self-test) and copies them + Contraband's
# Defs in here, so the repo is a complete installable mod.
@@ -19,6 +19,7 @@ CORE="$SIB/rimworld-core"
CB="$SIB/rimworld-contraband"
JUST="$SIB/rimworld-justice"
GANGS="$SIB/rimworld-gangs"
WARD="$SIB/rimworld-ward"
command -v dotnet >/dev/null || { echo "dotnet not on PATH" >&2; exit 1; }
@@ -27,6 +28,7 @@ dotnet build "$CORE/Source/Core/Core.csproj" -c Release -v q --n
dotnet build "$JUST/Source/Justice/Justice.csproj" -c Release -v q --nologo
dotnet build "$CB/Source/Contraband/Contraband.csproj" -c Release -v q --nologo
dotnet build "$GANGS/Source/Gangs/Gangs.csproj" -c Release -v q --nologo
dotnet build "$WARD/Source/Ward/Ward.csproj" -c Release -v q --nologo
echo "==> vendoring DLLs into Assemblies/"
mkdir -p "$HERE/Assemblies"
@@ -36,6 +38,7 @@ cp "$CORE/Assemblies/InstitutionCore.dll" "$HERE/Assemblies/"
cp "$CB/Assemblies/Contraband.dll" "$HERE/Assemblies/"
cp "$JUST/Assemblies/InstitutionJustice.dll" "$HERE/Assemblies/"
cp "$GANGS/Assemblies/InstitutionGangs.dll" "$HERE/Assemblies/"
cp "$WARD/Assemblies/Ward.dll" "$HERE/Assemblies/"
echo "==> vendoring the feature content"
for d in Defs Patches Languages Textures; do
@@ -47,6 +50,16 @@ if [[ -e "$JUST/Defs" ]]; then
mkdir -p "$HERE/Defs"
cp -r "$JUST/Defs/." "$HERE/Defs/"
fi
# Ward ships defs (interaction mode, hediffs, treatment station, room role) and its own textures.
# All its files are *_Ward.xml or under a Ward/ texture subdir, so nothing collides with the others.
if [[ -e "$WARD/Defs" ]]; then
mkdir -p "$HERE/Defs"
cp -r "$WARD/Defs/." "$HERE/Defs/"
fi
if [[ -e "$WARD/Textures" ]]; then
mkdir -p "$HERE/Textures"
cp -r "$WARD/Textures/." "$HERE/Textures/"
fi
echo "==> done. Assemblies:"
ls -1 "$HERE/Assemblies/" | sed 's/^/ /'