Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Source/ShiftChange/Harness/DebugTools_LifecycleHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using static ShiftChange.HarnessLifecycle;
using static ShiftChange.HarnessGizmos;
using static ShiftChange.HarnessOwnership;
using static ShiftChange.HarnessRecolor;
using static ShiftChange.HarnessRideAlong;
using static ShiftChange.HarnessScoring;
using static ShiftChange.HarnessSwap;
Expand Down Expand Up @@ -422,6 +423,9 @@ internal static bool Run(Map map, IntVec3 origin, bool toast)
ParkedKitRefusesSpares);
Case(map, pad, "but takes one once the parked kit is worn through",
WornOutParkedKitStillGoesShopping);
// A known gap unless Ideology is active.
Case(map, pad, "a recolor order goes through on shift, the optimizer's recolor does not, and none reaches a parked garment",
RecolorOrderGoesThroughOnShift);
// These two need a live map and no fixture, so they sit after the fixture cases and
// before the game-replacing ones below.
Case("a work target with no room of its own still resolves one",
Expand Down
141 changes: 141 additions & 0 deletions Source/ShiftChange/Harness/HarnessRecolor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// HARNESS only, compiled out of a plain Release build. The guard is whole-file, always
// (check-invariants.py enforces it); why: docs/DESIGN.md, "Development tooling".
#if HARNESS
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
using Verse.AI;
using static ShiftChange.DebugTools_LifecycleHarness;

namespace ShiftChange
{
/// <summary>
/// Harness case for <see cref="Patch_OptimizeApparelOnShift"/> and
/// <see cref="Patch_RecolorExecutionGuard"/>: on shift the optimizer's recolor never runs,
/// the player's order from a styling station does, and no recolor reaches a parked garment.
/// </summary>
internal static class HarnessRecolor
{
/// <summary>
/// On shift the styling station's order is taken while the optimizer's recolor is not,
/// and a recolor naming a parked garment is refused, ordered or not. Driven through the
/// giver, a job's reservations and <c>TryTakeOrderedJob</c> as the station calls it, each
/// refusal beside a control that differs only in what the guard keys on. Guards the
/// order being refused on shift, with a log warning. A known gap without Ideology, whose
/// defs the job and the station are.
/// </summary>
internal static bool RecolorOrderGoesThroughOnShift(Fixture fix)
{
if (!ModsConfig.IdeologyActive || JobDefOf.RecolorApparel == null
|| ThingDefOf.StylingStation == null || ThingDefOf.Dye == null)
{
return ExpectKnownGap(false, "a recolor order goes through on shift",
"Ideology is not active on this mod list; add ludeon.rimworld.ideology to it");
}
Pawn pawn = fix.Pawn;
Apparel uniform = fix.Comp.issuedUniform.Count > 0 ? fix.Comp.issuedUniform[0] : null;
Apparel parked = HarnessScoring.ParkedGarment(fix);
bool ok = Expect(CompShiftStand.OnShiftStandFor(pawn) == fix.Comp,
"the pawn is on shift (precondition)")
& Expect(uniform != null && pawn.apparel.WornApparel.Contains(uniform),
"and wears the stand's uniform (precondition)")
& Expect(parked != null && parked.ParentHolder is Building_OutfitStand,
"and their own garment is parked in the stand (precondition)");
if (!ok)
{
return false;
}

// The station across the pad, its interaction cell free, and dye beside it.
DebugTools_Fixtures.Spawn(fix.Map, ThingDefOf.StylingStation, ThingDefOf.WoodLog,
fix.Stand.Position + new IntVec3(2, 0, 4), Rot4.North);
Thing dye = ThingMaker.MakeThing(ThingDefOf.Dye);
dye.stackCount = 10;
GenSpawn.Spawn(dye, fix.Stand.Position + new IntVec3(4, 0, 2), fix.Map);
uniform.DesiredColor = new Color(0.8f, 0.2f, 0.2f);
ok &= Expect(JobGiver_OptimizeApparel.TryCreateRecolorJob(pawn, out _, dryRun: true),
"the station would offer Recolor apparel (precondition)");

ok &= Expect(!OptimizerJob(pawn).IsValid,
"on shift the optimizer gives no job, though the uniform wants dyeing");
ok &= Expect(RefusedAtReservations(pawn, RecolorJob(pawn, ordered: false, names: null)),
"a recolor job the player did not order is refused on shift");

Job order = RecolorJob(pawn, ordered: false, names: null);
ok &= Expect(order != null && order.GetTargetQueue(TargetIndex.B).Contains(uniform),
"the station's order names the uniform");
if (order != null)
{
ok &= Expect(pawn.jobs.TryTakeOrderedJob(order, JobTag.Misc),
"the station's order is taken on shift")
& Expect(pawn.CurJobDef == JobDefOf.RecolorApparel,
"and the pawn starts it")
& Expect(pawn.apparel.WornApparel.Contains(uniform),
"still in uniform, since an order is never intercepted");
pawn.jobs.StopAll();
}
ok &= Expect(RefusedAtReservations(pawn, RecolorJob(pawn, ordered: true, names: parked)),
"an order naming the parked garment is refused");

CompShiftStand.OnShiftStands.Remove(pawn);
ThinkResult offShift = OptimizerJob(pawn);
ok &= Expect(offShift.IsValid && offShift.Job.def == JobDefOf.RecolorApparel
&& Reserves(pawn, offShift.Job),
"off shift the optimizer's recolor is given and reserves (control)")
& Expect(RefusedAtReservations(pawn, RecolorJob(pawn, ordered: false, names: parked)),
"and one naming the parked garment is still refused");
return ok;
}

/// <summary>The optimizer's answer for this pawn now, its retry delay cleared.</summary>
internal static ThinkResult OptimizerJob(Pawn pawn)
{
pawn.mindState.nextApparelOptimizeTick = Find.TickManager.TicksGame;
return new JobGiver_OptimizeApparel().TryIssueJobPackage(pawn, default(JobIssueParams));
}

/// <summary>
/// The recolor job the station builds, optionally marked as an order and pointed at one
/// garment in place of the worn ones.
/// </summary>
internal static Job RecolorJob(Pawn pawn, bool ordered, Apparel names)
{
if (!JobGiver_OptimizeApparel.TryCreateRecolorJob(pawn, out Job job) || job == null)
{
return null;
}
job.playerForced = ordered;
if (names != null)
{
List<LocalTargetInfo> queue = job.GetTargetQueue(TargetIndex.B);
queue.Clear();
queue.Add(names);
job.count = 1;
}
return job;
}

/// <summary>Whether the job's reservations succeed, releasing whatever they took.</summary>
internal static bool Reserves(Pawn pawn, Job job)
{
if (job == null)
{
return false;
}
bool reserved = job.TryMakePreToilReservations(pawn, errorOnFailed: false);
pawn.ClearReservationsForJob(job);
return reserved;
}

/// <summary>
/// Whether a job was built and then failed its reservations. A job that could not be
/// built is no refusal, so it does not count as one.
/// </summary>
internal static bool RefusedAtReservations(Pawn pawn, Job job)
{
return job != null && !Reserves(pawn, job);
}
}
}
#endif
22 changes: 14 additions & 8 deletions Source/ShiftChange/Swap/Patch_OptimizeApparelOnShift.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ namespace ShiftChange
/// optimizes the pawn's own clothes as normal.</para>
///
/// <para>The styling station's float-menu order calls <c>TryCreateRecolorJob</c> directly
/// and never passes through this giver, but <see cref="Patch_RecolorExecutionGuard"/> still
/// refuses that order while the pawn is on shift, at its reservations.</para>
/// and never passes through this giver. It is the player's order, so it proceeds on shift
/// and dyes what the pawn wears, the uniform included:
/// <see cref="Patch_RecolorExecutionGuard"/> lets it through.</para>
/// </summary>
[HarmonyPatch(typeof(JobGiver_OptimizeApparel), "TryGiveJob")]
public static class Patch_OptimizeApparelOnShift
Expand Down Expand Up @@ -68,10 +69,11 @@ public static bool Prefix(Pawn pawn, ref Job __result)
/// the game saved resumes without asking for reservations, so this does not reach it.
///
/// <para>Failing <c>TryMakePreToilReservations</c> is the clean kill: a queued job that fails
/// it is ended quietly as no longer valid, and the pawn picks another job. A player's order
/// fails it too, with a log warning. The job is blocked while the pawn is on shift (the worn
/// apparel is the uniform) and when any queued garment is parked in an outfit stand; off
/// shift, in their own clothes, recoloring runs as normal.</para>
/// it is ended quietly as no longer valid, and the pawn picks another job. The job is
/// blocked while the pawn is on shift (the worn apparel is the uniform) unless the player
/// ordered it, and whenever any queued garment is parked in an outfit stand, ordered or not,
/// since the pawn no longer wears it. Off shift, in their own clothes, recoloring runs as
/// normal.</para>
/// </summary>
[HarmonyPatch(typeof(JobDriver_RecolorApparel), nameof(JobDriver_RecolorApparel.TryMakePreToilReservations))]
public static class Patch_RecolorExecutionGuard
Expand All @@ -85,12 +87,16 @@ public static bool Prefix(JobDriver_RecolorApparel __instance, ref bool __result
{
SessionGuard.Ensure();
Pawn pawn = __instance.pawn;
if (pawn != null && CompShiftStand.OnShiftStandFor(pawn) != null)
Job job = __instance.job;
// TryTakeOrderedJob sets playerForced before it asks for reservations, and the
// flag is saved with a queued job.
bool ordered = job != null && job.playerForced;
if (pawn != null && !ordered && CompShiftStand.OnShiftStandFor(pawn) != null)
{
__result = false;
return false;
}
List<LocalTargetInfo> queue = __instance.job?.GetTargetQueue(TargetIndex.B);
List<LocalTargetInfo> queue = job?.GetTargetQueue(TargetIndex.B);
if (queue != null)
{
for (int i = 0; i < queue.Count; i++)
Expand Down
16 changes: 11 additions & 5 deletions docs/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,16 +638,22 @@ Found in play: Ideology's apparel-recolor branch rides that giver
(`JobGiver_OptimizeApparel.cs:84`) and fires whenever any *worn* item has a
`DesiredColor`. On shift that is the uniform, so a pawn walked off and permanently
dyed staged kit its favourite colour. The styling station's float-menu order
bypasses the giver, but the second guard below refuses it while the pawn is on
shift. `nextApparelOptimizeTick` is untouched, so civvies optimize normally after
bypasses the giver and proceeds, because direct orders are never blocked: on
shift it dyes what the colonist wears, the uniform included, in the colours the
player picked in the styling dialog. A uniform dyed that way goes back to its
stand in its new colours.
`nextApparelOptimizeTick` is untouched, so civvies optimize normally after
changing out.

A second guard works when a `RecolorApparel` job starts: a queued one is saved
with the pawn and survives a load, and the driver dyes whatever its queue names,
wherever it sits. Its `TryMakePreToilReservations` is failed — the vanilla-routine
way to cancel — when the pawn is on shift or any queued garment is parked in a
stand. A job already running when the game saved resumes without asking for
reservations, so the guard does not reach it.
way to cancel — when the pawn is on shift and the job is not `playerForced`, or
when any queued garment is parked in a stand, ordered or not. The order reaches
this guard too: `Pawn_JobTracker.TryTakeOrderedJob` sets `playerForced` and then
asks for reservations, and logs a warning when they fail, so the flag is what
lets it through. A job already running when the game saved resumes without
asking for reservations, so the guard does not reach it.

## Withholding from trade

Expand Down
23 changes: 20 additions & 3 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,22 @@ means anything as one: a checked-out pawn refuses a spare duplicating the kit
parked in their stand (`ParkedKitRefusesSpares`), and takes one once that kit
is worn through (`WornOutParkedKitStillGoesShopping`).

**Recolor** (`HarnessRecolor`). `RecolorOrderGoesThroughOnShift` drives
Ideology's apparel recolor through the wardrobe optimizer, a job's own
reservations, and `TryTakeOrderedJob` as the styling station calls it. On
shift, the optimizer gives nothing and a recolor job the player did not order
is refused, while the station's order is taken and the pawn starts it in
uniform. An order naming a garment parked in the stand is refused. Off shift,
the optimizer's recolor comes back and reserves, and one naming the parked
garment is still refused. The job, the station and the order are Ideology's,
so the case is a known gap on a mod list without it. The minimal list
`run-harness.sh` writes names only Odyssey as a known expansion, and the game
turns on every installed expansion it has not seen, so that list has Ideology
whenever it is installed. `--full` has it when the copied list does, and a
runner that names every expansion as known has only the ones it activates.
The case does not check that the order finishes and dyes anything: the pawn
never walks under the harness, so it stops the job once it has started.

**Tables and rooms** (`HarnessTables`). Three cases need no map and check that
the static tables still match the def database, where every lookup is
silent-fail and a renamed def empties its row with no error: the room-role
Expand Down Expand Up @@ -492,9 +508,10 @@ room under `OpenGround`. "Four engine traps" says why each is there.
find on startup — but nothing pins the two collectors that reach stand
contents in the first place, so a route quietly rerouted around the deal would
take the protection with it and say nothing.
- **Pooling on and off, the optimizer pause, the recolor guard, `SwapPlan`'s
rollback.** No cases yet. The retry cooldown is asserted on the allowed-area
path only.
- **Pooling on and off, `SwapPlan`'s rollback.** No cases yet. The retry
cooldown is asserted on the allowed-area path only. The recolor case does not
reach a recolor job that was already running when the game saved, which
resumes without asking for reservations.
- **A revert of `StaysInBed` to `pawn.InBed()`** — the specific defect the
wake-up case was written for. The fixture reaches "on a bed" by assigning
`Position`, never by running a lay-down driver, so posture stays `NotLaying`
Expand Down
5 changes: 5 additions & 0 deletions media/changelog-v1.5.3.bbcode
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[list]
[*]Fixed: with Ideology, ordering a colonist in uniform to Recolor apparel at a styling station did nothing, and left a warning in the log. The order now goes through, and the colonist dyes the clothes you picked new colours for in Change style. When those include the stand's outfit, it goes back to the stand in its new colours, and the next colonist to borrow it wears them.
[*]Fixed: if a stand's owner died while the stand was boxed up for a move, setting it down made them its owner again, so nobody else could use it. A dead owner is now dropped when the stand is placed. A stand already held for a dead colonist this way needs them removed once with Set owner.
[*]Unchanged: a colonist in uniform still never recolors anything on their own; only your order does. Off shift, in their own clothes, recoloring works as before. A living owner still gets their stand back after a move.
[/list]
7 changes: 7 additions & 0 deletions media/comment-v1.5.3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
v1.5.3 update:

Fixed: with Ideology, a colonist in uniform ordered to Recolor apparel at a styling station now does it. Colours picked for the stand's outfit stay on it when it goes back to the stand.

Fixed: a stand whose owner died while it was boxed for a move no longer goes back to them when it is set down.

Colonists in uniform still never recolor anything on their own.
Loading