PredictedRandom Helpers (#42797)

* PredictedRandom Helpers

* fixxxx

* documentation!!!

* pram

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
This commit is contained in:
Princess Cheeseballs
2026-02-04 18:20:38 -08:00
committed by GitHub
parent c0f691a142
commit 41f042b8f9
16 changed files with 56 additions and 88 deletions

View File

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

View File

@@ -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<CursedMaskComponent> 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<CursedMaskExpression>());
_appearance.SetData(ent, CursedMaskVisuals.State, ent.Comp.CurrentState);
_movementSpeedModifier.RefreshMovementSpeedModifiers(wearer);

View File

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

View File

@@ -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<DiceComponent> 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);

View File

@@ -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))

View File

@@ -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?

View File

@@ -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;

View File

@@ -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<DamageTypePrototype>? spriteState = null;

View File

@@ -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];

View File

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

View File

@@ -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)

View File

@@ -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
/// <summary>
/// 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.
/// </summary>
/// <param name="timing">An instance if IGameTiming.
/// We use the integer value of the current tick to ensure a different seed every tick.</param>
/// <param name="netEnt">The relevant net entity to our seed.
/// This allows different entities to have different seeds and therefore different results on the same game-tick.</param>
/// <param name="netEnt2">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 <see cref="netEnt"/>.</param>
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);
}
/// <summary>
/// Checks a probability against a <see cref="PredictedRandom"/> instance.
/// Returns true if the amount rolled is below the probability.
/// </summary>
public static bool PredictedProb(IGameTiming timing, float probability, NetEntity netEnt1, NetEntity? netEnt2 = null)
{
var rand = PredictedRandom(timing, netEnt1, netEnt2);
return rand.Prob(probability);
}
}
}

View File

@@ -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!

View File

@@ -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();

View File

@@ -13,16 +13,7 @@ public sealed class RandomTriggerOnTriggerSystem : XOnTriggerSystem<RandomTrigge
protected override void OnTrigger(Entity<RandomTriggerOnTriggerComponent> ent, EntityUid target, ref TriggerEvent args)
{
// TODO: Replace with RandomPredicted once the engine PR is merged
var hash = new List<int>
{
(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

View File

@@ -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>
{
(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<MindRoleTriggerConditionComponent> ent, ref AttemptTriggerEvent args)
{