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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+24
View File
@@ -0,0 +1,24 @@
# Changelog
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [SemVer](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- **Institution** — the aggregate mod. One install bundles the whole suite: the **Core** propensity /
record engine plus the **Contraband**, **Justice**, and **Gangs** feature layers, each behind its own
in-game toggle (all on by default). The layers remain modular — the suite can be split back into
separate mods at any time; this is packaging, not a merge.
- **Settings panel** — a checkbox per layer:
- **Contraband** — concealment, search, escape tunnels.
- **Justice** — classification, deterrence, discipline & reform, parole, regime.
- **Gangs** — prison contraband economies and social capstone.
- **Policing** — colony crime and weighed arrests. A large behavioural change, so it carries its own
switch (on by default): your own colonists commit crimes on the propensity spectrum, and a weighed
arrest can end in a cell or a resisted breakout.
Toggling a layer off disables its systems in place, without uninstalling anything.
### Notes
- Requires **Harmony**. The suite's only Harmony-using code is Justice's prisoner-recreation postfix.
- The standalone **Foul Play** framework bridges into this suite when both are installed.
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<JobDef>
<defName>Institution_Investigate</defName>
<driverClass>Contraband.JobDriver_Investigate</driverClass>
<reportString>investigating a crime scene.</reportString>
<casualInterruptible>true</casualInterruptible>
</JobDef>
</Defs>
+134
View File
@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<!--
The "locked cabinet of sedatives" in the treatment station's description, made real. Sedation is
the other lever a ward has: not therapy, but a way to buy calm through a crisis. It costs a dose
and a groggy, useless day from the patient — it settles a breakdown, it does not treat what
causes them. A warden administers one to a patient at the edge of a break.
-->
<ThingDef ParentName="ResourceBase">
<defName>Ward_Sedative</defName>
<label>sedative</label>
<description>A measured dose of fast-acting sedative. A warden can administer one to a patient on the edge of a breakdown to buy calm — at the cost of a groggy, near-useless day. It settles a crisis; it does not treat one.</description>
<graphicData>
<!-- Placeholder: reuse vanilla industrial medicine's art until Ward ships its own. -->
<texPath>Things/Item/Resource/Medicine/MedicineIndustrial</texPath>
<graphicClass>Graphic_StackCount</graphicClass>
</graphicData>
<stackLimit>25</stackLimit>
<healthAffectsPrice>false</healthAffectsPrice>
<statBases>
<MarketValue>14</MarketValue>
<Mass>0.05</Mass>
<Flammability>0.7</Flammability>
<DeteriorationRate>2</DeteriorationRate>
</statBases>
<thingCategories>
<li>Medicine</li>
</thingCategories>
<tradeTags>
<li>Medicine</li>
</tradeTags>
</ThingDef>
<RecipeDef>
<defName>Ward_MakeSedative</defName>
<label>make sedatives</label>
<description>Synthesize a batch of sedatives from neutroamine.</description>
<jobString>Making sedatives.</jobString>
<workAmount>800</workAmount>
<workSpeedStat>DrugSynthesisSpeed</workSpeedStat>
<effectWorking>Cook</effectWorking>
<soundWorking>Recipe_Drug</soundWorking>
<workSkill>Intellectual</workSkill>
<ingredients>
<li>
<filter>
<thingDefs>
<li>Neutroamine</li>
</thingDefs>
</filter>
<count>1</count>
</li>
</ingredients>
<fixedIngredientFilter>
<thingDefs>
<li>Neutroamine</li>
</thingDefs>
</fixedIngredientFilter>
<products>
<Ward_Sedative>4</Ward_Sedative>
</products>
<recipeUsers>
<li>DrugLab</li>
</recipeUsers>
<researchPrerequisite>DrugProduction</researchPrerequisite>
</RecipeDef>
<!--
The sedated state: very calm (a large negative break-threshold offset = breaks far less), but
groggy and barely productive, and gone in about a day. Deliberately a bad hediff — it is a crutch
with a cost, not free stability.
-->
<HediffDef>
<defName>Ward_Sedated</defName>
<label>sedated</label>
<description>Dosed with a sedative. Very unlikely to break — and too groggy to do much of anything.</description>
<hediffClass>HediffWithComps</hediffClass>
<defaultLabelColor>(0.6, 0.6, 0.9)</defaultLabelColor>
<scenarioCanAdd>false</scenarioCanAdd>
<isBad>true</isBad>
<maxSeverity>1.0</maxSeverity>
<comps>
<!-- About one in-game day from a full dose to nothing. -->
<li Class="HediffCompProperties_SeverityPerDay">
<severityPerDay>-1.0</severityPerDay>
</li>
</comps>
<stages>
<li>
<statOffsets>
<!-- Negative = breaks at a much lower mood = breaks far less. -->
<MentalBreakThreshold>-0.40</MentalBreakThreshold>
</statOffsets>
<capMods>
<li>
<capacity>Consciousness</capacity>
<offset>-0.25</offset>
</li>
<li>
<capacity>Moving</capacity>
<offset>-0.15</offset>
</li>
</capMods>
</li>
</stages>
</HediffDef>
<!--
A NEW warden work-giver, above treatment in priority: a crisis pre-empts a counselling session.
Like the treatment giver it inherits WorkGiver_Warden.ShouldSkip, so it respects prison-commons
areas for free.
-->
<WorkGiverDef>
<defName>Ward_WardenAdministerSedative</defName>
<label>administer sedative</label>
<giverClass>Ward.WorkGiver_Warden_Sedate</giverClass>
<workType>Warden</workType>
<verb>sedate</verb>
<gerund>sedating</gerund>
<priorityInType>90</priorityInType>
<requiredCapacities>
<li>Manipulation</li>
</requiredCapacities>
</WorkGiverDef>
<JobDef>
<defName>Ward_AdministerSedative</defName>
<driverClass>Ward.JobDriver_AdministerSedative</driverClass>
<reportString>administering a sedative to TargetA.</reportString>
<casualInterruptible>false</casualInterruptible>
</JobDef>
</Defs>
+165
View File
@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<!--
The warden's job. This is a NEW WorkGiver class, which is the reason Ward survives contact
with the prison-mod ecosystem:
- Custom Prisoner Interactions patches the named vanilla warden givers (_Chat, _Convert,
_Enslave, _ReleasePrisoner). It cannot see this one, so it cannot break it.
- Prison Commons patches WorkGiver_Warden.ShouldSkip on the BASE class. Ward inherits
from WorkGiver_Warden and does not override ShouldSkip, so Ward's warden work respects
prison-commons areas for free, without Ward knowing Prison Commons exists.
-->
<WorkGiverDef>
<defName>Ward_WardenPsychiatricCare</defName>
<label>provide psychiatric care</label>
<giverClass>Ward.WorkGiver_Warden_PsychiatricCare</giverClass>
<workType>Warden</workType>
<verb>treat</verb>
<gerund>treating</gerund>
<!-- Above chat (60) and below feeding: an untreated patient deteriorates, a bored one doesn't. -->
<priorityInType>70</priorityInType>
<requiredCapacities>
<li>Talking</li>
<li>Hearing</li>
</requiredCapacities>
</WorkGiverDef>
<!-- Distinct from the PrisonerInteractionModeDef's defName: [DefOf] binds fields to defNames
by name, so two defs sharing one name cannot both be reached from a DefOf class. -->
<JobDef>
<defName>Ward_ProvidePsychiatricCare</defName>
<driverClass>Ward.JobDriver_PsychiatricCare</driverClass>
<reportString>treating TargetA.</reportString>
<casualInterruptible>false</casualInterruptible>
</JobDef>
<!--
What treatment actually buys the patient: a lower chance of breaking. Severity decays, so
an untreated patient slides back. This is the entire feedback loop: treat them and they
stabilise, neglect them and they don't.
-->
<HediffDef>
<defName>Ward_UnderTreatment</defName>
<label>under psychiatric care</label>
<description>This person is receiving regular counselling. They are less likely to suffer a mental break.</description>
<hediffClass>HediffWithComps</hediffClass>
<defaultLabelColor>(0.6, 0.8, 0.9)</defaultLabelColor>
<scenarioCanAdd>false</scenarioCanAdd>
<maxSeverity>1.0</maxSeverity>
<isBad>false</isBad>
<comps>
<!-- Roughly a week from a full course back to nothing, with no wardens attending. -->
<li Class="HediffCompProperties_SeverityPerDay">
<severityPerDay>-0.15</severityPerDay>
</li>
</comps>
<stages>
<li>
<minSeverity>0</minSeverity>
<statOffsets>
<!-- Negative offset = breaks at a LOWER mood = breaks less often. -->
<MentalBreakThreshold>-0.10</MentalBreakThreshold>
</statOffsets>
</li>
</stages>
</HediffDef>
<ThoughtDef>
<defName>Ward_Counselled</defName>
<thoughtClass>Thought_Memory</thoughtClass>
<durationDays>2</durationDays>
<stackLimit>3</stackLimit>
<stages>
<li>
<label>counselled</label>
<description>Someone sat with me and listened. It helped, a little.</description>
<baseMoodEffect>6</baseMoodEffect>
</li>
</stages>
</ThoughtDef>
<!--
The other half of the design. A ward you do not staff is not a neutral place to keep
someone. It is a worse one. Held under psychiatric care and left untreated, the patient
resents it, which pushes them back toward the break that got them committed.
-->
<ThoughtDef>
<defName>Ward_Neglected</defName>
<thoughtClass>Thought_Memory</thoughtClass>
<durationDays>3</durationDays>
<stackLimit>4</stackLimit>
<stages>
<li>
<label>left in the ward</label>
<description>They locked me in here for my own good and then forgot about me.</description>
<baseMoodEffect>-8</baseMoodEffect>
</li>
</stages>
</ThoughtDef>
<!--
Recovery: the arc that turns commitment from a one-way park into a treatment with an ending.
It only advances once the patient has no treatable condition left (see the job) and, like
everything here, decays without sustained attention. When it reaches the "stabilised" stage the
discharge alert fires: the patient is well enough to let go.
-->
<HediffDef>
<defName>Ward_Recovery</defName>
<label>recovering</label>
<description>This person has worked through what they were committed for and is stabilising. Once fully recovered they can safely be discharged — held longer, they will start to resent it.</description>
<hediffClass>HediffWithComps</hediffClass>
<defaultLabelColor>(0.55, 0.85, 0.65)</defaultLabelColor>
<scenarioCanAdd>false</scenarioCanAdd>
<maxSeverity>1.0</maxSeverity>
<isBad>false</isBad>
<comps>
<!-- Recovery has to be sustained too: a couple of days of no attention and it slips back. -->
<li Class="HediffCompProperties_SeverityPerDay">
<severityPerDay>-0.10</severityPerDay>
</li>
</comps>
<stages>
<li>
<minSeverity>0</minSeverity>
<label>on the mend</label>
</li>
<li>
<minSeverity>0.999</minSeverity>
<label>stabilised</label>
</li>
</stages>
</HediffDef>
<!-- Applied as recovery progresses: the patient can feel it working. -->
<ThoughtDef>
<defName>Ward_Recovering</defName>
<thoughtClass>Thought_Memory</thoughtClass>
<durationDays>2</durationDays>
<stackLimit>3</stackLimit>
<stages>
<li>
<label>on the mend</label>
<description>Whatever was wrong with me, it's easing. I think I might be getting better.</description>
<baseMoodEffect>4</baseMoodEffect>
</li>
</stages>
</ThoughtDef>
<!-- A breakthrough: the rare day the work pays off all at once. -->
<ThoughtDef>
<defName>Ward_Breakthrough</defName>
<thoughtClass>Thought_Memory</thoughtClass>
<durationDays>5</durationDays>
<stackLimit>1</stackLimit>
<stages>
<li>
<label>a breakthrough</label>
<description>Something finally shifted. For the first time in a long while, I feel like myself.</description>
<baseMoodEffect>10</baseMoodEffect>
</li>
</stages>
</ThoughtDef>
</Defs>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<!--
The whole mod hangs off this def.
RimWorld.GuestStatus is a compiled 3-value enum (Guest / Prisoner / Slave). A mod cannot
add a fourth. That is exactly why Hospitality carries ~74 Harmony patches: it wants a pawn
who is in your colony but is not your colonist, the enum has no slot for one, so it keeps a
shadow guest system alive by patching bed validity, allowed areas and the work JobGiver.
Ward does not pay that tax. A committed pawn IS a prisoner in vanilla's own sense of the
word. Arresting your own colonist already performs the colonist to prisoner transition.
All Ward adds is a new *mode* to hold them under, and PrisonerInteractionModeDef is a Def,
not an enum. Prison Labor already proves this works: it ships seven of them.
Non-exclusive, so it stacks rather than competing for the radio button. That is what lets
Ward coexist with Prison Labor's work modes and with Custom Prisoner Interactions instead
of fighting them for the same slot.
-->
<PrisonerInteractionModeDef>
<defName>Ward_PsychiatricCare</defName>
<label>psychiatric care</label>
<listOrder>150</listOrder> <!-- between ReduceResistance (100) and Release (200) -->
<isNonExclusiveInteraction>true</isNonExclusiveInteraction>
<mustBeAwake>false</mustBeAwake>
<allowOnWildMan>false</allowOnWildMan>
<allowInClassicIdeoMode>true</allowInClassicIdeoMode>
</PrisonerInteractionModeDef>
</Defs>
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<!--
RoomRoleDef.workerClass is a public field, so a new room role is XML plus one worker class
and zero patches. Vanilla already ships PrisonCell, PrisonBarracks and Hospital on exactly
this rail; the ward is the fourth sibling.
The score has to beat PrisonCell's, or a ward full of prisoner beds just reads as a cell.
RoomRoleWorker_PsychWard only scores at all when a treatment station is present, so an
ordinary prison cell is untouched and keeps its vanilla role.
-->
<RoomRoleDef>
<defName>Ward_PsychWard</defName>
<label>psychiatric ward</label>
<workerClass>Ward.RoomRoleWorker_PsychWard</workerClass>
<relatedStats>
<li>Impressiveness</li>
<li>Cleanliness</li>
<li>Space</li>
</relatedStats>
</RoomRoleDef>
</Defs>
+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<!--
The thing that turns a room full of prisoner beds into a ward rather than a cell block.
It is deliberately cheap: the cost of a ward is not the furniture, it is the warden hours
and the labour you give up by not putting the patient to work.
-->
<ThingDef ParentName="BuildingBase">
<defName>Ward_TreatmentStation</defName>
<label>treatment station</label>
<description>A desk, a chair bolted to the floor, and a locked cabinet of sedatives. A warden set to psychiatric care will use it to counsel patients held in this room.\n\nA room containing one is treated as a psychiatric ward rather than a prison cell. Whether that is a mercy depends entirely on how you run it.</description>
<thingClass>Building</thingClass>
<graphicData>
<texPath>Things/Building/Ward/TreatmentStation</texPath>
<!-- Single, not Multi: one texture, no _north/_east/_south/_west set to keep in sync. -->
<graphicClass>Graphic_Single</graphicClass>
<drawSize>(2,1)</drawSize>
<damageData>
<rect>(0,0.05,2,0.9)</rect>
</damageData>
</graphicData>
<castEdgeShadows>true</castEdgeShadows>
<altitudeLayer>Building</altitudeLayer>
<passability>PassThroughOnly</passability>
<pathCost>60</pathCost>
<blockWind>false</blockWind>
<fillPercent>0.4</fillPercent>
<statBases>
<MaxHitPoints>120</MaxHitPoints>
<WorkToBuild>1600</WorkToBuild>
<Flammability>1.0</Flammability>
<Beauty>0</Beauty>
</statBases>
<size>(2,1)</size>
<costList>
<Steel>40</Steel>
<MedicineIndustrial>2</MedicineIndustrial>
</costList>
<researchPrerequisites>
<li>MedicineProduction</li>
</researchPrerequisites>
<designationCategory>Misc</designationCategory>
<rotatable>true</rotatable>
<selectable>true</selectable>
<terrainAffordanceNeeded>Light</terrainAffordanceNeeded>
<building>
<ai_chillDestination>false</ai_chillDestination>
</building>
</ThingDef>
</Defs>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<WorkGiverDef>
<defName>Institution_Investigate</defName>
<label>investigate crimes</label>
<giverClass>Contraband.WorkGiver_Investigate</giverClass>
<workType>Institution_Investigating</workType>
<verb>investigate</verb>
<gerund>investigating</gerund>
<priorityInType>100</priorityInType>
<requiredCapacities>
<li>Manipulation</li>
<li>Sight</li>
</requiredCapacities>
</WorkGiverDef>
</Defs>
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Defs>
<WorkTypeDef>
<defName>Institution_Investigating</defName>
<labelShort>police</labelShort>
<pawnLabel>Constable</pawnLabel>
<gerundLabel>investigating</gerundLabel>
<description>Working open crime scenes to gather evidence and identify the culprit.</description>
<verb>investigate</verb>
<naturalPriority>455</naturalPriority>
<relevantSkills>
<li>Intellectual</li>
<li>Social</li>
</relevantSkills>
<workTags>
<li>Intellectual</li>
<li>Social</li>
</workTags>
</WorkTypeDef>
</Defs>
+6 -4
View File
@@ -20,15 +20,17 @@ Arrest is the hinge. Nothing hard-codes "prisoner" — every held-person system
|---|---|---|---|
| [Institution: Core](https://git.onetick.ninja/flan/rimworld-core) | the shared engine — **propensity** (nature × nurture), the **criminal record**, the **secured-context** predicate. Invisible on its own | — | shipping |
| [Institution: Contraband](https://git.onetick.ninja/flan/rimworld-contraband) | the physical smuggling loop — conceal / improvise shivs / Prison-Architect tunnels / warden search / corruption | Core | shipping |
| [Institution: Justice](https://git.onetick.ninja/flan/rimworld-justice) | corrections & policing — classification, deterrence, discipline, parole, regime (a policing/cops layer is planned here) | Core, Harmony | shipping |
| [Institution: Justice](https://git.onetick.ninja/flan/rimworld-justice) | corrections & **policing** — classification, deterrence, discipline & reform, parole, regime, and colony crime → witnesses → a weighed arrest | Core, Harmony | shipping |
| [Institution: Gangs](https://git.onetick.ninja/flan/rimworld-gangs) | gangs as **contraband economies** — joining by disposition, smuggling networks, rivalry, fights-as-crime | Core, Contraband, Justice | shipping |
| [Institution: Ward](https://git.onetick.ninja/flan/rimworld-ward) | involuntary psychiatric commitment — the third pawn state, neither free nor guilty; also carries the suite's in-game test harness | — | shipping |
| [Institution: Ward](https://git.onetick.ninja/flan/rimworld-ward) | involuntary **psychiatric care** — a prisoner committed for treatment, not punishment, on Core's shared treatment engine: counselling reduces the disorder, a recovery track leads to discharge, and neglect deteriorates. Folded into the suite as its psychiatric-care layer | Core, Harmony | shipping |
| [Foul Play](https://git.onetick.ninja/flan/rimworld-foulplay) | throw a container of something off a body; a Noita-flasks liquid framework (vessels + substances) with the **Piss Nuke** flagship; bridges to the suite | standalone | shipping |
The original monolith split along its natural seams in 2026-07: the substrate became **Core**, the
corrections/justice systems became **Justice**, and the gang layer became **Gangs**, leaving
**Contraband** as the physical smuggling loop. Regime (prisoner recreation) folded into Justice; a
policing/cops layer is where Justice grows next.
**Contraband** as the physical smuggling loop. Regime (prisoner recreation) folded into Justice, which
now also ships **policing** (colony crime → witnesses → a weighed arrest). **Ward** — once a standalone
sister mod — is folded in as the psychiatric-care layer, consuming Core's shared treatment engine, so
the suite is now five layers over one Core, bundled into a single install with a toggle each.
## Principles
+8 -1
View File
@@ -5,7 +5,7 @@ namespace InstitutionSuite
{
/// <summary>
/// The single player-facing mod. It bundles the Institution feature layers -- Contraband,
/// Justice, Gangs -- into one install with a checkbox per layer, and it owns NO gameplay code of
/// Justice, Gangs, Ward -- into one install with a checkbox per layer, and it owns NO gameplay code of
/// its own. All it does is read the player's choices and write them into Core's shared
/// <see cref="Contraband.InstitutionSettings"/>, which the feature assemblies (shipped alongside
/// in this same mod) read at their entry points.
@@ -46,6 +46,10 @@ namespace InstitutionSuite
l.CheckboxLabeled("Policing (colony crime)",
ref settings.policing,
"The pre-arrest layer: your own colonists commit crimes on a spectrum (rarer in small, tight colonies), witnesses and a constable's work name the culprit, and a weighed arrest can end in a cell -- or a resisted breakout. A big behavioural change; turn it off to keep the rest of Justice without colony crime.");
l.Gap();
l.CheckboxLabeled("Ward (psychiatric care)",
ref settings.ward,
"Commit a prisoner for treatment instead of punishment: sustained counselling in a proper ward reduces the disorder that broke them and builds a recovery track toward discharge. Neglect a committed patient and they deteriorate. Uses Harmony.");
l.End();
settings.Apply(); // live: a toggle takes effect immediately for the gated behaviour
}
@@ -57,6 +61,7 @@ namespace InstitutionSuite
public bool justice = true;
public bool gangs = true;
public bool policing = true;
public bool ward = true;
/// <summary>Mirror the choices into Core's shared flags that every feature reads.</summary>
public void Apply()
@@ -65,6 +70,7 @@ namespace InstitutionSuite
Contraband.InstitutionSettings.justice = justice;
Contraband.InstitutionSettings.gangs = gangs;
Contraband.InstitutionSettings.policing = policing;
Contraband.InstitutionSettings.ward = ward;
}
public override void ExposeData()
@@ -73,6 +79,7 @@ namespace InstitutionSuite
Scribe_Values.Look(ref justice, "justice", true);
Scribe_Values.Look(ref gangs, "gangs", true);
Scribe_Values.Look(ref policing, "policing", true);
Scribe_Values.Look(ref ward, "ward", true);
base.ExposeData();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+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/^/ /'