diff --git a/AirlockDoor/AirlockDoor.cs b/AirlockDoor/AirlockDoor.cs
index 0c00ce70..55ff2378 100644
--- a/AirlockDoor/AirlockDoor.cs
+++ b/AirlockDoor/AirlockDoor.cs
@@ -1,487 +1,506 @@
-/*
- * Copyright 2024 Peter Han
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
- * and associated documentation files (the "Software"), to deal in the Software without
- * restriction, including without limitation the rights to use, copy, modify, merge, publish,
- * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all copies or
- * substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
- * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
- * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-using KSerialization;
-using System;
-using UnityEngine;
-
-namespace PeterHan.AirlockDoor {
- ///
- /// A version of Door that never permits gas and liquids to pass unless set to open.
- ///
- [SerializationConfig(MemberSerialization.OptIn)]
- public sealed partial class AirlockDoor : StateMachineComponent,
- ISaveLoadable, ISim200ms {
- ///
- /// The status item showing the door's current state.
- ///
- private static StatusItem doorControlState;
-
- ///
- /// The status item showing the door's stored charge in kJ.
- ///
- private static StatusItem storedCharge;
-
- ///
- /// Prevents initialization from multiple threads at once.
- ///
- private static readonly object INIT_LOCK = new object();
-
- ///
- /// The port ID of the automation input port.
- ///
- internal static readonly HashedString OPEN_CLOSE_PORT_ID = "DoorOpenClose";
-
- ///
- /// The parameter for the sound indicating whether the door has power.
- ///
- private static readonly HashedString SOUND_POWERED_PARAMETER = "doorPowered";
-
- ///
- /// The parameter for the sound indicating the progress of the door close.
- ///
- private static readonly HashedString SOUND_PROGRESS_PARAMETER = "doorProgress";
-
- ///
- /// When the door is first instantiated, initializes the static fields, to avoid the
- /// crash that the stock Door has if it is loaded too early.
- ///
- private static void StaticInit() {
- doorControlState = new StatusItem("CurrentDoorControlState", "BUILDING",
- "", StatusItem.IconType.Info, NotificationType.Neutral, false, OverlayModes.
- None.ID) {
- resolveStringCallback = (str, data) => {
- bool locked = (data as AirlockDoor)?.locked ?? true;
- return str.Replace("{ControlState}", Strings.Get(
- "STRINGS.BUILDING.STATUSITEMS.CURRENTDOORCONTROLSTATE." + (locked ?
- "LOCKED" : "AUTO")));
- }
- };
- storedCharge = new StatusItem("AirlockStoredCharge", "BUILDING", "",
- StatusItem.IconType.Info, NotificationType.Neutral, false, OverlayModes.None.
- ID) {
- resolveStringCallback = (str, data) => {
- if (data is AirlockDoor door)
- str = string.Format(str, GameUtil.GetFormattedRoundedJoules(door.
- EnergyAvailable), GameUtil.GetFormattedRoundedJoules(door.
- EnergyCapacity), GameUtil.GetFormattedRoundedJoules(door.
- EnergyPerUse));
- return str;
- }
- };
- }
-
- ///
- /// The energy available to use for transiting Duplicants.
- ///
- public float EnergyAvailable {
- get {
- return energyAvailable;
- }
- }
-
- ///
- /// The maximum energy capacity.
- ///
- [SerializeField]
- public float EnergyCapacity;
-
- ///
- /// The maximum energy consumed per use.
- ///
- [SerializeField]
- public float EnergyPerUse;
-
- ///
- /// Counts Duplicants entering the airlock travelling left to right.
- ///
- internal SideReferenceCounter EnterLeft { get; private set; }
-
- ///
- /// Counts Duplicants entering the airlock travelling right to left.
- ///
- internal SideReferenceCounter EnterRight { get; private set; }
-
- ///
- /// Counts Duplicants leaving the airlock travelling right to left.
- ///
- internal SideReferenceCounter ExitLeft { get; private set; }
-
- ///
- /// Counts Duplicants leaving the airlock travelling left to right.
- ///
- internal SideReferenceCounter ExitRight { get; private set; }
-
- ///
- /// Returns true if the airlock door is currently open for entry or exit from the left.
- ///
- /// Whether the door is open on the left.
- public bool IsLeftOpen {
- get {
- return smi.IsInsideState(smi.sm.left.waitEnter) || smi.IsInsideState(smi.sm.
- left.waitEnterClose) || smi.IsInsideState(smi.sm.left.waitExit) ||
- smi.IsInsideState(smi.sm.left.waitExitClose);
- }
- }
-
- ///
- /// Returns true if the airlock door is currently open for entry or exit from the right.
- ///
- /// Whether the door is open on the right.
- public bool IsRightOpen {
- get {
- return smi.IsInsideState(smi.sm.right.waitEnter) || smi.IsInsideState(smi.sm.
- right.waitEnterClose) || smi.IsInsideState(smi.sm.right.waitExit) ||
- smi.IsInsideState(smi.sm.right.waitExitClose);
- }
- }
-
- ///
- /// The queued chore if a toggle errand is pending to open/close the door.
- ///
- private Chore changeStateChore;
-
- ///
- /// The sound played while the door is closing.
- ///
- private string doorClosingSound;
-
- ///
- /// The sound played while the door is opening.
- ///
- private string doorOpeningSound;
-
- [Serialize]
- private float energyAvailable;
-
- ///
- /// The current door state.
- ///
- [Serialize]
- [SerializeField]
- private bool locked;
-
- ///
- /// The energy meter.
- ///
- private MeterController meter;
-
- ///
- /// The door state requested by automation.
- ///
- private bool requestedState;
-
- // These fields are populated automatically by KMonoBehaviour
-#pragma warning disable IDE0044
-#pragma warning disable CS0649
- [MyCmpReq]
- public Building building;
-
- [MyCmpGet]
- private EnergyConsumer consumer;
-
- [MyCmpAdd]
- private LoopingSounds loopingSounds;
-
- [MyCmpReq]
- private Operational operational;
-
- [MyCmpGet]
- private KSelectable selectable;
-#pragma warning restore CS0649
-#pragma warning restore IDE0044
-
- internal AirlockDoor() {
- locked = requestedState = false;
- energyAvailable = 0.0f;
- EnergyCapacity = 1000.0f;
- EnergyPerUse = 0.0f;
- }
-
- ///
- /// Gets the base cell (center bottom) of this door.
- ///
- /// The door's foundation cell.
- public int GetBaseCell() {
- return building.GetCell();
- }
-
- public override int GetHashCode() {
- return building.GetCell();
- }
-
- ///
- /// Whether the door has enough energy for one use.
- ///
- /// true if there is energy for a Duplicant to pass, or false otherwise.
- public bool HasEnergy() {
- return EnergyAvailable >= EnergyPerUse;
- }
-
- ///
- /// Whether the door is currently passing Duplicants.
- ///
- /// true if the door is active, or false if it is idle / disabled / out of power.
- public bool IsDoorActive() {
- return smi.IsInsideState(smi.sm.left) || smi.IsInsideState(smi.sm.
- vacuum) || smi.IsInsideState(smi.sm.right) || smi.IsInsideState(smi.sm.
- vacuum_check);
- }
-
- ///
- /// Whether a Duplicant can traverse the door.
- ///
- /// true if the door is passable, or false otherwise.
- public bool IsUsable() {
- return operational.IsFunctional && HasEnergy();
- }
-
- ///
- /// Whether a Duplicant can traverse the door. Also true if the door is currently
- /// operating.
- ///
- /// true if the door is passable, or false otherwise.
- private bool IsUsableOrActive() {
- return IsUsable() || IsDoorActive();
- }
-
- protected override void OnPrefabInit() {
- base.OnPrefabInit();
- lock (INIT_LOCK) {
- if (doorControlState == null)
- StaticInit();
- }
- // Adding new sounds is actually very challenging
- doorClosingSound = GlobalAssets.GetSound("MechanizedAirlock_closing");
- doorOpeningSound = GlobalAssets.GetSound("MechanizedAirlock_opening");
- }
-
- protected override void OnSpawn() {
- base.OnSpawn();
- var structureTemperatures = GameComps.StructureTemperatures;
- var handle = structureTemperatures.GetHandle(gameObject);
- structureTemperatures.Bypass(handle);
- Subscribe((int)GameHashes.LogicEvent, OnLogicValueChanged);
- // Handle transitions
- EnterLeft = new SideReferenceCounter(this, smi.sm.waitEnterLeft);
- EnterRight = new SideReferenceCounter(this, smi.sm.waitEnterRight);
- ExitLeft = new SideReferenceCounter(this, smi.sm.waitExitLeft);
- ExitRight = new SideReferenceCounter(this, smi.sm.waitExitRight);
- requestedState = locked;
- smi.StartSM();
- RefreshControlState();
- SetFakeFloor(true);
- // Lock out the critters
- foreach (int cell in building.PlacementCells) {
- Grid.CritterImpassable[cell] = true;
- Grid.HasDoor[cell] = true;
- Pathfinding.Instance.AddDirtyNavGridCell(cell);
- }
- if (TryGetComponent(out KAnimControllerBase kac))
- // Stock mechanized airlocks are 5.0f powered
- kac.PlaySpeedMultiplier = 4.0f;
- // Layer is ignored if you use infront
- meter = new MeterController(kac, "meter_target", "meter", Meter.Offset.
- Infront, Grid.SceneLayer.NoLayer);
- // Door is always powered when used
- if (doorClosingSound != null)
- loopingSounds.UpdateFirstParameter(doorClosingSound, SOUND_POWERED_PARAMETER,
- 1.0f);
- if (doorOpeningSound != null)
- loopingSounds.UpdateFirstParameter(doorOpeningSound, SOUND_POWERED_PARAMETER,
- 1.0f);
- selectable.SetStatusItem(Db.Get().StatusItemCategories.OperatingEnergy,
- storedCharge, this);
- }
-
- protected override void OnCleanUp() {
- SetFakeFloor(false);
- foreach (int cell in building.PlacementCells) {
- // Clear the airlock flags, render critter and duplicant passable
- Grid.HasDoor[cell] = false;
- Game.Instance.SetDupePassableSolid(cell, false, Grid.Solid[cell]);
- Grid.CritterImpassable[cell] = false;
- Pathfinding.Instance.AddDirtyNavGridCell(cell);
- }
- Unsubscribe((int)GameHashes.LogicEvent, OnLogicValueChanged);
- base.OnCleanUp();
- }
-
- private void OnLogicValueChanged(object data) {
- var logicValueChanged = (LogicValueChanged)data;
- if (logicValueChanged.portID == OPEN_CLOSE_PORT_ID) {
- int newValue = logicValueChanged.newValue;
- if (changeStateChore != null) {
- changeStateChore.Cancel("Automation state change");
- changeStateChore = null;
- }
- // Bit 0 green: automatic, bit 0 red: lock the door
- requestedState = !LogicCircuitNetwork.IsBitActive(0, newValue);
- }
- }
-
- ///
- /// Updates the locked/open/auto state in the UI and the state machine.
- ///
- private void RefreshControlState() {
- smi.sm.isLocked.Set(locked, smi);
- Trigger((int)GameHashes.DoorControlStateChanged, locked ? Door.ControlState.
- Locked : Door.ControlState.Auto);
- UpdateWorldState();
- selectable.SetStatusItem(Db.Get().StatusItemCategories.Main, doorControlState,
- this);
- }
-
- ///
- /// Enables or disables the fake floor along the top of the door.
- ///
- /// true to add a fake floor, or false to remove it.
- private void SetFakeFloor(bool enable) {
- // Place fake floor along the top
- int width = building.Def.WidthInCells, start = Grid.PosToCell(this), height =
- building.Def.HeightInCells;
- for (int i = 0; i < width; i++) {
- int target = Grid.OffsetCell(start, i, height);
- if (Grid.IsValidCell(target)) {
- if (enable)
- Grid.FakeFloor.Add(target);
- else
- Grid.FakeFloor.Remove(target);
- Pathfinding.Instance.AddDirtyNavGridCell(target);
- }
- }
- }
-
- public void Sim200ms(float dt) {
- if (requestedState != locked && !IsDoorActive()) {
- // Automation locked or unlocked the door
- locked = requestedState;
- RefreshControlState();
- Trigger((int)GameHashes.DoorStateChanged, this);
- }
- if (operational.IsOperational) {
- float power = energyAvailable, capacity = EnergyCapacity;
- // Update active status
- if (consumer.IsPowered && power < capacity) {
- // Charging
- bool wasUsable = HasEnergy();
- operational.SetActive(true);
- energyAvailable = Math.Min(capacity, power + consumer.WattsUsed * dt);
- if (HasEnergy() != wasUsable) {
- UpdateWorldState();
- Trigger((int)GameHashes.OperationalFlagChanged, this);
- }
- UpdateMeter();
- } else
- // Not charging
- operational.SetActive(false);
- } else
- operational.SetActive(false);
- }
-
- ///
- /// Updates the energy meter.
- ///
- private void UpdateMeter() {
- meter?.SetPositionPercent(Math.Min(1.0f, energyAvailable / EnergyCapacity));
- }
-
- ///
- /// Updates the state of the door's cells in the game.
- ///
- private void UpdateWorldState() {
- bool usable = IsUsableOrActive(), openLeft = IsLeftOpen, openRight = IsRightOpen;
- int baseCell = building.GetCell(), centerUpCell = Grid.CellAbove(baseCell);
- if (Grid.IsValidBuildingCell(baseCell))
- Game.Instance.SetDupePassableSolid(baseCell, !locked && usable, false);
- if (Grid.IsValidBuildingCell(centerUpCell))
- Game.Instance.SetDupePassableSolid(centerUpCell, !locked && usable, false);
- // Left side cells controlled by left open
- UpdateWorldState(Grid.CellLeft(baseCell), usable, openLeft);
- UpdateWorldState(Grid.CellUpLeft(baseCell), usable, openLeft);
- // Right side cells controlled by right open
- UpdateWorldState(Grid.CellRight(baseCell), usable, openRight);
- UpdateWorldState(Grid.CellUpRight(baseCell), usable, openRight);
- var inst = Pathfinding.Instance;
- foreach (var cell in building.PlacementCells)
- inst.AddDirtyNavGridCell(cell);
- }
-
- ///
- /// Updates the world state of one cell in the airlock.
- ///
- /// The cell to update.
- /// Whether the door is currently usable.
- /// Whether that cell is currently open.
- private void UpdateWorldState(int cell, bool usable, bool open) {
- if (Grid.IsValidBuildingCell(cell))
- Game.Instance.SetDupePassableSolid(cell, !locked && usable, !open || !usable);
- }
-
- ///
- /// Counts the number of Duplicants waiting at transition points.
- ///
- internal sealed class SideReferenceCounter {
- ///
- /// How many Duplicants are currently waiting.
- ///
- public int WaitingCount { get; private set; }
-
- ///
- /// The master door.
- ///
- private readonly AirlockDoor door;
-
- ///
- /// The parameter to set if a request is pending.
- ///
- private readonly States.BoolParameter parameter;
-
- internal SideReferenceCounter(AirlockDoor door, States.BoolParameter parameter) {
- this.door = door ?? throw new ArgumentNullException("door");
- this.parameter = parameter ?? throw new ArgumentNullException("parameter");
- }
-
- ///
- /// Completes transition through the point.
- ///
- public void Finish() {
- if (door != null) {
- if (door.locked)
- parameter.Set(false, door.smi);
- else {
- int count = Math.Max(0, WaitingCount - 1);
- WaitingCount = count;
- if (count == 0)
- parameter.Set(false, door.smi);
- }
- }
- }
-
- ///
- /// Queues a request to traverse the transition point.
- ///
- public void Queue() {
- if (door != null && !door.locked && door.IsUsableOrActive()) {
- WaitingCount++;
- parameter.Set(true, door.smi);
- }
- }
- }
- }
-}
+/*
+ * Copyright 2024 Peter Han
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+ * and associated documentation files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+ * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+using KSerialization;
+using System;
+using UnityEngine;
+
+namespace PeterHan.AirlockDoor {
+ ///
+ /// A version of Door that never permits gas and liquids to pass unless set to open.
+ ///
+ [SerializationConfig(MemberSerialization.OptIn)]
+ public sealed partial class AirlockDoor : StateMachineComponent,
+ ISaveLoadable, ISim200ms {
+ ///
+ /// The status item showing the door's current state.
+ ///
+ private static StatusItem doorControlState;
+
+ ///
+ /// The status item showing the door's stored charge in kJ.
+ ///
+ private static StatusItem storedCharge;
+
+ ///
+ /// Prevents initialization from multiple threads at once.
+ ///
+ private static readonly object INIT_LOCK = new object();
+
+ ///
+ /// The port ID of the automation input port.
+ ///
+ internal static readonly HashedString OPEN_CLOSE_PORT_ID = "DoorOpenClose";
+
+ ///
+ /// The parameter for the sound indicating whether the door has power.
+ ///
+ private static readonly HashedString SOUND_POWERED_PARAMETER = "doorPowered";
+
+ ///
+ /// The parameter for the sound indicating the progress of the door close.
+ ///
+ private static readonly HashedString SOUND_PROGRESS_PARAMETER = "doorProgress";
+
+ ///
+ /// When the door is first instantiated, initializes the static fields, to avoid the
+ /// crash that the stock Door has if it is loaded too early.
+ ///
+ private static void StaticInit() {
+ doorControlState = new StatusItem("CurrentDoorControlState", "BUILDING",
+ "", StatusItem.IconType.Info, NotificationType.Neutral, false, OverlayModes.
+ None.ID) {
+ resolveStringCallback = (str, data) => {
+ bool locked = (data as AirlockDoor)?.locked ?? true;
+ return str.Replace("{ControlState}", Strings.Get(
+ "STRINGS.BUILDING.STATUSITEMS.CURRENTDOORCONTROLSTATE." + (locked ?
+ "LOCKED" : "AUTO")));
+ }
+ };
+ storedCharge = new StatusItem("AirlockStoredCharge", "BUILDING", "",
+ StatusItem.IconType.Info, NotificationType.Neutral, false, OverlayModes.None.
+ ID) {
+ resolveStringCallback = (str, data) => {
+ if (data is AirlockDoor door)
+ str = string.Format(str, GameUtil.GetFormattedRoundedJoules(door.
+ EnergyAvailable), GameUtil.GetFormattedRoundedJoules(door.
+ EnergyCapacity), GameUtil.GetFormattedRoundedJoules(door.
+ EnergyPerUse));
+ return str;
+ }
+ };
+ }
+
+ ///
+ /// The energy available to use for transiting Duplicants.
+ ///
+ public float EnergyAvailable {
+ get {
+ return energyAvailable;
+ }
+ }
+
+ ///
+ /// The maximum energy capacity.
+ ///
+ [SerializeField]
+ public float EnergyCapacity;
+
+ ///
+ /// The maximum energy consumed per use.
+ ///
+ [SerializeField]
+ public float EnergyPerUse;
+
+ ///
+ /// Counts Duplicants entering the airlock travelling left to right.
+ ///
+ internal SideReferenceCounter EnterLeft { get; private set; }
+
+ ///
+ /// Counts Duplicants entering the airlock travelling right to left.
+ ///
+ internal SideReferenceCounter EnterRight { get; private set; }
+
+ ///
+ /// Counts Duplicants leaving the airlock travelling right to left.
+ ///
+ internal SideReferenceCounter ExitLeft { get; private set; }
+
+ ///
+ /// Counts Duplicants leaving the airlock travelling left to right.
+ ///
+ internal SideReferenceCounter ExitRight { get; private set; }
+
+ ///
+ /// Returns true if the airlock door is currently open for entry or exit from the left.
+ ///
+ /// Whether the door is open on the left.
+ public bool IsLeftOpen {
+ get {
+ return smi.IsInsideState(smi.sm.left.waitEnter) || smi.IsInsideState(smi.sm.
+ left.waitEnterClose) || smi.IsInsideState(smi.sm.left.waitExit) ||
+ smi.IsInsideState(smi.sm.left.waitExitClose);
+ }
+ }
+
+ ///
+ /// Returns true if the airlock door is currently open for entry or exit from the right.
+ ///
+ /// Whether the door is open on the right.
+ public bool IsRightOpen {
+ get {
+ return smi.IsInsideState(smi.sm.right.waitEnter) || smi.IsInsideState(smi.sm.
+ right.waitEnterClose) || smi.IsInsideState(smi.sm.right.waitExit) ||
+ smi.IsInsideState(smi.sm.right.waitExitClose);
+ }
+ }
+
+ ///
+ /// The queued chore if a toggle errand is pending to open/close the door.
+ ///
+ private Chore changeStateChore;
+
+ ///
+ /// The sound played while the door is closing.
+ ///
+ private string doorClosingSound;
+
+ ///
+ /// The sound played while the door is opening.
+ ///
+ private string doorOpeningSound;
+
+ [Serialize]
+ private float energyAvailable;
+
+ ///
+ /// The current door state.
+ ///
+ [Serialize]
+ [SerializeField]
+ private bool locked;
+
+ ///
+ /// The energy meter.
+ ///
+ private MeterController meter;
+
+ ///
+ /// The door state requested by automation.
+ ///
+ private bool requestedState;
+
+ ///
+ /// Updates the world state if the door is plugged or unplugged.
+ ///
+ private bool wasConnected;
+
+ // These fields are populated automatically by KMonoBehaviour
+#pragma warning disable IDE0044
+#pragma warning disable CS0649
+ [MyCmpReq]
+ public Building building;
+
+ [MyCmpGet]
+ private EnergyConsumer consumer;
+
+ [MyCmpAdd]
+ private LoopingSounds loopingSounds;
+
+ [MyCmpReq]
+ private Operational operational;
+
+ [MyCmpGet]
+ private KSelectable selectable;
+#pragma warning restore CS0649
+#pragma warning restore IDE0044
+
+ internal AirlockDoor() {
+ locked = requestedState = wasConnected = false;
+ energyAvailable = 0.0f;
+ EnergyCapacity = 1000.0f;
+ EnergyPerUse = 0.0f;
+ }
+
+ ///
+ /// Gets the base cell (center bottom) of this door.
+ ///
+ /// The door's foundation cell.
+ public int GetBaseCell() {
+ return building.GetCell();
+ }
+
+ public override int GetHashCode() {
+ return building.GetCell();
+ }
+
+ ///
+ /// Whether the door has enough energy for one use.
+ ///
+ /// true if there is energy for a Duplicant to pass, or false otherwise.
+ public bool HasEnergy() {
+ return EnergyAvailable >= EnergyPerUse;
+ }
+
+ ///
+ /// Whether the door is currently passing Duplicants.
+ ///
+ /// true if the door is active, or false if it is idle / disabled / out of power.
+ public bool IsDoorActive() {
+ return smi.IsInsideState(smi.sm.left) || smi.IsInsideState(smi.sm.
+ vacuum) || smi.IsInsideState(smi.sm.right) || smi.IsInsideState(smi.sm.
+ vacuum_check);
+ }
+
+ ///
+ /// Whether the door is not even plugged in at all.
+ ///
+ /// true if the door is completely unplugged, or false otherwise.
+ public bool IsUnplugged() {
+ return !consumer.IsConnected;
+ }
+
+ ///
+ /// Whether a Duplicant can traverse the door.
+ ///
+ /// true if the door is passable, or false otherwise.
+ public bool IsUsable() {
+ return operational.IsFunctional && HasEnergy();
+ }
+
+ ///
+ /// Whether a Duplicant can traverse the door. Also true if the door is currently
+ /// operating.
+ ///
+ /// true if the door is passable, or false otherwise.
+ private bool IsUsableOrActive() {
+ return IsUsable() || IsDoorActive();
+ }
+
+ protected override void OnPrefabInit() {
+ base.OnPrefabInit();
+ lock (INIT_LOCK) {
+ if (doorControlState == null)
+ StaticInit();
+ }
+ // Adding new sounds is actually very challenging
+ doorClosingSound = GlobalAssets.GetSound("MechanizedAirlock_closing");
+ doorOpeningSound = GlobalAssets.GetSound("MechanizedAirlock_opening");
+ }
+
+ protected override void OnSpawn() {
+ base.OnSpawn();
+ var structureTemperatures = GameComps.StructureTemperatures;
+ var handle = structureTemperatures.GetHandle(gameObject);
+ structureTemperatures.Bypass(handle);
+ Subscribe((int)GameHashes.LogicEvent, OnLogicValueChanged);
+ // Handle transitions
+ EnterLeft = new SideReferenceCounter(this, smi.sm.waitEnterLeft);
+ EnterRight = new SideReferenceCounter(this, smi.sm.waitEnterRight);
+ ExitLeft = new SideReferenceCounter(this, smi.sm.waitExitLeft);
+ ExitRight = new SideReferenceCounter(this, smi.sm.waitExitRight);
+ requestedState = locked;
+ smi.StartSM();
+ RefreshControlState();
+ SetFakeFloor(true);
+ // Lock out the critters
+ foreach (int cell in building.PlacementCells) {
+ Grid.CritterImpassable[cell] = true;
+ Grid.HasDoor[cell] = true;
+ Pathfinding.Instance.AddDirtyNavGridCell(cell);
+ }
+ if (TryGetComponent(out KAnimControllerBase kac))
+ // Stock mechanized airlocks are 5.0f powered
+ kac.PlaySpeedMultiplier = 4.0f;
+ // Layer is ignored if you use infront
+ meter = new MeterController(kac, "meter_target", "meter", Meter.Offset.
+ Infront, Grid.SceneLayer.NoLayer);
+ // Door is always powered when used
+ if (doorClosingSound != null)
+ loopingSounds.UpdateFirstParameter(doorClosingSound, SOUND_POWERED_PARAMETER,
+ 1.0f);
+ if (doorOpeningSound != null)
+ loopingSounds.UpdateFirstParameter(doorOpeningSound, SOUND_POWERED_PARAMETER,
+ 1.0f);
+ selectable.SetStatusItem(Db.Get().StatusItemCategories.OperatingEnergy,
+ storedCharge, this);
+ }
+
+ protected override void OnCleanUp() {
+ SetFakeFloor(false);
+ foreach (int cell in building.PlacementCells) {
+ // Clear the airlock flags, render critter and duplicant passable
+ Grid.HasDoor[cell] = false;
+ Game.Instance.SetDupePassableSolid(cell, false, Grid.Solid[cell]);
+ Grid.CritterImpassable[cell] = false;
+ Pathfinding.Instance.AddDirtyNavGridCell(cell);
+ }
+ Unsubscribe((int)GameHashes.LogicEvent, OnLogicValueChanged);
+ base.OnCleanUp();
+ }
+
+ private void OnLogicValueChanged(object data) {
+ var logicValueChanged = (LogicValueChanged)data;
+ if (logicValueChanged.portID == OPEN_CLOSE_PORT_ID) {
+ int newValue = logicValueChanged.newValue;
+ if (changeStateChore != null) {
+ changeStateChore.Cancel("Automation state change");
+ changeStateChore = null;
+ }
+ // Bit 0 green: automatic, bit 0 red: lock the door
+ requestedState = !LogicCircuitNetwork.IsBitActive(0, newValue);
+ }
+ }
+
+ ///
+ /// Updates the locked/open/auto state in the UI and the state machine.
+ ///
+ private void RefreshControlState() {
+ smi.sm.isLocked.Set(locked, smi);
+ Trigger((int)GameHashes.DoorControlStateChanged, locked ? Door.ControlState.
+ Locked : Door.ControlState.Auto);
+ UpdateWorldState();
+ selectable.SetStatusItem(Db.Get().StatusItemCategories.Main, doorControlState,
+ this);
+ }
+
+ ///
+ /// Enables or disables the fake floor along the top of the door.
+ ///
+ /// true to add a fake floor, or false to remove it.
+ private void SetFakeFloor(bool enable) {
+ // Place fake floor along the top
+ int width = building.Def.WidthInCells, start = Grid.PosToCell(this), height =
+ building.Def.HeightInCells;
+ for (int i = 0; i < width; i++) {
+ int target = Grid.OffsetCell(start, i, height);
+ if (Grid.IsValidCell(target)) {
+ if (enable)
+ Grid.FakeFloor.Add(target);
+ else
+ Grid.FakeFloor.Remove(target);
+ Pathfinding.Instance.AddDirtyNavGridCell(target);
+ }
+ }
+ }
+
+ public void Sim200ms(float dt) {
+ bool connected = consumer != null && consumer.IsConnected;
+ if (requestedState != locked && !IsDoorActive()) {
+ // Automation locked or unlocked the door
+ locked = requestedState;
+ RefreshControlState();
+ Trigger((int)GameHashes.DoorStateChanged, this);
+ }
+ if (wasConnected != connected) {
+ wasConnected = connected;
+ UpdateWorldState();
+ }
+ if (operational.IsOperational) {
+ float power = energyAvailable, capacity = EnergyCapacity;
+ // Update active status
+ if (consumer.IsPowered && power < capacity) {
+ // Charging
+ bool wasUsable = HasEnergy();
+ operational.SetActive(true);
+ energyAvailable = Math.Min(capacity, power + consumer.WattsUsed * dt);
+ if (HasEnergy() != wasUsable) {
+ UpdateWorldState();
+ Trigger((int)GameHashes.OperationalFlagChanged, this);
+ }
+ UpdateMeter();
+ } else
+ // Not charging
+ operational.SetActive(false);
+ } else
+ operational.SetActive(false);
+ }
+
+ ///
+ /// Updates the energy meter.
+ ///
+ private void UpdateMeter() {
+ meter?.SetPositionPercent(Math.Min(1.0f, energyAvailable / EnergyCapacity));
+ }
+
+ ///
+ /// Updates the state of the door's cells in the game.
+ ///
+ private void UpdateWorldState() {
+ bool usable = IsUsableOrActive(), openLeft = IsLeftOpen, openRight = IsRightOpen,
+ unplugged = IsUnplugged();
+ int baseCell = building.GetCell(), centerUpCell = Grid.CellAbove(baseCell);
+ if (Grid.IsValidBuildingCell(baseCell))
+ Game.Instance.SetDupePassableSolid(baseCell, !locked && usable, false);
+ if (Grid.IsValidBuildingCell(centerUpCell))
+ Game.Instance.SetDupePassableSolid(centerUpCell, !locked && usable, false);
+ // Left side cells controlled by left open
+ UpdateWorldState(Grid.CellLeft(baseCell), usable || unplugged, openLeft);
+ UpdateWorldState(Grid.CellUpLeft(baseCell), usable || unplugged, openLeft);
+ // Right side cells controlled by right open
+ UpdateWorldState(Grid.CellRight(baseCell), usable || unplugged, openRight);
+ UpdateWorldState(Grid.CellUpRight(baseCell), usable || unplugged, openRight);
+ var inst = Pathfinding.Instance;
+ foreach (var cell in building.PlacementCells)
+ inst.AddDirtyNavGridCell(cell);
+ }
+
+ ///
+ /// Updates the world state of one cell in the airlock.
+ ///
+ /// The cell to update.
+ /// Whether the door is currently usable.
+ /// Whether that cell is currently open.
+ private void UpdateWorldState(int cell, bool usable, bool open) {
+ if (Grid.IsValidBuildingCell(cell))
+ Game.Instance.SetDupePassableSolid(cell, !locked && usable, !open || !usable);
+ }
+
+ ///
+ /// Counts the number of Duplicants waiting at transition points.
+ ///
+ internal sealed class SideReferenceCounter {
+ ///
+ /// How many Duplicants are currently waiting.
+ ///
+ public int WaitingCount { get; private set; }
+
+ ///
+ /// The master door.
+ ///
+ private readonly AirlockDoor door;
+
+ ///
+ /// The parameter to set if a request is pending.
+ ///
+ private readonly States.BoolParameter parameter;
+
+ internal SideReferenceCounter(AirlockDoor door, States.BoolParameter parameter) {
+ this.door = door ?? throw new ArgumentNullException("door");
+ this.parameter = parameter ?? throw new ArgumentNullException("parameter");
+ }
+
+ ///
+ /// Completes transition through the point.
+ ///
+ public void Finish() {
+ if (door != null) {
+ if (door.locked)
+ parameter.Set(false, door.smi);
+ else {
+ int count = Math.Max(0, WaitingCount - 1);
+ WaitingCount = count;
+ if (count == 0)
+ parameter.Set(false, door.smi);
+ }
+ }
+ }
+
+ ///
+ /// Queues a request to traverse the transition point.
+ ///
+ public void Queue() {
+ if (door != null && !door.locked && door.IsUsableOrActive()) {
+ WaitingCount++;
+ parameter.Set(true, door.smi);
+ }
+ }
+ }
+ }
+}
diff --git a/AirlockDoor/AirlockDoor.csproj b/AirlockDoor/AirlockDoor.csproj
index 33283fb1..e75a5ce7 100644
--- a/AirlockDoor/AirlockDoor.csproj
+++ b/AirlockDoor/AirlockDoor.csproj
@@ -2,11 +2,11 @@
Airlock Door
- 3.12.0.0
+ 3.13.0.0
PeterHan.AirlockDoor
A powered airlock door that allows passage without ever exchanging liquid or gas.
3.1.0.0
- 600112
+ 642443
Vanilla;Mergedown
diff --git a/AirlockDoor/AirlockDoorPatches.cs b/AirlockDoor/AirlockDoorPatches.cs
index f4faf4de..ea200bfb 100644
--- a/AirlockDoor/AirlockDoorPatches.cs
+++ b/AirlockDoor/AirlockDoorPatches.cs
@@ -66,10 +66,10 @@ public override void OnLoad(Harmony harmony) {
}
///
- /// Applied to MinionConfig to add the navigator transition for airlocks.
+ /// Applied to BaseMinionConfig to add the navigator transition for airlocks.
///
- [PLibPatch(RunAt.AfterDbInit, nameof(MinionConfig.OnSpawn),
- RequireType = nameof(MinionConfig), PatchType = HarmonyPatchType.Postfix)]
+ [PLibPatch(RunAt.AfterDbInit, nameof(BaseMinionConfig.BaseOnSpawn),
+ RequireType = nameof(BaseMinionConfig), PatchType = HarmonyPatchType.Postfix)]
internal static void MinionSpawn_Postfix(GameObject go) {
if (go.TryGetComponent(out Navigator nav))
nav.transitionDriver.overrideLayers.Add(new AirlockDoorTransitionLayer(nav));
diff --git a/AirlockDoor/AirlockDoorTransitionLayer.cs b/AirlockDoor/AirlockDoorTransitionLayer.cs
index 39b962f2..1f20ea79 100644
--- a/AirlockDoor/AirlockDoorTransitionLayer.cs
+++ b/AirlockDoor/AirlockDoorTransitionLayer.cs
@@ -1,188 +1,188 @@
-/*
- * Copyright 2024 Peter Han
- * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
- * and associated documentation files (the "Software"), to deal in the Software without
- * restriction, including without limitation the rights to use, copy, modify, merge, publish,
- * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all copies or
- * substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
- * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
- * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-using PeterHan.PLib.Core;
-using System.Collections.Generic;
-
-namespace PeterHan.AirlockDoor {
- ///
- /// Handles Duplicant navigation through an airlock door.
- ///
- public sealed class AirlockDoorTransitionLayer : TransitionDriver.InterruptOverrideLayer {
- ///
- /// The layer to check for airlock doors.
- ///
- private readonly int buildingLayer;
-
- ///
- /// The doors to be opened.
- ///
- private readonly IDictionary doors;
-
- public AirlockDoorTransitionLayer(Navigator navigator) : base(navigator) {
- buildingLayer = (int)PGameUtils.GetObjectLayer(nameof(ObjectLayer.Building),
- ObjectLayer.Building);
- doors = new Dictionary(8);
- }
-
- ///
- /// Adds a door if it is present in this cell.
- ///
- /// The cell to check for the door.
- /// The cell of the Duplicant navigating the door.
- private void AddDoor(int doorCell, int navCell) {
- if (Grid.HasDoor[doorCell]) {
- var go = Grid.Objects[doorCell, buildingLayer];
- if (go != null && go.TryGetComponent(out AirlockDoor door) && door.isSpawned &&
- !doors.ContainsKey(door))
- RequestOpenDoor(door, doorCell, navCell);
- }
- }
-
- ///
- /// For each door, checks to see if all are in a state where the Duplicant may pass.
- ///
- /// true if doors are open, or false otherwise.
- private bool AreAllDoorsOpen() {
- bool open = true;
- AirlockDoor door;
- foreach (var pair in doors)
- if ((door = pair.Key) != null) {
- switch (pair.Value) {
- case DoorRequestType.EnterLeft:
- case DoorRequestType.ExitLeft:
- if (!door.IsLeftOpen)
- open = false;
- break;
- case DoorRequestType.EnterRight:
- case DoorRequestType.ExitRight:
- if (!door.IsRightOpen)
- open = false;
- break;
- }
- }
- return open;
- }
-
- public override void BeginTransition(Navigator navigator, Navigator.
- ActiveTransition transition) {
- if (doors.Count == 0 && navigator != null && transition != null) {
- int cell = Grid.PosToCell(navigator);
- int targetCell = Grid.OffsetCell(cell, transition.x, transition.y);
- AddDoor(targetCell, cell);
- // If duplicant is inside a tube they are only 1 cell tall
- if (navigator.CurrentNavType != NavType.Tube)
- AddDoor(Grid.CellAbove(targetCell), cell);
- // Include any other offsets
- var offsets = transition.navGridTransition.voidOffsets;
- if (offsets != null) {
- int n = offsets.Length;
- for (int i = 0; i < n; i++)
- AddDoor(Grid.OffsetCell(cell, offsets[i]), cell);
- }
- // If not open, start a transition with the dupe waiting for the door
- if (doors.Count > 0 && !AreAllDoorsOpen()) {
- base.BeginTransition(navigator, transition);
- transition.anim = navigator.NavGrid.GetIdleAnim(navigator.CurrentNavType);
- transition.start = originalTransition.start;
- transition.end = originalTransition.start;
- }
- }
- }
-
- ///
- /// Clears all pending transitions.
- ///
- private void ClearTransitions() {
- AirlockDoor door;
- foreach (var pair in doors)
- if ((door = pair.Key) != null) {
- switch (pair.Value) {
- case DoorRequestType.EnterLeft:
- door.EnterLeft?.Finish();
- break;
- case DoorRequestType.EnterRight:
- door.EnterRight?.Finish();
- break;
- case DoorRequestType.ExitLeft:
- door.ExitLeft?.Finish();
- break;
- case DoorRequestType.ExitRight:
- door.ExitRight?.Finish();
- break;
- }
- }
- doors.Clear();
- }
-
- public override void Destroy() {
- base.Destroy();
- ClearTransitions();
- }
-
- public override void EndTransition(Navigator navigator, Navigator.
- ActiveTransition transition) {
- base.EndTransition(navigator, transition);
- ClearTransitions();
- }
-
- protected override bool IsOverrideComplete() {
- return base.IsOverrideComplete() && AreAllDoorsOpen();
- }
-
- ///
- /// Requests a door to open if necessary.
- ///
- /// The door that is being traversed.
- /// The cell where the navigator is moving.
- /// The cell where the navigator is standing now.
- private void RequestOpenDoor(AirlockDoor door, int doorCell, int navCell) {
- int baseCell = door.GetBaseCell();
- // Based on coordinates, determine what is required of the door
- CellOffset targetOffset = Grid.GetOffset(baseCell, doorCell), navOffset =
- Grid.GetOffset(doorCell, navCell);
- int dx = targetOffset.x;
- if (dx > 0) {
- // Right side door
- if (navOffset.x > 0) {
- doors.Add(door, DoorRequestType.EnterRight);
- door.EnterRight?.Queue();
- } else {
- doors.Add(door, DoorRequestType.ExitRight);
- door.ExitRight?.Queue();
- }
- } else if (dx < 0) {
- // Left side door
- if (navOffset.x > 0) {
- doors.Add(door, DoorRequestType.ExitLeft);
- door.ExitLeft?.Queue();
- } else {
- doors.Add(door, DoorRequestType.EnterLeft);
- door.EnterLeft?.Queue();
- }
- } // Else, entering center cell which is "always" passable
- }
-
- ///
- /// The types of requests that can be made of an airlock door.
- ///
- private enum DoorRequestType {
- EnterLeft, EnterRight, ExitLeft, ExitRight
- }
- }
-}
+/*
+ * Copyright 2024 Peter Han
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software
+ * and associated documentation files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
+ * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+using PeterHan.PLib.Core;
+using System.Collections.Generic;
+
+namespace PeterHan.AirlockDoor {
+ ///
+ /// Handles Duplicant navigation through an airlock door.
+ ///
+ public sealed class AirlockDoorTransitionLayer : TransitionDriver.InterruptOverrideLayer {
+ ///
+ /// The layer to check for airlock doors.
+ ///
+ private readonly int buildingLayer;
+
+ ///
+ /// The doors to be opened.
+ ///
+ private readonly IDictionary doors;
+
+ public AirlockDoorTransitionLayer(Navigator navigator) : base(navigator) {
+ buildingLayer = (int)PGameUtils.GetObjectLayer(nameof(ObjectLayer.Building),
+ ObjectLayer.Building);
+ doors = new Dictionary(8);
+ }
+
+ ///
+ /// Adds a door if it is present in this cell.
+ ///
+ /// The cell to check for the door.
+ /// The cell of the Duplicant navigating the door.
+ private void AddDoor(int doorCell, int navCell) {
+ if (Grid.HasDoor[doorCell]) {
+ var go = Grid.Objects[doorCell, buildingLayer];
+ if (go != null && go.TryGetComponent(out AirlockDoor door) && door.isSpawned &&
+ !doors.ContainsKey(door) && !door.IsUnplugged())
+ RequestOpenDoor(door, doorCell, navCell);
+ }
+ }
+
+ ///
+ /// For each door, checks to see if all are in a state where the Duplicant may pass.
+ ///
+ /// true if doors are open, or false otherwise.
+ private bool AreAllDoorsOpen() {
+ bool open = true;
+ AirlockDoor door;
+ foreach (var pair in doors)
+ if ((door = pair.Key) != null) {
+ switch (pair.Value) {
+ case DoorRequestType.EnterLeft:
+ case DoorRequestType.ExitLeft:
+ if (!door.IsLeftOpen)
+ open = false;
+ break;
+ case DoorRequestType.EnterRight:
+ case DoorRequestType.ExitRight:
+ if (!door.IsRightOpen)
+ open = false;
+ break;
+ }
+ }
+ return open;
+ }
+
+ public override void BeginTransition(Navigator navigator, Navigator.
+ ActiveTransition transition) {
+ if (doors.Count == 0 && navigator != null && transition != null) {
+ int cell = Grid.PosToCell(navigator);
+ int targetCell = Grid.OffsetCell(cell, transition.x, transition.y);
+ AddDoor(targetCell, cell);
+ // If duplicant is inside a tube they are only 1 cell tall
+ if (navigator.CurrentNavType != NavType.Tube)
+ AddDoor(Grid.CellAbove(targetCell), cell);
+ // Include any other offsets
+ var offsets = transition.navGridTransition.voidOffsets;
+ if (offsets != null) {
+ int n = offsets.Length;
+ for (int i = 0; i < n; i++)
+ AddDoor(Grid.OffsetCell(cell, offsets[i]), cell);
+ }
+ // If not open, start a transition with the dupe waiting for the door
+ if (doors.Count > 0 && !AreAllDoorsOpen()) {
+ base.BeginTransition(navigator, transition);
+ transition.anim = navigator.NavGrid.GetIdleAnim(navigator.CurrentNavType);
+ transition.start = originalTransition.start;
+ transition.end = originalTransition.start;
+ }
+ }
+ }
+
+ ///
+ /// Clears all pending transitions.
+ ///
+ private void ClearTransitions() {
+ AirlockDoor door;
+ foreach (var pair in doors)
+ if ((door = pair.Key) != null) {
+ switch (pair.Value) {
+ case DoorRequestType.EnterLeft:
+ door.EnterLeft?.Finish();
+ break;
+ case DoorRequestType.EnterRight:
+ door.EnterRight?.Finish();
+ break;
+ case DoorRequestType.ExitLeft:
+ door.ExitLeft?.Finish();
+ break;
+ case DoorRequestType.ExitRight:
+ door.ExitRight?.Finish();
+ break;
+ }
+ }
+ doors.Clear();
+ }
+
+ public override void Destroy() {
+ base.Destroy();
+ ClearTransitions();
+ }
+
+ public override void EndTransition(Navigator navigator, Navigator.
+ ActiveTransition transition) {
+ base.EndTransition(navigator, transition);
+ ClearTransitions();
+ }
+
+ protected override bool IsOverrideComplete() {
+ return base.IsOverrideComplete() && AreAllDoorsOpen();
+ }
+
+ ///
+ /// Requests a door to open if necessary.
+ ///
+ /// The door that is being traversed.
+ /// The cell where the navigator is moving.
+ /// The cell where the navigator is standing now.
+ private void RequestOpenDoor(AirlockDoor door, int doorCell, int navCell) {
+ int baseCell = door.GetBaseCell();
+ // Based on coordinates, determine what is required of the door
+ CellOffset targetOffset = Grid.GetOffset(baseCell, doorCell), navOffset =
+ Grid.GetOffset(doorCell, navCell);
+ int dx = targetOffset.x;
+ if (dx > 0) {
+ // Right side door
+ if (navOffset.x > 0) {
+ doors.Add(door, DoorRequestType.EnterRight);
+ door.EnterRight?.Queue();
+ } else {
+ doors.Add(door, DoorRequestType.ExitRight);
+ door.ExitRight?.Queue();
+ }
+ } else if (dx < 0) {
+ // Left side door
+ if (navOffset.x > 0) {
+ doors.Add(door, DoorRequestType.ExitLeft);
+ door.ExitLeft?.Queue();
+ } else {
+ doors.Add(door, DoorRequestType.EnterLeft);
+ door.EnterLeft?.Queue();
+ }
+ } // Else, entering center cell which is "always" passable if powered
+ }
+
+ ///
+ /// The types of requests that can be made of an airlock door.
+ ///
+ private enum DoorRequestType {
+ EnterLeft, EnterRight, ExitLeft, ExitRight
+ }
+ }
+}
diff --git a/StockBugFix/StockBugFix.csproj b/StockBugFix/StockBugFix.csproj
index bb8b9bdc..41f1a7be 100644
--- a/StockBugFix/StockBugFix.csproj
+++ b/StockBugFix/StockBugFix.csproj
@@ -2,12 +2,12 @@
Stock Bug Fix
- 4.6.0.0
+ 4.7.0.0
PeterHan.StockBugFix
Fixes bugs and annoyances in the stock game.
- 3.2.0.0
+ 3.3.0.0
true
- 617614
+ 642443
Vanilla;Mergedown
diff --git a/StockBugFix/StockQOLPatches.cs b/StockBugFix/StockQOLPatches.cs
index b39b5773..cfb2096a 100644
--- a/StockBugFix/StockQOLPatches.cs
+++ b/StockBugFix/StockQOLPatches.cs
@@ -165,11 +165,14 @@ private static float GetRequiredFoodPerCycle(IEnumerable dupes)
var totalCalories = 0.0f;
if (dupes != null)
foreach (var dupe in dupes) {
- var caloriesPerSecond = Db.Get().Amounts.Calories.Lookup(dupe).
- GetDelta();
- // "tummyless" attribute adds float.PositiveInfinity
- if (!float.IsInfinity(caloriesPerSecond))
- totalCalories += caloriesPerSecond * Constants.SECONDS_PER_CYCLE;
+ var kcalAttribute = Db.Get().Amounts.Calories.Lookup(dupe);
+ // No kcal on bionic minions
+ if (kcalAttribute != null) {
+ var caloriesPerSecond = kcalAttribute.GetDelta();
+ // "tummyless" attribute adds float.PositiveInfinity
+ if (!float.IsInfinity(caloriesPerSecond))
+ totalCalories += caloriesPerSecond * Constants.SECONDS_PER_CYCLE;
+ }
}
return Mathf.Abs(totalCalories);
}