From 41f042b8f9748852e560b8a5ab288d4cb21fd819 Mon Sep 17 00:00:00 2001 From: Princess Cheeseballs <66055347+Princess-Cheeseballs@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:20:38 -0800 Subject: [PATCH] PredictedRandom Helpers (#42797) * PredictedRandom Helpers * fixxxx * documentation!!! * pram --------- Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com> --- .../Body/Systems/SharedBloodstreamSystem.cs | 11 +++---- .../Clothing/SharedCursedMaskSystem.cs | 3 +- Content.Shared/Clumsy/ClumsySystem.cs | 26 ++++------------ Content.Shared/Dice/SharedDiceSystem.cs | 3 +- .../SharedEntityEffectsSystem.cs | 8 +---- Content.Shared/Flash/SharedFlashSystem.cs | 5 +--- .../Fluids/EntitySystems/DrainSystem.cs | 5 +--- .../GhostTypes/GhostSpriteStateSystem.cs | 4 +-- .../Kitchen/SharedKitchenSpikeSystem.cs | 5 +--- .../EntitySystems/IngestionSystem.Utensils.cs | 6 +--- .../EntitySystems/MessyDrinkerSystem.cs | 5 +--- .../Random/Helpers/SharedRandomExtensions.cs | 30 +++++++++++++++++++ Content.Shared/Throwing/CatchableSystem.cs | 5 +--- .../Traits/Assorted/NarcolepsySystem.cs | 4 +-- .../Systems/RandomTriggerOnTriggerSystem.cs | 11 +------ .../Systems/TriggerSystem.Condition.cs | 13 ++------ 16 files changed, 56 insertions(+), 88 deletions(-) diff --git a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs index 2bce22c0454..ec0521b7b63 100644 --- a/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs +++ b/Content.Shared/Body/Systems/SharedBloodstreamSystem.cs @@ -197,16 +197,13 @@ public abstract class SharedBloodstreamSystem : EntitySystem var totalFloat = total.Float(); TryModifyBleedAmount(ent.AsNullable(), totalFloat); - /// Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5 - /// The crit chance is currently the bleed rate modifier divided by 25. - /// Higher damage weapons have a higher chance to crit! + // Critical hit. Causes target to lose blood, using the bleed rate modifier of the weapon, currently divided by 5 + // The crit chance is currently the bleed rate modifier divided by 25. + // Higher damage weapons have a higher chance to crit! - // TODO: Replace with RandomPredicted once the engine PR is merged // Use both the receiver and the damage causing entity for the seed so that we have different results for multiple attacks in the same tick - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id, GetNetEntity(args.Origin)?.Id ?? 0 ); - var rand = new System.Random(seed); var prob = Math.Clamp(totalFloat / 25, 0, 1); - if (totalFloat > 0 && rand.Prob(prob)) + if (totalFloat > 0 && SharedRandomExtensions.PredictedProb(_timing, prob, GetNetEntity(ent), GetNetEntity(args.Origin))) { TryBleedOut(ent.AsNullable(), total / 5); _audio.PlayPredicted(ent.Comp.InstantBloodSound, ent, args.Origin); diff --git a/Content.Shared/Clothing/SharedCursedMaskSystem.cs b/Content.Shared/Clothing/SharedCursedMaskSystem.cs index 359e8ef769b..3cd700f4102 100644 --- a/Content.Shared/Clothing/SharedCursedMaskSystem.cs +++ b/Content.Shared/Clothing/SharedCursedMaskSystem.cs @@ -4,6 +4,7 @@ using Content.Shared.Damage.Systems; using Content.Shared.Examine; using Content.Shared.Inventory; using Content.Shared.Movement.Systems; +using Content.Shared.Random.Helpers; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -61,7 +62,7 @@ public abstract class SharedCursedMaskSystem : EntitySystem protected void RandomizeCursedMask(Entity ent, EntityUid wearer) { - var random = new System.Random((int) _timing.CurTick.Value); + var random = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(ent)); ent.Comp.CurrentState = random.Pick(Enum.GetValues()); _appearance.SetData(ent, CursedMaskVisuals.State, ent.Comp.CurrentState); _movementSpeedModifier.RefreshMovementSpeedModifiers(wearer); diff --git a/Content.Shared/Clumsy/ClumsySystem.cs b/Content.Shared/Clumsy/ClumsySystem.cs index 4650065ad62..54064c62bd8 100644 --- a/Content.Shared/Clumsy/ClumsySystem.cs +++ b/Content.Shared/Clumsy/ClumsySystem.cs @@ -48,10 +48,7 @@ public sealed class ClumsySystem : EntitySystem if (!ent.Comp.ClumsyHypo) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; args.TargetGettingInjected = args.EntityUsingInjector; @@ -67,10 +64,7 @@ public sealed class ClumsySystem : EntitySystem if (!ent.Comp.ClumsyDefib) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; args.DefibTarget = args.EntityUsingDefib; @@ -86,10 +80,7 @@ public sealed class ClumsySystem : EntitySystem if (!ent.Comp.ClumsyCatching) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(args.Item).Id); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; args.Cancelled = true; // fail to catch @@ -120,10 +111,7 @@ public sealed class ClumsySystem : EntitySystem if (args.Gun.Comp.ClumsyProof) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(args.Gun).Id); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; if (ent.Comp.GunShootFailDamage != null) @@ -145,10 +133,8 @@ public sealed class ClumsySystem : EntitySystem if (!ent.Comp.ClumsyVaulting) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id); - var rand = new System.Random(seed); - if (!_cfg.GetCVar(CCVars.GameTableBonk) && !rand.Prob(ent.Comp.ClumsyDefaultCheck)) + if (!_cfg.GetCVar(CCVars.GameTableBonk) + && !SharedRandomExtensions.PredictedProb(_timing, ent.Comp.ClumsyDefaultCheck, GetNetEntity(ent))) return; HitHeadClumsy(ent, args.BeingClimbedOn); diff --git a/Content.Shared/Dice/SharedDiceSystem.cs b/Content.Shared/Dice/SharedDiceSystem.cs index 71a51584d36..ed62355dca5 100644 --- a/Content.Shared/Dice/SharedDiceSystem.cs +++ b/Content.Shared/Dice/SharedDiceSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Examine; using Content.Shared.Interaction.Events; using Content.Shared.Popups; +using Content.Shared.Random.Helpers; using Content.Shared.Throwing; using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; @@ -72,7 +73,7 @@ public abstract class SharedDiceSystem : EntitySystem private void Roll(Entity entity, EntityUid? user = null) { - var rand = new System.Random((int)_timing.CurTick.Value); + var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(entity)); var roll = rand.Next(1, entity.Comp.Sides + 1); SetCurrentSide(entity, roll); diff --git a/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs b/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs index 2da23a25023..7b39cfff963 100644 --- a/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs +++ b/Content.Shared/EntityEffects/SharedEntityEffectsSystem.cs @@ -4,7 +4,6 @@ using Content.Shared.Chemistry.Reaction; using Content.Shared.EntityConditions; using Content.Shared.FixedPoint; using Content.Shared.Random.Helpers; -using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Shared.EntityEffects; @@ -95,13 +94,8 @@ public sealed partial class SharedEntityEffectsSystem : EntitySystem, IEntityEff return false; // TODO: Replace with proper random prediciton when it exists. - if (effect.Probability <= 1f) - { - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(target).Id, 0); - var rand = new System.Random(seed); - if (!rand.Prob(effect.Probability)) + if (effect.Probability <= 1f && !SharedRandomExtensions.PredictedProb(_timing, effect.Probability, GetNetEntity(target), GetNetEntity(user))) return false; - } // See if conditions apply if (!_condition.TryConditions(target, effect.Conditions)) diff --git a/Content.Shared/Flash/SharedFlashSystem.cs b/Content.Shared/Flash/SharedFlashSystem.cs index dad8e6d1562..4b8ff3ab7b7 100644 --- a/Content.Shared/Flash/SharedFlashSystem.cs +++ b/Content.Shared/Flash/SharedFlashSystem.cs @@ -205,10 +205,7 @@ public abstract class SharedFlashSystem : EntitySystem _entityLookup.GetEntitiesInRange(transform.Coordinates, range, _entSet); foreach (var entity in _entSet) { - // TODO: Use RandomPredicted https://github.com/space-wizards/RobustToolbox/pull/5849 - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(entity).Id); - var rand = new System.Random(seed); - if (!rand.Prob(probability)) + if (!SharedRandomExtensions.PredictedProb(_timing, probability, GetNetEntity(entity))) continue; // Is the entity affected by the flash either through status effects or by taking damage? diff --git a/Content.Shared/Fluids/EntitySystems/DrainSystem.cs b/Content.Shared/Fluids/EntitySystems/DrainSystem.cs index a6729b1bcd6..8b5fefdd8b5 100644 --- a/Content.Shared/Fluids/EntitySystems/DrainSystem.cs +++ b/Content.Shared/Fluids/EntitySystems/DrainSystem.cs @@ -246,10 +246,7 @@ public sealed class DrainSystem : EntitySystem if (args.Target == null) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, ent.Owner.GetHashCode()); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.UnclogProbability)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.UnclogProbability, GetNetEntity(ent))) { _popup.PopupPredicted(Loc.GetString("drain-component-unclog-fail", ("object", args.Target.Value)), args.Target.Value, args.User); return; diff --git a/Content.Shared/GhostTypes/GhostSpriteStateSystem.cs b/Content.Shared/GhostTypes/GhostSpriteStateSystem.cs index ef65387d5e9..d05eb5c72b8 100644 --- a/Content.Shared/GhostTypes/GhostSpriteStateSystem.cs +++ b/Content.Shared/GhostTypes/GhostSpriteStateSystem.cs @@ -48,9 +48,7 @@ public sealed class GhostSpriteStateSystem : EntitySystem var highestType = damageTypesSorted.First().Key; // We only need 1 of the values - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id); - var rand = new System.Random(seed); + var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(ent)); ProtoId? spriteState = null; diff --git a/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs b/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs index 05d8f92f556..fed55ddc293 100644 --- a/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs +++ b/Content.Shared/Kitchen/SharedKitchenSpikeSystem.cs @@ -290,10 +290,7 @@ public sealed class SharedKitchenSpikeSystem : EntitySystem PopupType.MediumCaution); // Get a random entry to spawn. - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_gameTiming.CurTick.Value, GetNetEntity(ent).Id); - var rand = new System.Random(seed); - + var rand = SharedRandomExtensions.PredictedRandom(_gameTiming, GetNetEntity(ent)); var index = rand.Next(butcherable.SpawnedEntities.Count); var entry = butcherable.SpawnedEntities[index]; diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs index f9cb03cb357..8446cf5d0f5 100644 --- a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.Utensils.cs @@ -65,11 +65,7 @@ public sealed partial class IngestionSystem if (!Resolve(entity, ref entity.Comp)) return; - // TODO: Once we have predicted randomness delete this for something sane... - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(entity).Id, GetNetEntity(userUid).Id); - var rand = new System.Random(seed); - - if (!rand.Prob(entity.Comp.BreakChance)) + if (!SharedRandomExtensions.PredictedProb(_timing, entity.Comp.BreakChance, GetNetEntity(entity), GetNetEntity(userUid))) return; _audio.PlayPredicted(entity.Comp.BreakSound, userUid, userUid, AudioParams.Default.WithVolume(-2f)); diff --git a/Content.Shared/Nutrition/EntitySystems/MessyDrinkerSystem.cs b/Content.Shared/Nutrition/EntitySystems/MessyDrinkerSystem.cs index f672edaab4e..7db62fb5956 100644 --- a/Content.Shared/Nutrition/EntitySystems/MessyDrinkerSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/MessyDrinkerSystem.cs @@ -37,10 +37,7 @@ public sealed class MessyDrinkerSystem : EntitySystem if (proto == null || !ent.Comp.SpillableTypes.Contains(proto.Value)) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.SpillChance)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.SpillChance, GetNetEntity(ent))) return; if (ent.Comp.SpillMessagePopup != null) diff --git a/Content.Shared/Random/Helpers/SharedRandomExtensions.cs b/Content.Shared/Random/Helpers/SharedRandomExtensions.cs index ceb4786ebef..981f16f3e54 100644 --- a/Content.Shared/Random/Helpers/SharedRandomExtensions.cs +++ b/Content.Shared/Random/Helpers/SharedRandomExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using Content.Shared.Dataset; using Content.Shared.FixedPoint; using Robust.Shared.Random; +using Robust.Shared.Timing; namespace Content.Shared.Random.Helpers { @@ -210,5 +211,34 @@ namespace Content.Shared.Random.Helpers } return hash; } + + // TODO: REPLACE ALL OF THIS WITH PREDICTED RANDOM WHEN ENGINE PR IS MERGED + /// + /// Creates an instance of System.Random that will be the same for both the server and client. + /// This allows for the client and server to roll the same results when determining things randomly, preventing mispredictions. + /// We generate a unique seed by getting 2-3 unique but predictable integers into a Hashcode. + /// + /// An instance if IGameTiming. + /// We use the integer value of the current tick to ensure a different seed every tick. + /// The relevant net entity to our seed. + /// This allows different entities to have different seeds and therefore different results on the same game-tick. + /// An optional relevant net entity to our seed. + /// Typically used if we have an entity checking random potentially multiple times per tick, to ensure we get a unique seed each time. + /// This entity should not be the same entity as . + public static System.Random PredictedRandom(IGameTiming timing, NetEntity netEnt, NetEntity? netEnt2 = null) + { + var seed = HashCodeCombine((int)timing.CurTick.Value, netEnt.Id, netEnt2?.Id ?? 0); + return new System.Random(seed); + } + + /// + /// Checks a probability against a instance. + /// Returns true if the amount rolled is below the probability. + /// + public static bool PredictedProb(IGameTiming timing, float probability, NetEntity netEnt1, NetEntity? netEnt2 = null) + { + var rand = PredictedRandom(timing, netEnt1, netEnt2); + return rand.Prob(probability); + } } } diff --git a/Content.Shared/Throwing/CatchableSystem.cs b/Content.Shared/Throwing/CatchableSystem.cs index 92ca7062b12..244522f192f 100644 --- a/Content.Shared/Throwing/CatchableSystem.cs +++ b/Content.Shared/Throwing/CatchableSystem.cs @@ -55,10 +55,7 @@ public sealed partial class CatchableSystem : EntitySystem if (attemptEv.Cancelled) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(ent).Id); - var rand = new System.Random(seed); - if (!rand.Prob(ent.Comp.CatchChance)) + if (!SharedRandomExtensions.PredictedProb(_timing, ent.Comp.CatchChance, GetNetEntity(ent))) return; // Try to catch! diff --git a/Content.Shared/Traits/Assorted/NarcolepsySystem.cs b/Content.Shared/Traits/Assorted/NarcolepsySystem.cs index f59a1cf2813..bdc055c6799 100644 --- a/Content.Shared/Traits/Assorted/NarcolepsySystem.cs +++ b/Content.Shared/Traits/Assorted/NarcolepsySystem.cs @@ -52,9 +52,7 @@ public sealed class NarcolepsySystem : EntitySystem if (narcolepsy.NextIncidentTime > _timing.CurTime) continue; - // TODO: Replace with RandomPredicted once the engine PR is merged - var seed = SharedRandomExtensions.HashCodeCombine((int)_timing.CurTick.Value, GetNetEntity(uid).Id); - var rand = new System.Random(seed); + var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(uid)); var duration = narcolepsy.MinDurationOfIncident + (narcolepsy.MaxDurationOfIncident - narcolepsy.MinDurationOfIncident) * rand.NextDouble(); diff --git a/Content.Shared/Trigger/Systems/RandomTriggerOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/RandomTriggerOnTriggerSystem.cs index 75acc005d00..5bd58e14c26 100644 --- a/Content.Shared/Trigger/Systems/RandomTriggerOnTriggerSystem.cs +++ b/Content.Shared/Trigger/Systems/RandomTriggerOnTriggerSystem.cs @@ -13,16 +13,7 @@ public sealed class RandomTriggerOnTriggerSystem : XOnTriggerSystem ent, EntityUid target, ref TriggerEvent args) { - // TODO: Replace with RandomPredicted once the engine PR is merged - var hash = new List - { - (int)_timing.CurTick.Value, - GetNetEntity(ent).Id, - args.User == null ? 0 : GetNetEntity(args.User.Value).Id, - }; - var seed = SharedRandomExtensions.HashCodeCombine(hash); - var rand = new System.Random(seed); - + var rand = SharedRandomExtensions.PredictedRandom(_timing, GetNetEntity(ent), GetNetEntity(args.User)); var keyOut = _prototypeManager.Index(ent.Comp.RandomKeyOut).Pick(rand); // Prevent recursive triggers diff --git a/Content.Shared/Trigger/Systems/TriggerSystem.Condition.cs b/Content.Shared/Trigger/Systems/TriggerSystem.Condition.cs index b039c8e9dec..36e00492209 100644 --- a/Content.Shared/Trigger/Systems/TriggerSystem.Condition.cs +++ b/Content.Shared/Trigger/Systems/TriggerSystem.Condition.cs @@ -70,17 +70,8 @@ public sealed partial class TriggerSystem if (args.Key != null && !ent.Comp.Keys.Contains(args.Key)) return; - // TODO: Replace with RandomPredicted once the engine PR is merged - var hash = new List - { - (int)_timing.CurTick.Value, - GetNetEntity(ent).Id, - args.User == null ? 0 : GetNetEntity(args.User.Value).Id, - }; - var seed = SharedRandomExtensions.HashCodeCombine(hash); - var rand = new System.Random(seed); - - args.Cancelled |= !rand.Prob(ent.Comp.SuccessChance); // When not successful, Cancelled = true + // When not successful, Cancelled = true + args.Cancelled |= !SharedRandomExtensions.PredictedProb(_timing, ent.Comp.SuccessChance, GetNetEntity(ent), GetNetEntity(args.User)); } private void OnMindRoleTriggerAttempt(Entity ent, ref AttemptTriggerEvent args) {