From a706f0bc70c20a96e4828d550bf5ae65ad52771c Mon Sep 17 00:00:00 2001 From: Peter Han Date: Fri, 22 Nov 2024 11:42:56 -0800 Subject: [PATCH] Add a workaround in Queue for Sinks for the Worker changes in U53-642695 --- QueueForSink/QueueForSink.csproj | 24 +- QueueForSink/WorkCheckpoint.cs | 513 ++++++++++++++++--------------- 2 files changed, 271 insertions(+), 266 deletions(-) diff --git a/QueueForSink/QueueForSink.csproj b/QueueForSink/QueueForSink.csproj index 2952488f..859414f1 100644 --- a/QueueForSink/QueueForSink.csproj +++ b/QueueForSink/QueueForSink.csproj @@ -1,12 +1,12 @@ - - - - Queue For Sinks - 3.13.0.0 - PeterHan.QueueForSinks - Forces Duplicants to queue up for sinks, wash basins, and ore scrubbers. - 3.2.0.0 - 537329 - Vanilla;Mergedown - - + + + + Queue For Sinks + 3.14.0.0 + PeterHan.QueueForSinks + Forces Duplicants to queue up for sinks, wash basins, and ore scrubbers. + 3.2.0.0 + 537329 + Vanilla;Mergedown + + diff --git a/QueueForSink/WorkCheckpoint.cs b/QueueForSink/WorkCheckpoint.cs index 28110542..9bb7fc4b 100644 --- a/QueueForSink/WorkCheckpoint.cs +++ b/QueueForSink/WorkCheckpoint.cs @@ -1,254 +1,259 @@ -/* - * 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 System.Threading; -using PeterHan.PLib.Core; -using UnityEngine; - -namespace PeterHan.QueueForSinks { - /// - /// A checkpoint component which prevents Duplicants from passing if a workable is in use - /// and they could use it. - /// - public abstract class WorkCheckpoint : KMonoBehaviour where T : Workable { - // These fields are filled in automatically by KMonoBehaviour -#pragma warning disable CS0649 - [MyCmpReq] - protected DirectionControl direction; -#pragma warning restore CS0649 - - /// - /// Whether the workable this checkpoint guards is currently in use. - /// - protected bool inUse; - - /// - /// The current reaction. - /// - private WorkCheckpointReactable reactable; - - /// - /// Used to ensure that only one Duplicant is released when a checkpoint is vacated. - /// - private volatile int token; - - /// - /// The workable which controls tasks. - /// - private T workable; - - protected WorkCheckpoint() { - token = 0; - } - - /// - /// Destroys the current reaction. - /// - private void ClearReactable() { - if (reactable != null) { - reactable.Cleanup(); - reactable = null; - } - } - - /// - /// Creates a new reaction. - /// - private void CreateNewReactable() { - reactable = new WorkCheckpointReactable(this); - } - - /// - /// Handles work events to update the status of this workable. - /// - private void HandleWorkableAction(Workable _, Workable.WorkableEvent evt) { - switch (evt) { - case Workable.WorkableEvent.WorkStarted: - token = 1; - inUse = true; - break; - case Workable.WorkableEvent.WorkCompleted: - case Workable.WorkableEvent.WorkStopped: - inUse = false; - break; - } - } - - /// - /// Called to see if a Duplicant shall not pass! - /// - /// The Duplicant to check. - /// The X direction the Duplicant is moving. - /// true if the Duplicant must stop, or false if they can pass - protected abstract bool MustStop(GameObject reactor, float direction); - - protected override void OnCleanUp() { - base.OnCleanUp(); - ClearReactable(); - if (workable != null) - workable.OnWorkableEventCB -= HandleWorkableAction; - token = 0; - } - - protected override void OnSpawn() { - base.OnSpawn(); - // Using MyCmpReq on generics was crashing - if (gameObject.TryGetComponent(out workable)) - workable.OnWorkableEventCB += HandleWorkableAction; - CreateNewReactable(); - } - - /// - /// Releases the token if it was taken, but no work was started within a few frames. - /// Handles cases where the Duplicant who took the token gets distracted or dies - /// before work can ever begin. - /// - private System.Collections.IEnumerator ReleaseToken() { - yield return null; - yield return null; - if (workable != null && workable.worker == null) - token = 1; - } - - /// - /// Tries to take the checkpoint token. - /// - /// true if the token was taken and the Duplicant may leave, or false if the - /// token is unavailable and the Duplicant must keep waiting. - internal bool TryTakeToken() { - bool taken = Interlocked.CompareExchange(ref token, 0, 1) == 1; - if (taken) - StartCoroutine(ReleaseToken()); - return taken; - } - - /// - /// A reaction which stops Duplicants in their tracks if they need to use a workable - /// that is already in use. - /// - private sealed class WorkCheckpointReactable : Reactable { - /// - /// This value changes every few updates, calculate it at runtime - /// - private static readonly ObjectLayer NUM_LAYERS = PGameUtils.GetObjectLayer( - nameof(ObjectLayer.NumLayers), ObjectLayer.NumLayers); - - /// - /// Set once the Duplicant has begun waiting. - /// - private bool begun; - - /// - /// The parent work checkpoint. - /// - private readonly WorkCheckpoint checkpoint; - - /// - /// The animation to play while stopped. - /// - private readonly KAnimFile distractedAnim; - - /// - /// The navigator of the Duplicant who is waiting. - /// - private Navigator nav; - - internal WorkCheckpointReactable(WorkCheckpoint checkpoint) : base(checkpoint. - gameObject, "WorkCheckpointReactable", Db.Get().ChoreTypes.Checkpoint, - 1, 1, false, 0.0f, 0.0f, float.PositiveInfinity, 0.0f, NUM_LAYERS) { - begun = false; - this.checkpoint = checkpoint; - distractedAnim = Assets.GetAnim("anim_idle_distracted_kanim"); - preventChoreInterruption = false; - } - - /// - /// Returns true if the Duplicant is waiting in the queue. - /// - /// true if the Duplicant is waiting in the queue, or false otherwise. - private bool InQueue() { - return checkpoint.workable.GetWorker() != null || (begun && !checkpoint. - TryTakeToken()); - } - - protected override void InternalBegin() { - reactor.TryGetComponent(out nav); - // Animation to make them stand impatiently in line - if (reactor.TryGetComponent(out KBatchedAnimController controller)) { - controller.AddAnimOverrides(distractedAnim, 1f); - controller.Play("idle_pre"); - controller.Queue("idle_default", KAnim.PlayMode.Loop); - } - checkpoint.CreateNewReactable(); - begun = true; - } - - public override bool InternalCanBegin(GameObject newReactor, Navigator. - ActiveTransition transition) { - bool disposed = checkpoint == null || checkpoint.workable == null; - if (disposed) - Cleanup(); - bool canBegin = !disposed && reactor == null; - if (canBegin) - canBegin = MustStop(newReactor, transition.x); - return canBegin; - } - - protected override void InternalCleanup() { - // Global cooldown needs to be reset with reactions to lines of sinks - if (reactor != null && reactor.TryGetComponent(out StateMachineController smc)) - smc.GetSMI()?.ClearLastReaction(); - nav = null; - begun = false; - } - - protected override void InternalEnd() { - if (reactor != null && reactor.TryGetComponent(out KBatchedAnimController - controller)) - controller.RemoveAnimOverrides(distractedAnim); - } - - /// - /// Returns whether a duplicant must stop and wait. - /// - /// The duplicant to check. - /// The X direction they are going. - /// true if they must wait, or false if they may pass. - private bool MustStop(GameObject dupe, float x) { - var dir = checkpoint.direction.allowedDirection; - SuffocationMonitor.Instance suff; - // Left is decreasing X, must be facing the correct direction - return (dir == WorkableReactable.AllowedDirection.Any || (dir == - WorkableReactable.AllowedDirection.Left) == x < 0.0f) && - dupe != null && checkpoint.MustStop(dupe, x) && ((suff = dupe. - GetSMI()) == null || !suff.IsSuffocating()) && - InQueue(); - } - - public override void Update(float dt) { - if (checkpoint == null || checkpoint.workable == null || nav == null) - Cleanup(); - else { - nav.AdvancePath(false); - if (!nav.path.IsValid() || !MustStop(reactor, nav.GetNextTransition().x)) - Cleanup(); - } - } - } - } -} +/* + * 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 System.Threading; +using PeterHan.PLib.Core; +using PeterHan.PLib.Detours; +using UnityEngine; + +namespace PeterHan.QueueForSinks { + /// + /// A checkpoint component which prevents Duplicants from passing if a workable is in use + /// and they could use it. + /// + public abstract class WorkCheckpoint : KMonoBehaviour where T : Workable { + // TODO Remove once versions before U52-621068 no longer need to be supported + private static readonly IDetouredField WORKER = + PDetours.DetourField(nameof(Workable.worker)); + + // These fields are filled in automatically by KMonoBehaviour +#pragma warning disable CS0649 + [MyCmpReq] + protected DirectionControl direction; +#pragma warning restore CS0649 + + /// + /// Whether the workable this checkpoint guards is currently in use. + /// + protected bool inUse; + + /// + /// The current reaction. + /// + private WorkCheckpointReactable reactable; + + /// + /// Used to ensure that only one Duplicant is released when a checkpoint is vacated. + /// + private volatile int token; + + /// + /// The workable which controls tasks. + /// + private T workable; + + protected WorkCheckpoint() { + token = 0; + } + + /// + /// Destroys the current reaction. + /// + private void ClearReactable() { + if (reactable != null) { + reactable.Cleanup(); + reactable = null; + } + } + + /// + /// Creates a new reaction. + /// + private void CreateNewReactable() { + reactable = new WorkCheckpointReactable(this); + } + + /// + /// Handles work events to update the status of this workable. + /// + private void HandleWorkableAction(Workable _, Workable.WorkableEvent evt) { + switch (evt) { + case Workable.WorkableEvent.WorkStarted: + token = 1; + inUse = true; + break; + case Workable.WorkableEvent.WorkCompleted: + case Workable.WorkableEvent.WorkStopped: + inUse = false; + break; + } + } + + /// + /// Called to see if a Duplicant shall not pass! + /// + /// The Duplicant to check. + /// The X direction the Duplicant is moving. + /// true if the Duplicant must stop, or false if they can pass + protected abstract bool MustStop(GameObject reactor, float direction); + + protected override void OnCleanUp() { + base.OnCleanUp(); + ClearReactable(); + if (workable != null) + workable.OnWorkableEventCB -= HandleWorkableAction; + token = 0; + } + + protected override void OnSpawn() { + base.OnSpawn(); + // Using MyCmpReq on generics was crashing + if (gameObject.TryGetComponent(out workable)) + workable.OnWorkableEventCB += HandleWorkableAction; + CreateNewReactable(); + } + + /// + /// Releases the token if it was taken, but no work was started within a few frames. + /// Handles cases where the Duplicant who took the token gets distracted or dies + /// before work can ever begin. + /// + private System.Collections.IEnumerator ReleaseToken() { + yield return null; + yield return null; + if (workable != null && WORKER.Get(workable) == null) + token = 1; + } + + /// + /// Tries to take the checkpoint token. + /// + /// true if the token was taken and the Duplicant may leave, or false if the + /// token is unavailable and the Duplicant must keep waiting. + internal bool TryTakeToken() { + bool taken = Interlocked.CompareExchange(ref token, 0, 1) == 1; + if (taken) + StartCoroutine(ReleaseToken()); + return taken; + } + + /// + /// A reaction which stops Duplicants in their tracks if they need to use a workable + /// that is already in use. + /// + private sealed class WorkCheckpointReactable : Reactable { + /// + /// This value changes every few updates, calculate it at runtime + /// + private static readonly ObjectLayer NUM_LAYERS = PGameUtils.GetObjectLayer( + nameof(ObjectLayer.NumLayers), ObjectLayer.NumLayers); + + /// + /// Set once the Duplicant has begun waiting. + /// + private bool begun; + + /// + /// The parent work checkpoint. + /// + private readonly WorkCheckpoint checkpoint; + + /// + /// The animation to play while stopped. + /// + private readonly KAnimFile distractedAnim; + + /// + /// The navigator of the Duplicant who is waiting. + /// + private Navigator nav; + + internal WorkCheckpointReactable(WorkCheckpoint checkpoint) : base(checkpoint. + gameObject, "WorkCheckpointReactable", Db.Get().ChoreTypes.Checkpoint, + 1, 1, false, 0.0f, 0.0f, float.PositiveInfinity, 0.0f, NUM_LAYERS) { + begun = false; + this.checkpoint = checkpoint; + distractedAnim = Assets.GetAnim("anim_idle_distracted_kanim"); + preventChoreInterruption = false; + } + + /// + /// Returns true if the Duplicant is waiting in the queue. + /// + /// true if the Duplicant is waiting in the queue, or false otherwise. + private bool InQueue() { + return checkpoint.workable.GetWorker() != null || (begun && !checkpoint. + TryTakeToken()); + } + + protected override void InternalBegin() { + reactor.TryGetComponent(out nav); + // Animation to make them stand impatiently in line + if (reactor.TryGetComponent(out KBatchedAnimController controller)) { + controller.AddAnimOverrides(distractedAnim, 1f); + controller.Play("idle_pre"); + controller.Queue("idle_default", KAnim.PlayMode.Loop); + } + checkpoint.CreateNewReactable(); + begun = true; + } + + public override bool InternalCanBegin(GameObject newReactor, Navigator. + ActiveTransition transition) { + bool disposed = checkpoint == null || checkpoint.workable == null; + if (disposed) + Cleanup(); + bool canBegin = !disposed && reactor == null; + if (canBegin) + canBegin = MustStop(newReactor, transition.x); + return canBegin; + } + + protected override void InternalCleanup() { + // Global cooldown needs to be reset with reactions to lines of sinks + if (reactor != null && reactor.TryGetComponent(out StateMachineController smc)) + smc.GetSMI()?.ClearLastReaction(); + nav = null; + begun = false; + } + + protected override void InternalEnd() { + if (reactor != null && reactor.TryGetComponent(out KBatchedAnimController + controller)) + controller.RemoveAnimOverrides(distractedAnim); + } + + /// + /// Returns whether a duplicant must stop and wait. + /// + /// The duplicant to check. + /// The X direction they are going. + /// true if they must wait, or false if they may pass. + private bool MustStop(GameObject dupe, float x) { + var dir = checkpoint.direction.allowedDirection; + SuffocationMonitor.Instance suff; + // Left is decreasing X, must be facing the correct direction + return (dir == WorkableReactable.AllowedDirection.Any || (dir == + WorkableReactable.AllowedDirection.Left) == x < 0.0f) && + dupe != null && checkpoint.MustStop(dupe, x) && ((suff = dupe. + GetSMI()) == null || !suff.IsSuffocating()) && + InQueue(); + } + + public override void Update(float dt) { + if (checkpoint == null || checkpoint.workable == null || nav == null) + Cleanup(); + else { + nav.AdvancePath(false); + if (!nav.path.IsValid() || !MustStop(reactor, nav.GetNextTransition().x)) + Cleanup(); + } + } + } + } +}