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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,25 @@ posted on the Steam Workshop for that version, sorted into Added, Changed, Remov

## [Unreleased]

### Added

- A mod setting, **New stands are used for shift changes**, on by default. Turn it off and every
outfit stand built from then on starts as "Not used for shift changes": ordinary furniture
until you set it up with its switch. Stands that already exist keep their own setting, whichever
way it is set.
- Hovering a stand's switch or its Set owner button now marks that stand on the map with the
pulsing square vanilla shows on a target you are about to pick. An arrow points to it as well,
as it does when you hover a storage link. With several stands selected, each stand shows its
own pair of buttons, and until now nothing on them said which stand they belong to.

### Fixed

- The description of **Unassigned stands are shared** said that with it off, an unassigned stand
behaves exactly like vanilla furniture. It does not: it dresses nobody, but
"Allow removing items" stays off and its contents stay out of trade. The description now says
so, and points at "Not used for shift changes" for a stand that should be ordinary furniture.
How the setting works is unchanged.

- If a stand's owner was kidnapped while the stand was boxed up for a move, setting it down made
them its owner again and kept the shift they had open at it, so nobody else could use it until
they came home. A kidnapped owner is now dropped when the stand is placed, as they already were
Expand Down
4 changes: 3 additions & 1 deletion Languages/English/Keyed/ShiftChange.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,10 @@
<ShiftChange.VacuumStandAcrossVacuum>{0} is heading through vacuum in work clothes that protect them less than their own. Their own clothes are across vacuum too, so they will not go back to change first.</ShiftChange.VacuumStandAcrossVacuum>
<ShiftChange.VacuumStandElsewhere>{0} is heading through vacuum in work clothes that protect them less than their own, and their own clothes are not on this map.</ShiftChange.VacuumStandElsewhere>

<ShiftChange.SettingNewStandsInService>New stands are used for shift changes</ShiftChange.SettingNewStandsInService>
<ShiftChange.SettingNewStandsInServiceDesc>When on, a newly built outfit stand reads its room and dresses whoever comes to work, relax or sleep there.\n\nWhen off, a newly built stand starts as 'Not used for shift changes' and stays ordinary furniture until you set it up with its switch. Use this when most of your stands are for storage or display.\n\nOnly stands built after you change this are affected. Stands that already exist keep their own setting.</ShiftChange.SettingNewStandsInServiceDesc>
<ShiftChange.SettingPoolUnassigned>Unassigned stands are shared</ShiftChange.SettingPoolUnassigned>
<ShiftChange.SettingPoolUnassignedDesc>When on, an outfit stand with no assigned owner may be used by any colonist about to do what it serves.\n\nWhen off, only stands with an explicit owner list ever dress anyone, and every unassigned stand behaves exactly like vanilla furniture.</ShiftChange.SettingPoolUnassignedDesc>
<ShiftChange.SettingPoolUnassignedDesc>When on, an outfit stand with no assigned owner may be used by any colonist about to do what it serves.\n\nWhen off, only stands with an explicit owner list ever dress anyone. An unassigned stand still counts as a shift stand, though: 'Allow removing items' stays off, and its contents stay out of trade if 'Keep contents out of trade' is ticked. To make a stand ordinary furniture, set it to 'Not used for shift changes'.</ShiftChange.SettingPoolUnassignedDesc>
<ShiftChange.SettingKeepDecent>Keep colonists decent</ShiftChange.SettingKeepDecent>
<ShiftChange.SettingKeepDecentDesc>Off by default, so nothing changes in a colony you are already playing.\n\nTurn it on and a stand will not leave a colonist without basic clothing: if what it hands out does not cover them, their innermost garments stay on and the rest of the swap happens as normal. That mostly matters with Change the whole outfit ticked, where a stand holding only a hat or a belt would otherwise send someone out with nothing else on.\n\nNudists, and anyone whose ideoligion prefers nudity, are exempt either way.</ShiftChange.SettingKeepDecentDesc>
<ShiftChange.SettingMedicalEmergencies>Medical emergencies change first</ShiftChange.SettingMedicalEmergencies>
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,17 @@ stands are the mechanized ones from
their own, raid or no raid, but "on their own next job" can be a long time if
the job they just started was sleep.

Three mod settings, under Options → Mod settings:
Four mod settings, under Options → Mod settings:

- **New stands are used for shift changes**, on by default. Turn it off and
every stand built from then on starts as "Not used for shift changes", so
stands kept for storage or display need nothing done to them, and you switch
on only the ones you want. Stands already built keep their own setting.
- **Unassigned stands are shared**, on by default. Turn it off and only stands
with an explicit owner list ever dress anyone.
with an explicit owner list ever dress anyone. An unassigned stand is still a
shift stand, though: "Allow removing items" stays off and its contents stay
out of trade. "Not used for shift changes" is what makes a stand ordinary
furniture.
- **Keep colonists decent**, off by default, described further up.
- **Medical emergencies change first**, off by default. On, a doctor answering
an emergency tend puts scrubs on first and a colonist in critical condition
Expand Down
9 changes: 8 additions & 1 deletion Source/ShiftChange/Dev/DebugTools_Fixtures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ internal static class DebugTools_Fixtures
/// <summary>The green tint of the researcher's duster.</summary>
internal static readonly Color DusterGreen = new Color(0.62f, 0.9f, 0.7f);

/// <summary>Spawns a player-faction thing with no style and graphic variant 0.</summary>
/// <summary>Spawns a player-faction thing with no style and graphic variant 0. A stand
/// starts in service whatever the new-stand setting says, so a fixture or a stage does not
/// depend on the profile it runs in.</summary>
internal static Thing Spawn(Map map, ThingDef def, ThingDef stuff, IntVec3 cell, Rot4 rot)
{
Thing thing = ThingMaker.MakeThing(def, stuff);
CompShiftStand stand = thing.TryGetComp<CompShiftStand>();
if (stand != null)
{
stand.excluded = false;
}
thing.SetFactionDirect(Faction.OfPlayer);
thing.SetStyleDef(null);
thing.overrideGraphicIndex = 0;
Expand Down
5 changes: 5 additions & 0 deletions Source/ShiftChange/Harness/DebugTools_LifecycleHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,9 @@ internal static bool Run(Map map, IntVec3 origin, bool toast)
Case(map, pad, "the medical emergency setting covers doctors and patients, never fires",
(m, p) => Stage(m, p, StageKit.Displacing, enclose: true),
MedicalEmergencySettingCoversTheRightWork);
Case(map, pad, "the new-stand setting decides what a stand made from then on starts as",
(m, p) => Stage(m, p, StageKit.Displacing, enclose: true),
NewStandSettingDecidesOnlyNewStands);
Case(map, pad, "the job classifier sorts every job shape in the arms' order",
(m, p) => Stage(m, p, StageKit.Displacing, enclose: true),
HarnessJobKinds.ClassifierSortsEveryJobShape);
Expand Down Expand Up @@ -422,6 +425,8 @@ internal static bool Run(Map map, IntVec3 origin, bool toast)
(m, p) => Stage(m, p, StageKit.Displacing, enclose: true), SwitchNamesWhatTheStandServes);
Case(map, pad, "selecting several stands shows each stand's own buttons",
(m, p) => Stage(m, p, StageKit.Displacing, enclose: true), StandButtonsStaySeparate);
Case(map, pad, "hovering a stand's button marks that stand and no other",
(m, p) => Stage(m, p, StageKit.Displacing, enclose: true), HoveredButtonsMarkTheirOwnStand);
Case(map, pad, "fishing dresses at a stand ticked for it, and at a radius stand by the shore",
(m, p) => StageWithDoorway(m, p, DefDatabase<WorkTypeDef>.GetNamedSilentFail("Fishing")),
FishingDressesAtAStandTickedForIt);
Expand Down
16 changes: 14 additions & 2 deletions Source/ShiftChange/Harness/DebugTools_SaveRoundTrip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,18 @@ internal static bool RoundTrip(Map map, CellRect pad)
&& saved.Contains("<shiftChangeServeRadius>9</shiftChangeServeRadius>"),
"the save carries the catchment under its prefixed keys");

SavedGameLoaderNow.LoadGameFromSaveFileNow(SaveName);
// Loaded with the new-stand setting off, which must not reach a stand that already
// exists: the setting is read only as a stand is made.
bool newStandsWas = ShiftChangeMod.Settings.newStandsInService;
ShiftChangeMod.Settings.newStandsInService = false;
try
{
SavedGameLoaderNow.LoadGameFromSaveFileNow(SaveName);
}
finally
{
ShiftChangeMod.Settings.newStandsInService = newStandsWas;
}

// Positive control for the absence assertions, logged after the load so that a load
// which cleared the queue could not hide it.
Expand Down Expand Up @@ -265,7 +276,8 @@ internal static bool RoundTrip(Map map, CellRect pad)
"and the load put it back in the registry")
& DebugTools_LifecycleHarness.Expect(
!ledger.IsExcluded,
"the loaded stand is in service (control for the sweep below)")
"the loaded stand is in service though the new-stand setting was off "
+ "(and the control for the sweep below)")
& DebugTools_LifecycleHarness.Expect(
Patch_AllowRemovingToggle.AllowRemovingItemsRef != null
&& !Patch_AllowRemovingToggle.AllowRemovingItemsRef(loaded),
Expand Down
89 changes: 87 additions & 2 deletions Source/ShiftChange/Harness/HarnessGizmos.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
// 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 System.Linq;
using HarmonyLib;
using RimWorld;
using UnityEngine;
using Verse;
using static ShiftChange.DebugTools_LifecycleHarness;
using static ShiftChange.HarnessFixtures;

namespace ShiftChange
{
/// <summary>
/// Cases for the stand's buttons: what the switch says, and that selecting several stands keeps
/// each stand's buttons its own. Read from the comps' gizmos, never drawn.
/// Cases for the stand's buttons: what the switch says, that selecting several stands keeps
/// each stand's buttons its own, and that hovering one marks its stand. Read from the comps'
/// gizmos and from what vanilla queues to draw, never from the screen.
/// </summary>
internal static class HarnessGizmos
{
Expand Down Expand Up @@ -117,6 +121,87 @@ internal static bool StandButtonsStaySeparate(Fixture fix)
&& !myOwner.GroupsWith(theirOwner) && !theirOwner.GroupsWith(myOwner),
"and those stay separate too");
}

/// <summary>
/// Hovering a stand's switch or Set owner button marks that stand and no other, so with
/// several stands selected a player can find whose button is whose. The arrow is read
/// from vanilla's list, which the next frame draws and empties. The square is read from
/// the targeting material, whose colour vanilla rewrites each time it hands it out to
/// draw, so a colour set beforehand changes only if the square was drawn. A direct call
/// for the other stand is the control that both are read right.
/// </summary>
internal static bool HoveredButtonsMarkTheirOwnStand(Fixture fix)
{
ThingDef standDef = DefDatabase<ThingDef>.GetNamedSilentFail("Building_OutfitStand");
List<Vector3> arrows = AccessTools.Field(typeof(TargetHighlighter), "arrowPositions")
?.GetValue(null) as List<Vector3>;
Material square = GenDraw.CurTargetingMat;
if (!Expect(standDef != null, "the stand def resolves")
|| !Expect(arrows != null, "vanilla's arrow list resolves")
|| !Expect(fix.Map == Find.CurrentMap, "the fixture's map is the current map (control)"))
{
return false;
}
Building_OutfitStand other = DebugTools_Fixtures.Spawn(fix.Map, standDef, ThingDefOf.WoodLog,
fix.Stand.Position + new IntVec3(2, 0, 0), Rot4.North) as Building_OutfitStand;
CompShiftStand otherComp = other?.TryGetComp<CompShiftStand>();
if (otherComp == null)
{
return Expect(false, "a second stand could be staged");
}

void Reset()
{
arrows.Clear();
// Vanilla hands the square out opaque, never clear.
square.color = Color.clear;
}
bool Squared()
{
return square.color != Color.clear;
}
bool Marks(Thing stand)
{
return arrows.Any(a => (a - stand.DrawPos).Yto0().magnitude < 0.6f);
}
string Seen()
{
return " (" + arrows.Count + " arrows, square " + (Squared() ? "drawn" : "not drawn") + ")";
}
bool OnlyMarks(Command_Action button, Thing mine, Thing theirs, string name)
{
Reset();
if (!Expect(button?.onHover != null, name + " has a hover action"))
{
return false;
}
button.onHover();
return Expect(Marks(mine) && !Marks(theirs) && Squared(),
name + " marks its own stand and not the other" + Seen());
}

Reset();
bool ok = Expect(!Squared(), "the square's material keeps a colour set on it (control)");
CompShiftStand.PointAt(other);
ok = ok
& Expect(Marks(other) && !Marks(fix.Stand) && Squared(),
"a direct call for the second stand marks that stand only (control)" + Seen())
& OnlyMarks(Switch(fix.Comp), fix.Stand, other, "the first stand's switch")
& OnlyMarks(Switch(otherComp), other, fix.Stand, "the second stand's switch")
& OnlyMarks(OwnerButton(fix.Stand), fix.Stand, other, "the first stand's Set owner")
& OnlyMarks(OwnerButton(other), other, fix.Stand, "the second stand's Set owner");

// Vanilla keeps calling the last hovered button while another main tab is open, so a
// button can outlive its stand.
Command_Action stale = Switch(otherComp);
other.Destroy();
Reset();
stale?.onHover?.Invoke();
ok &= Expect(stale != null && arrows.Count == 0 && !Squared(),
"a button kept from a destroyed stand marks nothing" + Seen());
arrows.Clear();
return ok;
}
}
}
#endif
45 changes: 45 additions & 0 deletions Source/ShiftChange/Harness/HarnessTriggers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,51 @@ internal static bool MedicalEmergencySettingCoversTheRightWork(Fixture fix)
"the setting was restored for the cases that follow");
}

/// <summary>
/// The new-stand setting decides what a stand made from then on starts as, through
/// <c>ThingMaker.MakeThing</c>, the call a finished frame makes. A stand made with it off
/// spawns as vanilla furniture; the fixture's stand, made before the flip, stays in
/// service (control). That a load never applies it is asserted in
/// <see cref="DebugTools_SaveRoundTrip.RoundTrip"/>.
/// </summary>
internal static bool NewStandSettingDecidesOnlyNewStands(Fixture fix)
{
bool was = ShiftChangeMod.Settings.newStandsInService;
bool ok = Expect(was, "the setting ships on, which is the default under test");
try
{
CompShiftStand on = (ThingMaker.MakeThing(fix.Stand.def, ThingDefOf.WoodLog)
as ThingWithComps)?.TryGetComp<CompShiftStand>();
ok &= Expect(on != null && on.IsAutomatic, "with it on a new stand starts automatic");

ShiftChangeMod.Settings.newStandsInService = false;
Building_OutfitStand stand =
ThingMaker.MakeThing(fix.Stand.def, ThingDefOf.WoodLog) as Building_OutfitStand;
CompShiftStand off = stand?.TryGetComp<CompShiftStand>();
if (off == null || Patch_AllowRemovingToggle.AllowRemovingItemsRef == null)
{
return ok & Expect(false, "a stand and its removal flag could be made with it off");
}
ok &= Expect(off.IsExcluded, "with it off a new stand starts as not used for shift changes");

// Spawned with the removal flag on: a stand in service would force it off here.
stand.SetFactionDirect(Faction.OfPlayer);
Patch_AllowRemovingToggle.AllowRemovingItemsRef(stand) = true;
GenSpawn.Spawn(stand, fix.Stand.Position + new IntVec3(2, 0, 0), fix.Map, Rot4.North);
ok &= Expect(off.IsExcluded, "and spawning it keeps it so")
& Expect(Patch_AllowRemovingToggle.AllowRemovingItemsRef(stand),
"its Allow removing items stays as set, as on a vanilla stand")
& Expect(!off.BlocksTrade, "and its contents can be traded")
& Expect(!fix.Comp.IsExcluded, "while the stand made before the flip is still in service");
}
finally
{
ShiftChangeMod.Settings.newStandsInService = was;
}
return ok & Expect(ShiftChangeMod.Settings.newStandsInService,
"the setting was restored for the cases that follow");
}

/// <summary>
/// Deposit only: a colonist parks what the stand's filter accepts, keeps the rest of their
/// clothes on, and gets it all back on the return trip. The stand must be claimed at all:
Expand Down
10 changes: 9 additions & 1 deletion Source/ShiftChange/Ownership/CompAssignableToPawn_ShiftStand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,8 @@ protected override bool CanSetUninstallAssignedPawn(Pawn pawn)
}

/// <summary>
/// The base's Set owner gizmo with its hotkey stripped and its window swapped for ours.
/// The base's Set owner gizmo with its hotkey stripped, its window swapped for ours, and
/// the stand marked on the map while it is hovered.
/// <see cref="Patch_ForeignOwnerGizmos"/> hides other mods' own, so this is the only one.
/// </summary>
public override IEnumerable<Gizmo> CompGetGizmosExtra()
Expand All @@ -409,6 +410,13 @@ public override IEnumerable<Gizmo> CompGetGizmosExtra()
// and a merged button opens every stand's dialog, each closing the one
// before, so it edits one list while looking as if it edits all.
assign.groupable = false;
// Chained, not replaced: a hover another mod put on the base gizmo still runs.
System.Action hovered = assign.onHover;
assign.onHover = () =>
{
hovered?.Invoke();
CompShiftStand.PointAt(parent);
};
}
yield return gizmo;
}
Expand Down
Loading
Loading