diff --git a/DebugNotIncluded/DebugNotIncluded.csproj b/DebugNotIncluded/DebugNotIncluded.csproj index 3cfb3b56..c13ad642 100644 --- a/DebugNotIncluded/DebugNotIncluded.csproj +++ b/DebugNotIncluded/DebugNotIncluded.csproj @@ -2,11 +2,11 @@ Debug Not Included - 2.5.0.0 + 2.6.0.0 PeterHan.DebugNotIncluded Aids with mod debugging and management. Not intended to be a production mod manager. 2.0.0.0 - 526233 + 534889 Vanilla;Mergedown diff --git a/DebugNotIncluded/DebugNotIncludedPatches.cs b/DebugNotIncluded/DebugNotIncludedPatches.cs index e4f64bc8..aaedc866 100644 --- a/DebugNotIncluded/DebugNotIncludedPatches.cs +++ b/DebugNotIncluded/DebugNotIncludedPatches.cs @@ -66,7 +66,7 @@ public sealed class DebugNotIncludedPatches : UserMod2 { [PLibMethod(RunAt.AfterLayerableLoad)] internal static void AfterModsLoad() { // Input manager is not set up until this time - KInputHandler.Add(Global.Instance.GetInputManager().GetDefaultController(), + KInputHandler.Add(Global.GetInputManager().GetDefaultController(), new UISnapshotHandler(), 1024); } diff --git a/DecorReimagined/DecorReimagined.csproj b/DecorReimagined/DecorReimagined.csproj index 44f24d7d..1b373757 100644 --- a/DecorReimagined/DecorReimagined.csproj +++ b/DecorReimagined/DecorReimagined.csproj @@ -2,12 +2,12 @@ Decor Reimagined - 4.8.0.0 + 4.9.0.0 ReimaginationTeam.DecorRework Reworks decor, making an attractive colony a better colony. 4.1.0.0 true - 526233 + 534889 Vanilla;Mergedown diff --git a/DecorReimagined/DecorReimaginedOptions.cs b/DecorReimagined/DecorReimaginedOptions.cs index df22cd7d..526e5658 100644 --- a/DecorReimagined/DecorReimaginedOptions.cs +++ b/DecorReimagined/DecorReimaginedOptions.cs @@ -151,14 +151,14 @@ public void ApplyToSculpture(GameObject obj) { var statuses = Db.Get().ArtableStatuses; foreach (var stage in Db.GetArtableStages().GetPrefabStages(obj.PrefabID())) { var artLevel = stage.statusItem; - if (artLevel == statuses.Ugly) + if (artLevel == statuses.LookingUgly) stage.decor = CrudeArtDecor; - else if (artLevel == statuses.Okay) + else if (artLevel == statuses.LookingOkay) stage.decor = QuaintArtDecor; - else if (artLevel == statuses.Great) + else if (artLevel == statuses.LookingGreat) // Good1, Good2, Good3 stage.decor = MasterpieceArtDecor; - else if (artLevel == statuses.Ready) + else if (artLevel == statuses.AwaitingArting) stage.decor = DefaultArtDecor; } } diff --git a/DecorReimagined/DecorReimaginedPatches.cs b/DecorReimagined/DecorReimaginedPatches.cs index 0b8b0c68..dccde334 100644 --- a/DecorReimagined/DecorReimaginedPatches.cs +++ b/DecorReimagined/DecorReimaginedPatches.cs @@ -313,7 +313,12 @@ public static class IceSculptureConfig_DoPostConfigureComplete_Patch { /// Applied after DoPostConfigureComplete runs. /// internal static void Postfix(GameObject go) { - Options?.ApplyToSculpture(go); + try { + Options?.ApplyToSculpture(go); + } catch (MemberAccessException e) { + PUtil.LogWarning("Unable to patch Ice Sculptures:"); + PUtil.LogExcWarn(e); + } } } @@ -362,7 +367,12 @@ public static class MarbleSculptureConfig_DoPostConfigureComplete_Patch { /// Applied after DoPostConfigureComplete runs. /// internal static void Postfix(GameObject go) { - Options?.ApplyToSculpture(go); + try { + Options?.ApplyToSculpture(go); + } catch (MemberAccessException e) { + PUtil.LogWarning("Unable to patch Marble Sculptures:"); + PUtil.LogExcWarn(e); + } } } @@ -376,7 +386,12 @@ public static class MetalSculptureConfig_DoPostConfigureComplete_Patch { /// Applied after DoPostConfigureComplete runs. /// internal static void Postfix(GameObject go) { - Options?.ApplyToSculpture(go); + try { + Options?.ApplyToSculpture(go); + } catch (MemberAccessException e) { + PUtil.LogWarning("Unable to patch Metal Sculptures:"); + PUtil.LogExcWarn(e); + } } } @@ -415,7 +430,12 @@ public static class SculptureConfig_DoPostConfigureComplete_Patch { /// Applied after DoPostConfigureComplete runs. /// internal static void Postfix(GameObject go) { - Options?.ApplyToSculpture(go); + try { + Options?.ApplyToSculpture(go); + } catch (MemberAccessException e) { + PUtil.LogWarning("Unable to patch Large Sculptures:"); + PUtil.LogExcWarn(e); + } } } @@ -429,7 +449,12 @@ public static class SmallSculptureConfig_DoPostConfigureComplete_Patch { /// Applied after DoPostConfigureComplete runs. /// internal static void Postfix(GameObject go) { - Options?.ApplyToSculpture(go); + try { + Options?.ApplyToSculpture(go); + } catch (MemberAccessException e) { + PUtil.LogWarning("Unable to patch Small Sculptures:"); + PUtil.LogExcWarn(e); + } } } diff --git a/FastTrack/FastTrack.csproj b/FastTrack/FastTrack.csproj index 0128b0a2..29ed3e1f 100644 --- a/FastTrack/FastTrack.csproj +++ b/FastTrack/FastTrack.csproj @@ -2,11 +2,11 @@ Fast Track - 0.9.18.0 + 0.10.0.0 PeterHan.FastTrack Optimizes Oxygen Not Included to improve performance. - 0.9.10.0 - 526233 + 0.10.0.0 + 534889 Vanilla;Mergedown true true diff --git a/FastTrack/FastTrackMod.cs b/FastTrack/FastTrackMod.cs index 39cf62fb..1c04307b 100644 --- a/FastTrack/FastTrackMod.cs +++ b/FastTrack/FastTrackMod.cs @@ -68,8 +68,6 @@ internal static void AfterDbInit() { GamePatches.BackgroundRoomProber.Init(); if (options.ThreatOvercrowding) CritterPatches.OvercrowdingMonitor_UpdateState_Patch.InitTagBits(); - if (options.RadiationOpts) - GamePatches.FastProtonCollider.Init(); if (options.SensorOpts) SensorPatches.SensorPatches.Init(); if (options.AnimOpts) @@ -166,6 +164,8 @@ internal static void OnEndGame() { GamePatches.SolidTransferArmUpdater.DestroyInstance(); if (options.PickupOpts || options.FastUpdatePickups) PathPatches.DeferredTriggers.DestroyInstance(); + if (options.RadiationOpts) + GamePatches.FastProtonCollider.Cleanup(); if (options.AsyncPathProbe) PathPatches.PathProbeJobManager.DestroyInstance(); GamePatches.AchievementPatches.DestroyInstance(); @@ -244,6 +244,8 @@ internal static void OnStartGame() { PathPatches.PathProbeJobManager.CreateInstance(); if (options.CachePaths) PathPatches.PathCacher.Init(); + if (options.FastReachability) + SensorPatches.FastGroupProber.Init(); if (options.SideScreenOpts) { UIPatches.AdditionalDetailsPanelWrapper.Init(); UIPatches.DetailsPanelWrapper.Init(); @@ -251,6 +253,8 @@ internal static void OnStartGame() { } if (options.PickupOpts || options.FastUpdatePickups) PathPatches.DeferredTriggers.CreateInstance(); + if (options.RadiationOpts) + GamePatches.FastProtonCollider.Init(); if (inst != null) { var go = inst.gameObject; go.AddOrGet(); diff --git a/FastTrack/GamePatches/ConcurrentHandleVector.cs b/FastTrack/GamePatches/ConcurrentHandleVector.cs index 43954beb..65188768 100644 --- a/FastTrack/GamePatches/ConcurrentHandleVector.cs +++ b/FastTrack/GamePatches/ConcurrentHandleVector.cs @@ -28,10 +28,13 @@ namespace PeterHan.FastTrack.GamePatches { /// A faster and thread safe version of KCompactedVector. /// public sealed class ConcurrentHandleVector : ICollection { - public int Count => lookup.Count; - + /// + /// Allows modification of the base collection. + /// public IDictionary BackingDictionary => lookup; + public int Count => lookup.Count; + public bool IsReadOnly => false; public T this[HandleVector.Handle handle] { @@ -147,7 +150,21 @@ IEnumerator IEnumerable.GetEnumerator() { } public bool Remove(T item) { - throw new NotImplementedException(); + bool removed = false; + // This is a best effort method, it may go very poorly + if (item == null) { + // Look for null + foreach (var pair in lookup) + if (pair.Value == null && (removed = lookup.TryRemove(pair.Key, out _))) + break; + } else { + var ec = EqualityComparer.Default; + foreach (var pair in lookup) + if (ec.Equals(item, pair.Value) && (removed = lookup.TryRemove(pair.Key, + out _))) + break; + } + return removed; } /// diff --git a/FastTrack/GamePatches/FastProtonCollider.cs b/FastTrack/GamePatches/FastProtonCollider.cs index 6188f2c8..9b1ace62 100644 --- a/FastTrack/GamePatches/FastProtonCollider.cs +++ b/FastTrack/GamePatches/FastProtonCollider.cs @@ -27,14 +27,18 @@ namespace PeterHan.FastTrack.GamePatches { [SkipSaveFileSerialization] public sealed class FastProtonCollider : KMonoBehaviour { /// - /// The tag bits that will prevent collision. + /// The tags that will prevent collision. /// - private static TagBits COLLIDE_IGNORE; + private static readonly Tag[] COLLIDE_IGNORE = { + GameTags.Dead, GameTags.Dying + }; /// - /// The tag bits with which to collide. + /// The tags with which to collide. /// - private static TagBits COLLIDE_WITH; + private static readonly Tag[] COLLIDE_WITH = { + GameTags.Creature, GameTags.Minion + }; /// /// Could not find a const for this in game... @@ -45,27 +49,39 @@ public sealed class FastProtonCollider : KMonoBehaviour { /// Or this one... /// public const int DISEASE_PER_CELL = 5; - + /// - /// The scene partitioner layer name to use. + /// Cleans up the collision mask layer. /// - internal const string RADBOLTS = nameof(HighEnergyParticle); - + internal static void Cleanup() { + hepLayer.Dispose(); + } + /// - /// The layer used for colliding ~~beams~~ radbolts. + /// Checks to see if a radbolt is colliding with another object. /// - internal static ScenePartitionerLayer hepLayer; + /// The position of the radbolt. + /// The location of the other object. + /// true if they are colliding, or false otherwise. + private static bool CollidesWith(Vector3 pos, Vector3 other) { + Vector2 difference = pos - other; + return difference.sqrMagnitude <= HighEnergyParticleConfig. + PARTICLE_COLLISION_SIZE * HighEnergyParticleConfig.PARTICLE_COLLISION_SIZE; + } /// - /// Initializes the tag masks used for collision. + /// Initializes the collision mask layer. /// internal static void Init() { - COLLIDE_WITH.SetTag(GameTags.Creature); - COLLIDE_WITH.SetTag(GameTags.Minion); - COLLIDE_IGNORE.SetTag(GameTags.Dead); - COLLIDE_IGNORE.SetTag(GameTags.Dying); + hepLayer = new ThreadsafePartitionerLayer("Radbolts", Grid.WidthInCells, Grid. + HeightInCells); } + /// + /// The layer used for colliding ~~beams~~ radbolts. + /// + internal static ThreadsafePartitionerLayer hepLayer; + #pragma warning disable IDE0044 #pragma warning disable CS0649 // These fields are automatically populated by KMonoBehaviour @@ -87,7 +103,7 @@ internal static void Init() { /// /// The partitioner entry into the radbolt collision layer. /// - private HandleVector.Handle partitionerEntry; + private ThreadsafePartitionerEntry partitionerEntry; /// /// The port that could capture this radbolt if it passes close enough. @@ -101,7 +117,7 @@ internal static void Init() { internal FastProtonCollider() { cachedCell = Grid.InvalidCell; - partitionerEntry = HandleVector.InvalidHandle; + partitionerEntry = null; port = null; } @@ -158,7 +174,7 @@ private bool CheckLivingCollision(GameObject item) { // Is it a creature/Duplicant and not already dead? bool collided = false; if (item != null && item.TryGetComponent(out KPrefabID prefabID) && prefabID. - HasAnyTags(ref COLLIDE_WITH) && !prefabID.HasAnyTags(ref COLLIDE_IGNORE) && + HasAnyTags(COLLIDE_WITH) && !prefabID.HasAnyTags(COLLIDE_IGNORE) && item.TryGetComponent(out Health hp) && !hp.IsDefeated()) { collided = true; hp.Damage(DAMAGE_ON_HIT); @@ -198,13 +214,15 @@ private void CheckLivingCollision(int cell) { /// The exact position of this radbolt. /// Whether the radbolt collided with another radbolt. private bool CheckRadboltCollision(int cell, Vector3 pos) { - var hits = ListPool.Allocate(); + var hits = ListPool.Allocate(); Grid.CellToXY(cell, out int x, out int y); - GameScenePartitioner.Instance.GatherEntries(x - 1, y - 1, 3, 3, hepLayer, hits); + hepLayer.Gather(hits, x - 1, y - 1, 3, 3); + if (hits.Count > 0) + PUtil.LogWarning(hits.Join(",")); int n = hits.Count; bool collided = false; for (int i = 0; i < n && !collided; i++) - if (hits[i].obj is HighEnergyParticle otherHEP && otherHEP != null && + if (hits[i].data is HighEnergyParticle otherHEP && otherHEP != null && otherHEP != hep && otherHEP.isCollideable && CollidesWith(pos, otherHEP.transform.position)) { hep.payload += otherHEP.payload; @@ -234,35 +252,25 @@ private bool CheckSolidTileCollision(int cell) { return collided; } - /// - /// Checks to see if this radbolt is colliding with another object. - /// - /// The position of this radbolt. - /// The location of the other object. - /// true if they are colliding, or false otherwise. - private bool CollidesWith(Vector3 pos, Vector3 other) { - Vector2 difference = pos - other; - return difference.sqrMagnitude <= HighEnergyParticleConfig. - PARTICLE_COLLISION_SIZE * HighEnergyParticleConfig.PARTICLE_COLLISION_SIZE; - } - /// /// Creates the scene partitioner entry for this collider. /// /// The current cell of this radbolt. private void CreatePartitioner(int cell) { - var gsp = GameScenePartitioner.Instance; - if (gsp != null && hepLayer != null) - partitionerEntry = gsp.Add(nameof(HighEnergyParticle), hep, cell, hepLayer, - null); + if (hepLayer != null) { + Grid.CellToXY(cell, out int x, out int y); + partitionerEntry = hepLayer.Add(x, y, hep, null); + } } /// /// Destroys the scene partitioner entry for this collider. /// private void DestroyPartitioner() { - if (partitionerEntry.IsValid()) - GameScenePartitioner.Instance.Free(ref partitionerEntry); + if (partitionerEntry != null) { + partitionerEntry.Dispose(); + partitionerEntry = null; + } } /// @@ -290,11 +298,10 @@ public void MovingUpdate(float dt) { SimMessages.ModifyDiseaseOnCell(newCell, diseaseIndex, DISEASE_PER_CELL); payload -= HighEnergyParticleConfig.PER_CELL_FALLOFF; - if (partitionerEntry.IsValid()) - // GSP had to be valid for the entry to exist in the first place - GameScenePartitioner.Instance.UpdatePosition(partitionerEntry, - newCell); - else + if (partitionerEntry != null) { + Grid.CellToXY(cell, out int x, out int y); + partitionerEntry.UpdatePosition(x, y); + } else CreatePartitioner(newCell); } if (payload <= 0.0f) diff --git a/FastTrack/GamePatches/SuitMarkerUpdater.cs b/FastTrack/GamePatches/SuitMarkerUpdater.cs index 56b32477..9fe54500 100644 --- a/FastTrack/GamePatches/SuitMarkerUpdater.cs +++ b/FastTrack/GamePatches/SuitMarkerUpdater.cs @@ -40,64 +40,95 @@ private static void DropSuit(Assignables equipment) { if (assignable.TryGetComponent(out Notifier notifier)) notifier.Add(notification); } + + /// + /// Processes a Duplicant putting on a suit. + /// + /// The checkpoint to walk by. + /// The Duplicant that is reacting. + /// true if the reaction was processed, or false otherwise. + internal static bool EquipReact(SuitMarker checkpoint, GameObject reactor) { + bool react = false; + if (reactor.TryGetComponent(out MinionIdentity id) && checkpoint.TryGetComponent( + out SuitMarkerUpdater updater)) { + var lockers = updater.docks; + int n = lockers.Count; + float bestScore = -float.MaxValue; + SuitLocker target = null; + for (int i = 0; i < n && bestScore < 1.0f; i++) { + var locker = lockers[i]; + float score = GetSuitScore(locker.GetStoredOutfit(), out _); + if (score >= 0.0f && (target == null || score > bestScore)) { + target = locker; + bestScore = score; + } + } + if (target != null) { + target.EquipTo(id.GetEquipment()); + updater.UpdateSuitStatus(); + } + react = true; + } + return react; + } /// - /// Checks the status of a suit locker to see if the suit can be used. + /// A much faster version of SuitLocker.GetSuitScore to evaluate whether a suit in a + /// dock can be used. /// - /// The suit dock to check. - /// Will contain with the suit if fully charged. - /// Will contain the suit if partially charged. - /// Will contain any suit inside. - /// true if the locker is vacant, or false if it is occupied. - internal static bool GetSuitStatus(SuitLocker locker, out KPrefabID fullyCharged, - out KPrefabID partiallyCharged, out KPrefabID suit) { - var smi = locker.smi; - bool vacant = false; - float minCharge = TUNING.EQUIPMENT.SUITS.MINIMUM_USABLE_SUIT_CHARGE; - suit = locker.GetStoredOutfit(); - // CanDropOffSuit calls GetStoredOutfit again, avoid! - if (suit == null) { - if (smi.sm.isConfigured.Get(smi) && !smi.sm.isWaitingForSuit.Get(smi)) - vacant = true; - fullyCharged = null; - partiallyCharged = null; - } else if (suit.TryGetComponent(out SuitTank tank) && tank.PercentFull() >= - minCharge) { - // Check for jet suit tank of petroleum - if (suit.TryGetComponent(out JetSuitTank petroTank)) { - fullyCharged = tank.IsFull() && petroTank.IsFull() ? suit : null; - partiallyCharged = petroTank.PercentFull() >= minCharge ? suit : null; + /// The suit to check. + /// The suit with sufficient charge, if any. + /// The score of the suit, which must be 0.0f or more to be used. + private static float GetSuitScore(KPrefabID outfit, out KPrefabID partiallyCharged) { + float score = -1.0f, charge; + KPrefabID result = null; + if (outfit != null && outfit.TryGetComponent(out SuitTank tank) && (charge = tank. + PercentFull()) >= TUNING.EQUIPMENT.SUITS.MINIMUM_USABLE_SUIT_CHARGE) { + if (outfit.TryGetComponent(out JetSuitTank jetTank)) { + float jetCharge = jetTank.PercentFull(); + if (jetCharge >= TUNING.EQUIPMENT.SUITS.MINIMUM_USABLE_SUIT_CHARGE) { + score = Mathf.Min(charge, jetCharge); + result = outfit; + } } else { - fullyCharged = tank.IsFull() ? suit : null; - partiallyCharged = suit; + score = charge; + result = outfit; } - } else { - fullyCharged = null; - partiallyCharged = null; } - return vacant; + partiallyCharged = result; + return score; } - + /// - /// Processes a Duplicant walking by the checkpoint. + /// Processes a Duplicant taking off a suit. /// /// The checkpoint to walk by. /// The Duplicant that is reacting. /// true if the reaction was processed, or false otherwise. - internal static bool React(SuitMarker checkpoint, GameObject reactor) { + internal static bool UnequipReact(SuitMarker checkpoint, GameObject reactor) { bool react = false; - if (reactor.TryGetComponent(out MinionIdentity id) && checkpoint.TryGetComponent( - out SuitMarkerUpdater updater)) { + if (reactor.TryGetComponent(out MinionIdentity id) && reactor.TryGetComponent( + out Navigator nav) && checkpoint.TryGetComponent(out SuitMarkerUpdater + updater)) { var equipment = id.GetEquipment(); - bool hasSuit = equipment.IsSlotOccupied(Db.Get().AssignableSlots.Suit); - if (reactor.TryGetComponent(out KBatchedAnimController kbac)) - kbac.RemoveAnimOverrides(checkpoint.interactAnim); - // If not wearing a suit, or the navigator can pass this checkpoint - bool changed = (!hasSuit || (reactor.TryGetComponent(out Navigator nav) && - (nav.flags & checkpoint.PathFlag) > PathFinder.PotentialPath.Flags. - None)) && updater.TryEquipSuit(equipment, hasSuit); - // Dump on floor, if they pass by with a suit and taking it off is impossible - if (!changed && hasSuit) + if ((nav.flags & checkpoint.PathFlag) > PathFinder.PotentialPath.Flags.None) { + var lockers = updater.docks; + int n = lockers.Count; + SuitLocker target = null; + for (int i = 0; i < n; i++) { + var locker = lockers[i]; + if (locker.CanDropOffSuit()) { + target = locker; + break; + } + } + react = target != null; + if (react) { + target.UnequipFrom(equipment); + updater.UpdateSuitStatus(); + } + } + if (!react) DropSuit(equipment); react = true; } @@ -160,50 +191,6 @@ public void Sim1000ms(float _) { } } - /// - /// Attempts to equip or uneqip a suit to a passing Duplicant. - /// - /// The Duplicant to be equipped or uneqipped. - /// Whether they already have a suit. - /// true if a suit was added or removed, or false otherwise. - private bool TryEquipSuit(Equipment equipment, bool hasSuit) { - bool changed = false; - int n = docks.Count; - for (int i = 0; i < n; i++) { - var dock = docks[i]; - if (dock != null) { - if (GetSuitStatus(dock, out var fullyCharged, out _, out _) && hasSuit) { - dock.UnequipFrom(equipment); - changed = true; - break; - } - if (!hasSuit && fullyCharged != null) { - dock.EquipTo(equipment); - changed = true; - break; - } - } - } - if (!hasSuit && !changed) { - SuitLocker bestAvailable = null; - float maxScore = 0f; - for (int i = 0; i < n; i++) { - var dock = docks[i]; - if (dock != null && dock.GetSuitScore() > maxScore) { - bestAvailable = dock; - maxScore = dock.GetSuitScore(); - } - } - if (bestAvailable != null) { - bestAvailable.EquipTo(equipment); - changed = true; - } - } - if (changed) - UpdateSuitStatus(); - return changed; - } - /// /// Updates the status of the suits in nearby suit docks for pathfinding and /// animation purposes. @@ -211,17 +198,24 @@ private bool TryEquipSuit(Equipment equipment, bool hasSuit) { internal void UpdateSuitStatus() { if (suitCheckpoint != null) { KPrefabID availableSuit = null; - int charged = 0, vacancies = 0; - foreach (var dock in docks) + int charged = 0, vacancies = 0, n = docks.Count; + for (int i = 0; i < n; i++) { + var dock = docks[i]; if (dock != null) { - if (GetSuitStatus(dock, out _, out var partiallyCharged, - out var outfit)) + var smi = dock.smi; + var outfit = dock.GetStoredOutfit(); + if (smi.sm.isConfigured.Get(smi) && !smi.sm.isWaitingForSuit. + Get(smi) && outfit == null) vacancies++; - else if (partiallyCharged != null) - charged++; - if (availableSuit == null) + else { + GetSuitScore(outfit, out var partiallyCharged); + if (partiallyCharged != null) + charged++; + } + if (availableSuit == null && outfit != null) availableSuit = outfit; } + } bool hasSuit = availableSuit != null; if (hasSuit != hadAvailableSuit) { anim.Play(hasSuit ? "off" : "no_suit"); @@ -232,6 +226,24 @@ internal void UpdateSuitStatus() { } } } + + /// + /// Applied to SuitMarker.EquipSuitReactable to make the Run method more efficient and + /// use the SuitMarkerUpdater.. + /// + [HarmonyPatch(typeof(SuitMarker.EquipSuitReactable), nameof(SuitMarker. + EquipSuitReactable.Run))] + public static class SuitMarker_EquipSuitReactable_Run_Patch { + internal static bool Prepare() => FastTrackOptions.Instance.MiscOpts; + + /// + /// Applied before Run runs. + /// + internal static bool Prefix(GameObject ___reactor, SuitMarker ___suitMarker) { + return ___reactor != null && ___suitMarker != null && !SuitMarkerUpdater. + EquipReact(___suitMarker, ___reactor); + } + } /// /// Applied to SuitMarker to add an improved updater to each instance. @@ -251,36 +263,36 @@ internal static void Postfix(SuitMarker __instance) { } /// - /// Applied to SuitMarker.SuitMarkerReactable to make the Run method more efficient and - /// use the SuitMarkerUpdater.. + /// Applied to SuitMarker to turn off the expensive Update method. The SuitMarkerUpdater + /// component can update the SuitMarker at more appropriate rates. /// - [HarmonyPatch(typeof(SuitMarker.SuitMarkerReactable), nameof(SuitMarker. - SuitMarkerReactable.Run))] - public static class SuitMarker_SuitMarkerReactable_Run_Patch { + [HarmonyPatch(typeof(SuitMarker), nameof(SuitMarker.Update))] + public static class SuitMarker_Update_Patch { internal static bool Prepare() => FastTrackOptions.Instance.MiscOpts; /// - /// Applied before Run runs. + /// Applied before Update runs. /// - internal static bool Prefix(GameObject ___reactor, SuitMarker ___suitMarker) { - return ___reactor != null && ___suitMarker != null && !SuitMarkerUpdater.React( - ___suitMarker, ___reactor); + internal static bool Prefix() { + return false; } } - + /// - /// Applied to SuitMarker to turn off the expensive Update method. The SuitMarkerUpdater - /// component can update the SuitMarker at more appropriate rates. + /// Applied to SuitMarker.UnequipSuitReactable to make the Run method more efficient and + /// use the SuitMarkerUpdater.. /// - [HarmonyPatch(typeof(SuitMarker), nameof(SuitMarker.Update))] - public static class SuitMarker_Update_Patch { + [HarmonyPatch(typeof(SuitMarker.UnequipSuitReactable), nameof(SuitMarker. + UnequipSuitReactable.Run))] + public static class SuitMarker_UnequipSuitReactable_Run_Patch { internal static bool Prepare() => FastTrackOptions.Instance.MiscOpts; /// - /// Applied before Update runs. + /// Applied before Run runs. /// - internal static bool Prefix() { - return false; + internal static bool Prefix(GameObject ___reactor, SuitMarker ___suitMarker) { + return ___reactor != null && ___suitMarker != null && !SuitMarkerUpdater. + UnequipReact(___suitMarker, ___reactor); } } } diff --git a/FastTrack/SensorPatches/FastGroupProber.cs b/FastTrack/SensorPatches/FastGroupProber.cs index 21130d0b..e7e11023 100644 --- a/FastTrack/SensorPatches/FastGroupProber.cs +++ b/FastTrack/SensorPatches/FastGroupProber.cs @@ -49,16 +49,20 @@ internal static void Cleanup() { /// /// Creates the singleton instance of this class. /// - /// The scene partitioner layer to use. - internal static void Init(ScenePartitionerLayer mask) { + internal static void Init() { Cleanup(); - Instance = new FastGroupProber(mask); + Instance = new FastGroupProber(); } #if DEBUG internal int[] Cells => cells; #endif + /// + /// The partitioner layer used to handle updates. + /// + public ThreadsafePartitionerLayer Mask { get; } + /// /// The cells which were added (background thread use only). /// @@ -83,12 +87,7 @@ internal static void Init(ScenePartitionerLayer mask) { /// The cells which were marked dirty during path probes. /// private readonly ConcurrentQueue dirtyCells; - - /// - /// The mask used to trigger changes to the reachable objects layer. - /// - internal readonly ScenePartitionerLayer mask; - + /// /// The cells which were marked dirty during path probes. /// @@ -114,13 +113,14 @@ internal static void Init(ScenePartitionerLayer mask) { /// private readonly EventWaitHandle trigger; - private FastGroupProber(ScenePartitionerLayer mask) { + private FastGroupProber() { added = new List(256); alreadyDirty = new HashSet(); cells = new int[Grid.CellCount]; destroyed = false; dirtyCells = new ConcurrentQueue(); - this.mask = mask; + Mask = new ThreadsafePartitionerLayer("Path Updates", Grid.WidthInCells, Grid. + HeightInCells); probers = new ConcurrentDictionary(2, 64); removed = new HashSet(); toDestroy = new Queue(8); @@ -144,7 +144,7 @@ internal void Allocate(object prober) { } public void Dispose() { - // mask is destroyed by the GameScenePartitioner OnCleanUp + Mask.Dispose(); toDo.Clear(); destroyed = true; trigger.Set(); @@ -296,15 +296,12 @@ internal void Remove(object prober) { /// the queue on the foreground thread. /// internal void Update() { - var gsp = GameScenePartitioner.Instance; - if (gsp != null) { - // Must trigger scene partitioner updates on the foreground thread - var dirtyList = ListPool.Allocate(); - while (dirtyCells.TryDequeue(out int cell)) - dirtyList.Add(cell); - gsp.TriggerEvent(dirtyList, mask, this); - dirtyList.Recycle(); - } + // Trigger partitioner updates on the foreground thread + var dirtyList = ListPool.Allocate(); + while (dirtyCells.TryDequeue(out int cell)) + dirtyList.Add(cell); + Mask.Trigger(dirtyList, this); + dirtyList.Recycle(); int n = toDo.Count; if (n > 0 && FastTrackMod.GameRunning) { if (n > MIN_PROCESS) diff --git a/FastTrack/SensorPatches/FastReachabilityMonitor.cs b/FastTrack/SensorPatches/FastReachabilityMonitor.cs index 8dacaf8c..542d8e26 100644 --- a/FastTrack/SensorPatches/FastReachabilityMonitor.cs +++ b/FastTrack/SensorPatches/FastReachabilityMonitor.cs @@ -34,11 +34,6 @@ namespace PeterHan.FastTrack.SensorPatches { /// [SkipSaveFileSerialization] public sealed class FastReachabilityMonitor : KMonoBehaviour, ISim4000ms { - /// - /// The name of the layer used for the reachability scene partitioner. - /// - public const string REACHABILITY = nameof(FastReachabilityMonitor); - /// /// The last set of extents from which this item was reachable. /// @@ -52,7 +47,7 @@ public sealed class FastReachabilityMonitor : KMonoBehaviour, ISim4000ms { /// /// Registers a scene partitioner entry for nav cells changing. /// - private HandleVector.Handle partitionerEntry; + private ThreadsafePartitionerEntry partitionerEntry; /// /// The existing reachability monitor used to check and trigger events. @@ -62,12 +57,14 @@ public sealed class FastReachabilityMonitor : KMonoBehaviour, ISim4000ms { internal FastReachabilityMonitor() { lastExtents = new Extents(int.MinValue, int.MinValue, 1, 1); master = null; - partitionerEntry = HandleVector.InvalidHandle; + partitionerEntry = null; } public override void OnCleanUp() { - if (partitionerEntry.IsValid()) - GameScenePartitioner.Instance.Free(ref partitionerEntry); + if (partitionerEntry != null) { + partitionerEntry.Dispose(); + partitionerEntry = null; + } if (master != null) { master.Unsubscribe((int)GameHashes.Landed); master.Unsubscribe((int)GameHashes.CellChanged); @@ -117,25 +114,26 @@ public void UpdateOffsets() { var inst = FastGroupProber.Instance; if (inst != null && master != null) { int cell = Grid.PosToCell(master.transform.position); - var gsp = GameScenePartitioner.Instance; if (Grid.IsValidCell(cell) && cell > 0) { var extents = new Extents(cell, smi.master.GetOffsets(cell)); // Only if the extents actually changed if (extents.x != lastExtents.x || extents.y != lastExtents.y || extents. width != lastExtents.width || extents.height != lastExtents. height) { - if (partitionerEntry.IsValid()) - gsp.UpdatePosition(partitionerEntry, extents); + if (partitionerEntry != null) + partitionerEntry.UpdatePosition(extents); else - partitionerEntry = gsp.Add("FastReachabilityMonitor.UpdateOffsets", - this, extents, inst.mask, OnReachableChanged); + partitionerEntry = inst.Mask.Add(extents, this, + OnReachableChanged); inst.Enqueue(smi); lastExtents = extents; } } else if (lastExtents.x >= 0 || lastExtents.y >= 0) { // Payloads and worn items are sometimes moved to (0, 0) or cell -1 - if (partitionerEntry.IsValid()) - gsp.Free(ref partitionerEntry); + if (partitionerEntry != null) { + partitionerEntry.Dispose(); + partitionerEntry = null; + } smi.sm.isReachable.Set(false, smi); lastExtents.x = int.MinValue; lastExtents.y = int.MinValue; diff --git a/FastTrack/SharedPatches.cs b/FastTrack/SharedPatches.cs index 5a881c4d..9477a4fe 100644 --- a/FastTrack/SharedPatches.cs +++ b/FastTrack/SharedPatches.cs @@ -87,34 +87,6 @@ internal static void Postfix() { } } - /// - /// Applied to GameScenePartitioner to create a mask for triggering updates. - /// - /// XXX: There are only a few mask layers left! - /// - [HarmonyPatch(typeof(GameScenePartitioner), nameof(GameScenePartitioner.OnPrefabInit))] - public static class GameScenePartitioner_OnPrefabInit_Patch { - internal static bool Prepare() { - var options = FastTrackOptions.Instance; - return options.FastReachability || options.RadiationOpts; - } - - /// - /// Applied after OnPrefabInit runs. - /// - internal static void Postfix(ScenePartitioner ___partitioner) { - var options = FastTrackOptions.Instance; - if (___partitioner != null) { - if (options.FastReachability) - SensorPatches.FastGroupProber.Init(___partitioner.CreateMask(SensorPatches. - FastReachabilityMonitor.REACHABILITY)); - if (options.RadiationOpts) - GamePatches.FastProtonCollider.hepLayer = ___partitioner.CreateMask( - GamePatches.FastProtonCollider.RADBOLTS); - } - } - } - /// /// Applied to Global to start up some expensive things before Game.LateUpdate runs. /// diff --git a/FastTrack/ThreadsafePartitionerLayer.cs b/FastTrack/ThreadsafePartitionerLayer.cs new file mode 100644 index 00000000..b6985524 --- /dev/null +++ b/FastTrack/ThreadsafePartitionerLayer.cs @@ -0,0 +1,410 @@ +/* + * Copyright 2022 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; +using System.Collections.Generic; +using System.Threading; +using PeterHan.PLib.Core; + +namespace PeterHan.FastTrack { + /// + /// A thread-safe layer akin to ScenePartitionerLayer; there is no central manager like + /// GameScenePartitioner, instances must be managed separately. + /// + public sealed class ThreadsafePartitionerLayer : IDisposable { + /// + /// Entries are grouped in blocks of this size. + /// + public const int NODE_SIZE = 16; + + /// + /// The layer's friendly name. + /// + public readonly string name; + + /// + /// The next entry ID. + /// + private volatile int nextID; + + /// + /// The grid height in nodes. + /// + private readonly int nodeHeight; + + /// + /// The nodes used for partitioning. + /// + private readonly HashSet[,] nodes; + + /// + /// The grid width in nodes. + /// + private readonly int nodeWidth; + + /// + /// The event triggered when any changes occur in the layer. + /// + public Action OnEvent; + + public ThreadsafePartitionerLayer(string name, int width, int height) { + if (width <= 0) + throw new ArgumentOutOfRangeException(nameof(width)); + if (height <= 0) + throw new ArgumentOutOfRangeException(nameof(height)); + this.name = name ?? "Layer"; + nextID = 0; + nodeHeight = (height + NODE_SIZE - 1) / NODE_SIZE; + nodeWidth = (width + NODE_SIZE - 1) / NODE_SIZE; + nodes = new HashSet[nodeWidth, nodeHeight]; + for (int i = 0; i < nodeWidth; i++) + for (int j = 0; j < nodeHeight; j++) + // There are not that many, and they are pretty small when empty + nodes[i, j] = new HashSet(); + } + + /// + /// Adds a new scene partitioner entry. The extents will have a height and width of 1. + /// + /// The entry X coordinate. + /// The entry Y coordinate. + /// The data to associate with the entry. + /// The callback to run when the entry is gathered. + /// The partitioner entry. + public ThreadsafePartitionerEntry Add(int x, int y, object data, + Action onEvent) { + var extents = new Extents(x, y, 1, 1); + var entry = new ThreadsafePartitionerEntry(this, ref extents, Interlocked. + Increment(ref nextID), data, onEvent); + Update(entry); + return entry; + } + + /// + /// Adds a new scene partitioner entry. + /// + /// The extents occupied by the entry. + /// The data to associate with the entry. + /// The callback to run when the entry is gathered. + /// The partitioner entry. + public ThreadsafePartitionerEntry Add(Extents extents, object data, + Action onEvent) { + var entry = new ThreadsafePartitionerEntry(this, ref extents, Interlocked. + Increment(ref nextID), data, onEvent); + Update(entry); + return entry; + } + + /// + /// Limits the index to valid node X indexes. + /// + /// The X coordinate. + /// An X index inside the valid range. + private int ClampNodeX(int x) { + if (x < 0) x = 0; + if (x >= nodeWidth) x = nodeWidth - 1; + return x; + } + + /// + /// Limits the index to valid node Y indexes. + /// + /// The Y coordinate. + /// A Y index inside the valid range. + private int ClampNodeY(int y) { + if (y < 0) y = 0; + if (y >= nodeHeight) y = nodeHeight - 1; + return y; + } + + public void Dispose() { + OnEvent = null; + for (int i = 0; i < nodeWidth; i++) + for (int j = 0; j < nodeHeight; j++) { + var set = nodes[i, j]; + lock (set) { + set.Clear(); + } + } + } + + public override bool Equals(object obj) { + return obj is ThreadsafePartitionerLayer other && other.name == name; + } + + public override int GetHashCode() { + return name.GetHashCode(); + } + + /// + /// Gathers all of the entries which are exactly inside the given extents. + /// + /// The location where the gathered entries will be stored. + /// The region to search. + public void Gather(ICollection entries, Extents extents) { + Gather(entries, extents.x, extents.y, extents.width, extents.height); + } + + /// + /// Gathers all of the entries which are exactly inside the given extents. + /// + /// The location where the gathered entries will be stored. + /// The region X coordinate. + /// The region Y coordinate. + /// The width of the region. + /// The height of the region. + public void Gather(ICollection entries, int x, int y, + int width, int height) { + var nodeExtents = GetNodeExtents(x, y, width, height); + int nx = nodeExtents.x, nh = nodeExtents.height, maxX = x + width - 1, + maxY = y + height - 1, nsy = nodeExtents.y; + for (int dx = nodeExtents.width; dx > 0; dx--) { + int ny = nsy; + for (int dy = nh; dy > 0; dy--) { + var set = nodes[nx, ny]; + lock (set) { + foreach (var entry in set) { + ref var extents = ref entry.extents; + int ex = extents.x, ey = extents.y; + if (maxX >= ex && x <= ex + extents.width - 1 && maxY >= ey && y <= + ey + extents.height - 1) + entries.Add(entry); + } + } + ny++; + } + nx++; + } + } + + /// + /// Converts coordinates to node extents. The node extents are conservative and will + /// always include the coordinates if the coordinates are valid. + /// + /// The region X coordinate. + /// The region Y coordinate. + /// The width of the region. + /// The height of the region. + /// The coordinates as node indexes. + private Extents GetNodeExtents(int x, int y, int width, int height) { + int nx = ClampNodeX(x / NODE_SIZE); + int ny = ClampNodeY(y / NODE_SIZE); + return new Extents(nx, ny, ClampNodeX((x + width - 1) / NODE_SIZE) - nx + 1, + ClampNodeY((y + height - 1) / NODE_SIZE) - ny + 1); + } + + /// + /// Removes a partitioner entry. + /// + /// The entry to remove. + internal void Remove(ThreadsafePartitionerEntry entry) { + ref var ee = ref entry.extents; + var extents = GetNodeExtents(ee.x, ee.y, ee.width, ee.height); + int x = extents.x, height = extents.height, ey = extents.y; + for (int dx = extents.width; dx > 0; dx--) { + int y = ey; + for (int dy = height; dy > 0; dy--) { + var set = nodes[x, y]; + lock (set) { + set.Remove(entry); + } + y++; + } + x++; + } + } + + public override string ToString() { + return name; + } + + /// + /// Triggers an event on the specified cells. + /// + /// The cell locations (will be converted with Grid.CellToXY) to trigger. + /// The data to pass the event. + public void Trigger(IEnumerable cells, object data) { + var evt = OnEvent; + var uniqueEntries = HashSetPool.Allocate(); + foreach (int cell in cells) + if (Grid.IsValidCell(cell)) { + evt?.Invoke(cell, data); + Grid.CellToXY(cell, out int x, out int y); + Gather(uniqueEntries, x, y, 1, 1); + } + foreach (var entry in uniqueEntries) + entry.OnEvent?.Invoke(data); + uniqueEntries.Recycle(); + } + + /// + /// Triggers an event on the specified cell. + /// + /// The cell location (will be converted with Grid.CellToXY) to trigger. + /// The data to pass the event. + public void Trigger(int cell, object data) { + if (Grid.IsValidCell(cell)) { + Grid.CellToXY(cell, out int x, out int y); + var uniqueEntries = HashSetPool.Allocate(); + OnEvent?.Invoke(cell, data); + Gather(uniqueEntries, x, y, 1, 1); + foreach (var entry in uniqueEntries) + entry.OnEvent?.Invoke(data); + uniqueEntries.Recycle(); + } + } + + /// + /// Triggers an event on all cells in the given rectangle. + /// + /// The region X coordinate. + /// The region Y coordinate. + /// The width of the region. + /// The height of the region. + /// The data to pass the event. + public void Trigger(int x, int y, int width, int height, object data) { + int cx = x; + var evt = OnEvent; + var uniqueEntries = HashSetPool.Allocate(); + for (int dx = width; dx > 0; dx--) { + int cy = y; + for (int dy = height; dy > 0; dy--) { + int cell = Grid.XYToCell(cx, cy); + if (Grid.IsValidCell(cell)) + evt?.Invoke(cell, data); + cy++; + } + cx++; + } + Gather(uniqueEntries, x, y, width, height); + foreach (var entry in uniqueEntries) + entry.OnEvent?.Invoke(data); + uniqueEntries.Recycle(); + } + + /// + /// Adds a partitioner entry. + /// + /// The entry to re-add. + internal void Update(ThreadsafePartitionerEntry entry) { + ref var ee = ref entry.extents; + var extents = GetNodeExtents(ee.x, ee.y, ee.width, ee.height); + int x = extents.x, height = extents.height, ey = extents.y; + for (int dx = extents.width; dx > 0; dx--) { + int y = ey; + for (int dy = height; dy > 0; dy--) { + var set = nodes[x, y]; + lock (set) { + set.Add(entry); + } + y++; + } + x++; + } + } + } + + /// + /// A refernce to an entry used in the threadsafe scene partitioner. + /// + public sealed class ThreadsafePartitionerEntry : IEquatable, + IDisposable { + /// + /// The data stored in this partitioner entry. + /// + public object data; + + /// + /// The extents of this object. + /// + internal Extents extents; + + /// + /// The unique ID of this entry. + /// + internal readonly int id; + + /// + /// Called when a request to execute all entries in a given area is given. + /// + public readonly Action OnEvent; + + /// + /// The partitioner which owns this entry. + /// + private readonly ThreadsafePartitionerLayer partitioner; + + internal ThreadsafePartitionerEntry(ThreadsafePartitionerLayer partitioner, + ref Extents extents, int id, object data, Action onEvent) { + this.data = data; + this.extents = extents; + this.id = id; + this.partitioner = partitioner ?? throw new ArgumentNullException( + nameof(partitioner)); + OnEvent = onEvent; + } + + public void Dispose() { + partitioner.Remove(this); + } + + public override bool Equals(object obj) { + return obj is ThreadsafePartitionerEntry other && other.id == id; + } + + public bool Equals(ThreadsafePartitionerEntry other) { + return other != null && other.id == id; + } + + public override int GetHashCode() { + return id; + } + + /// + /// Reports a move of this entry to another location. + /// + /// The new X coordinate. + /// The new Y coordinate. + public void UpdatePosition(int x, int y) { + partitioner.Remove(this); + extents.x = x; + extents.y = y; + extents.height = 1; + extents.width = 1; + partitioner.Update(this); + } + + /// + /// Reports a move of this entry to another location. + /// + /// The new extents of this entry. + public void UpdatePosition(Extents newExtents) { + partitioner.Remove(this); + extents = newExtents; + partitioner.Update(this); + } + + public override string ToString() { + return "ScenePartitionerEntry[layer={0},x={1:D},y={2:D},width={3:D},height={4:D}]". + F(partitioner.name, extents.x, extents.y, extents.width, extents.height); + } + } +} diff --git a/FastTrack/UIPatches/SimpleInfoScreenWrapper.cs b/FastTrack/UIPatches/SimpleInfoScreenWrapper.cs index 4c9c0c2a..094de356 100644 --- a/FastTrack/UIPatches/SimpleInfoScreenWrapper.cs +++ b/FastTrack/UIPatches/SimpleInfoScreenWrapper.cs @@ -296,7 +296,7 @@ internal void Refresh(bool force) { int count = statusItems.Count; bool showStatus = count > 0; if (force) - Update200ms(); + UpdatePanels(); if (showStatus != statusActive) { sis.statusItemPanel.gameObject.SetActive(showStatus); statusActive = showStatus; @@ -551,16 +551,15 @@ private void SetPanels(GameObject target) { } public void Sim200ms(float _) { - if (sis.lastTarget != null && storageParent != null && isActiveAndEnabled) { - Update200ms(); - } + if (sis.lastTarget != null && storageParent != null && isActiveAndEnabled) + UpdatePanels(); } /// /// Updates the panels that should be updated every 200ms or when the selected object /// changes. /// - private void Update200ms() { + private void UpdatePanels() { var vitalsContainer = sis.vitalsContainer; RefreshStress(); if (vitalsActive) { diff --git a/FastTrack/VisualPatches/KAnimBatchPatches.cs b/FastTrack/VisualPatches/KAnimBatchPatches.cs index dd244a1e..a0b94274 100644 --- a/FastTrack/VisualPatches/KAnimBatchPatches.cs +++ b/FastTrack/VisualPatches/KAnimBatchPatches.cs @@ -202,9 +202,10 @@ private static KAnimBatchTextureCache.Entry SetupOverride(KAnimBatch instance, if (overrideTex == null) { var bg = instance.group; var properties = instance.matProperties; - overrideTex = bg.CreateTexture("SymbolOverrideInfoTex", KAnimBatchGroup. - GetBestTextureSize(bg.data.maxSymbolFrameInstancesPerbuild * bg. - maxGroupSize * SymbolOverrideInfoGpuData.FLOATS_PER_SYMBOL_OVERRIDE_INFO), + var size = KAnimBatchGroup.GetBestTextureSize(bg.data. + maxSymbolFrameInstancesPerbuild * bg.maxGroupSize * + SymbolOverrideInfoGpuData.FLOATS_PER_SYMBOL_OVERRIDE_INFO); + overrideTex = bg.CreateTexture("SymbolOverrideInfoTex", size.x, size.y, KAnimBatch.ShaderProperty_symbolOverrideInfoTex, KAnimBatch. ShaderProperty_SYMBOL_OVERRIDE_INFO_TEXTURE_SIZE); overrideTex.SetTextureAndSize(properties); diff --git a/NotEnoughTags/SpamObjectsHandler.cs b/NotEnoughTags/SpamObjectsHandler.cs index b7c1591e..e89b3f37 100644 --- a/NotEnoughTags/SpamObjectsHandler.cs +++ b/NotEnoughTags/SpamObjectsHandler.cs @@ -34,7 +34,7 @@ internal sealed class SpamObjectsHandler : IInputHandler { [PLibMethod(RunAt.AfterLayerableLoad)] internal static void AddSpamHandler() { - KInputHandler.Add(Global.Instance.GetInputManager().GetDefaultController(), + KInputHandler.Add(Global.GetInputManager().GetDefaultController(), new SpamObjectsHandler(), 512); }