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); }