using Barotrauma.Extensions; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Barotrauma.Items.Components; namespace Barotrauma { partial class LevelTrigger { [Flags] public enum TriggererType { None = 0, Human = 1, Creature = 2, Character = Human | Creature, Submarine = 4, Item = 8, OtherTrigger = 16 } public enum TriggerForceMode { Force, //default, apply a force to the object over time Acceleration, //apply an acceleration to the object, ignoring it's mass Impulse, //apply an instant force, ignoring deltaTime LimitVelocity //clamp the velocity of the triggerer to some value } public Action OnTriggered; /// /// Effects applied to entities that are inside the trigger /// private readonly List statusEffects = new List(); public IEnumerable StatusEffects { get { return statusEffects; } } /// /// Attacks applied to entities that are inside the trigger /// private readonly List attacks = new List(); private readonly float cameraShake; private Vector2 unrotatedForce; private float forceFluctuationTimer, currentForceFluctuation = 1.0f; private readonly HashSet triggerers = new HashSet(); private readonly TriggererType triggeredBy; private readonly Identifier triggerSpeciesOrGroup; private readonly PropertyConditional.LogicalComparison conditionals; private readonly float randomTriggerInterval; private readonly float randomTriggerProbability; private float randomTriggerTimer; private float triggeredTimer; private readonly HashSet tags = new HashSet(); //other triggers have to have at least one of these tags to trigger this one private readonly HashSet allowedOtherTriggerTags = new HashSet(); /// /// How long the trigger stays in the triggered state after triggerers have left /// private readonly float stayTriggeredDelay; public LevelTrigger ParentTrigger; public Dictionary TriggererPosition { get; private set; } private Vector2 worldPosition; public Vector2 WorldPosition { get { return worldPosition; } set { worldPosition = value; PhysicsBody?.SetTransform(ConvertUnits.ToSimUnits(value), PhysicsBody.Rotation); } } public float Rotation { get { return PhysicsBody == null ? 0.0f : PhysicsBody.Rotation; } set { if (PhysicsBody == null) return; PhysicsBody.SetTransform(PhysicsBody.Position, value); CalculateDirectionalForce(); } } public PhysicsBody PhysicsBody { get; private set; } public float TriggerOthersDistance { get; private set; } public IEnumerable Triggerers { get { return triggerers.AsEnumerable(); } } public bool IsTriggered { get { return (triggerers.Count > 0 || triggeredTimer > 0.0f) && (ParentTrigger == null || ParentTrigger.IsTriggered); } } public Vector2 Force { get; private set; } /// /// does the force diminish by distance /// public bool ForceFalloff { get; private set; } public float ForceFluctuationInterval { get; private set; } public float ForceFluctuationStrength { get; private set; } public float GlobalForceDecreaseInterval { get; private set; } private readonly TriggerForceMode forceMode; public TriggerForceMode ForceMode { get { return forceMode; } } /// /// Stop applying forces to objects if they're moving faster than this /// public float ForceVelocityLimit { get; private set; } public float ColliderRadius { get; private set; } public bool UseNetworkSyncing { get; private set; } public bool NeedsNetworkSyncing { get; set; } public Identifier InfectIdentifier { get; set; } public float InfectionChance { get; set; } private bool triggeredOnce; private readonly bool triggerOnce; public LevelTrigger(ContentXElement element, Vector2 position, float rotation, float scale = 1.0f, string parentDebugName = "") { TriggererPosition = new Dictionary(); worldPosition = position; if (element.Attributes("radius").Any() || element.Attributes("width").Any() || element.Attributes("height").Any()) { PhysicsBody = new PhysicsBody(element, scale) { CollisionCategories = Physics.CollisionLevel, CollidesWith = Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionProjectile | Physics.CollisionWall, }; PhysicsBody.FarseerBody.OnCollision += PhysicsBody_OnCollision; PhysicsBody.FarseerBody.OnSeparation += PhysicsBody_OnSeparation; PhysicsBody.FarseerBody.SetIsSensor(element.GetAttributeBool("sensor", true)); PhysicsBody.FarseerBody.BodyType = BodyType.Static; ColliderRadius = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f)); PhysicsBody.SetTransform(ConvertUnits.ToSimUnits(position), rotation); } cameraShake = element.GetAttributeFloat("camerashake", 0.0f); InfectIdentifier = element.GetAttributeIdentifier("infectidentifier", Identifier.Empty); InfectionChance = element.GetAttributeFloat("infectionchance", 0.05f); triggerOnce = element.GetAttributeBool("triggeronce", false); stayTriggeredDelay = element.GetAttributeFloat("staytriggereddelay", 0.0f); randomTriggerInterval = element.GetAttributeFloat("randomtriggerinterval", 0.0f); randomTriggerProbability = element.GetAttributeFloat("randomtriggerprobability", 0.0f); UseNetworkSyncing = element.GetAttributeBool("networksyncing", false); unrotatedForce = element.GetAttribute("force") != null && element.GetAttribute("force").Value.Contains(',') ? element.GetAttributeVector2("force", Vector2.Zero) : new Vector2(element.GetAttributeFloat("force", 0.0f), 0.0f); ForceFluctuationInterval = element.GetAttributeFloat("forcefluctuationinterval", 0.01f); ForceFluctuationStrength = Math.Max(element.GetAttributeFloat("forcefluctuationstrength", 0.0f), 0.0f); ForceFalloff = element.GetAttributeBool("forcefalloff", true); GlobalForceDecreaseInterval = element.GetAttributeFloat("globalforcedecreaseinterval", 0.0f); ForceVelocityLimit = ConvertUnits.ToSimUnits(element.GetAttributeFloat("forcevelocitylimit", float.MaxValue)); string forceModeStr = element.GetAttributeString("forcemode", "Force"); if (!Enum.TryParse(forceModeStr, out forceMode)) { DebugConsole.ThrowError("Error in LevelTrigger config: \"" + forceModeStr + "\" is not a valid force mode."); } CalculateDirectionalForce(); string triggeredByStr = element.GetAttributeString("triggeredby", "Character"); if (!Enum.TryParse(triggeredByStr, out triggeredBy)) { Identifier speciesOrGroup = triggeredByStr.ToIdentifier(); if (CharacterPrefab.Prefabs.Any(p => p.MatchesSpeciesNameOrGroup(speciesOrGroup))) { triggerSpeciesOrGroup = speciesOrGroup; triggeredBy = TriggererType.Character; } else { DebugConsole.ThrowError("Error in LevelTrigger config: \"" + triggeredByStr + "\" is not a valid triggerer type."); } } if (PhysicsBody != null) { PhysicsBody.CollidesWith = GetCollisionCategories(triggeredBy); } TriggerOthersDistance = element.GetAttributeFloat("triggerothersdistance", 0.0f); var tagsArray = element.GetAttributeStringArray("tags", Array.Empty()); foreach (string tag in tagsArray) { tags.Add(tag.ToLowerInvariant()); } if (triggeredBy.HasFlag(TriggererType.OtherTrigger)) { var otherTagsArray = element.GetAttributeStringArray("allowedothertriggertags", Array.Empty()); foreach (string tag in otherTagsArray) { allowedOtherTriggerTags.Add(tag.ToLowerInvariant()); } } string debugName = string.IsNullOrEmpty(parentDebugName) ? "LevelTrigger" : $"LevelTrigger in {parentDebugName}"; foreach (var subElement in element.Elements()) { switch (subElement.Name.ToString().ToLowerInvariant()) { case "statuseffect": LoadStatusEffect(statusEffects, subElement, debugName); break; case "attack": case "damage": LoadAttack(subElement, debugName, triggerOnce, attacks); break; } } conditionals = PropertyConditional.LoadConditionals(element); forceFluctuationTimer = Rand.Range(0.0f, ForceFluctuationInterval); randomTriggerTimer = Rand.Range(0.0f, randomTriggerInterval); } public static Category GetCollisionCategories(TriggererType triggeredBy) { var collidesWith = Physics.CollisionNone; if (triggeredBy.HasFlag(TriggererType.Human) || triggeredBy.HasFlag(TriggererType.Creature)) { collidesWith |= Physics.CollisionCharacter; } if (triggeredBy.HasFlag(TriggererType.Item)) { collidesWith |= Physics.CollisionItem | Physics.CollisionProjectile; } if (triggeredBy.HasFlag(TriggererType.Submarine)) { collidesWith |= Physics.CollisionWall; } return collidesWith; } private void CalculateDirectionalForce() { var ca = (float)Math.Cos(-Rotation); var sa = (float)Math.Sin(-Rotation); Force = new Vector2( ca * unrotatedForce.X + sa * unrotatedForce.Y, -sa * unrotatedForce.X + ca * unrotatedForce.Y); } public static void LoadStatusEffect(List statusEffects, ContentXElement element, string parentDebugName) { statusEffects.Add(StatusEffect.Load(element, parentDebugName)); } public static void LoadAttack(ContentXElement element, string parentDebugName, bool triggerOnce, List attacks) { var attack = new Attack(element, parentDebugName); if (!triggerOnce) { var multipliedAfflictions = attack.GetMultipliedAfflictions((float)Timing.Step); attack.Afflictions.Clear(); foreach (Affliction affliction in multipliedAfflictions) { attack.Afflictions.Add(affliction, null); } } attacks.Add(attack); } private bool PhysicsBody_OnCollision(Fixture fixtureA, Fixture fixtureB, Contact contact) { Entity entity = GetEntity(fixtureB); if (entity == null) { return false; } if (!IsTriggeredByEntity(entity, triggeredBy, triggerSpeciesOrGroup: triggerSpeciesOrGroup, conditionals: conditionals, mustBeOutside: true)) { return false; } if (!triggerers.Contains(entity)) { if (!IsTriggered) { OnTriggered?.Invoke(this, entity); } TriggererPosition[entity] = entity.WorldPosition; triggerers.Add(entity); } return true; } public static bool IsTriggeredByEntity( Entity entity, TriggererType triggeredBy, Identifier triggerSpeciesOrGroup, PropertyConditional.LogicalComparison conditionals, (bool mustBe, Submarine sub) mustBeOnSpecificSub = default, bool mustBeOutside = false) { if (entity is Character character) { if (mustBeOutside && character.CurrentHull != null) { return false; } if (mustBeOnSpecificSub.mustBe && character.Submarine != mustBeOnSpecificSub.sub) { return false; } if (!triggerSpeciesOrGroup.IsEmpty) { if (character.SpeciesName != triggerSpeciesOrGroup && character.Group != triggerSpeciesOrGroup) { return false; } } if (character.IsHuman) { if (!triggeredBy.HasFlag(TriggererType.Human)) { return false; } } else { if (!triggeredBy.HasFlag(TriggererType.Creature)) { return false; } } } else if (entity is Item item) { if (mustBeOutside && item.CurrentHull != null) { return false; } if (mustBeOnSpecificSub.mustBe && item.Submarine != mustBeOnSpecificSub.sub) { return false; } if (!triggeredBy.HasFlag(TriggererType.Item)) { return false; } } else if (entity is Submarine) { if (!triggeredBy.HasFlag(TriggererType.Submarine)) { return false; } } if (conditionals != null && entity is ISerializableEntity serializableEntity) { if (!PropertyConditional.CheckConditionals(serializableEntity, conditionals.Conditionals, conditionals.LogicalOperator)) { return false; } } return true; } private void PhysicsBody_OnSeparation(Fixture fixtureA, Fixture fixtureB, Contact contact) { Entity entity = GetEntity(fixtureB); if (entity == null) { return; } if (entity is Character character && (!character.Enabled || character.Removed) && triggerers.Contains(entity)) { TriggererPosition.Remove(entity); triggerers.Remove(entity); return; } if (CheckContactsForOtherFixtures(PhysicsBody, fixtureB, entity)) { return; } if (triggerers.Contains(entity)) { TriggererPosition.Remove(entity); triggerers.Remove(entity); } } /// /// Checks whether any fixture of the trigger body is in contact with any fixture belonging to the physics bodies of separatingEntity /// /// Physics body of the trigger /// Fixture that got separated from the trigger /// Entity that got separated from the trigger /// public static bool CheckContactsForOtherFixtures(PhysicsBody triggerBody, Fixture separatingFixture, Entity separatingEntity) { //check if there are contacts with any other fixture of the trigger //(the OnSeparation callback happens when two fixtures separate, //e.g. if a body stops touching the circular fixture at the end of a capsule-shaped body) foreach (Fixture triggerFixture in triggerBody.FarseerBody.FixtureList) { ContactEdge contactEdge = triggerFixture.Body.ContactList; while (contactEdge != null) { if (contactEdge.Contact != null && contactEdge.Contact.Enabled && contactEdge.Contact.IsTouching) { //which fixture of this contact belongs to the "other" body (not the trigger itself) Fixture otherFixture = contactEdge.Contact.FixtureA == triggerFixture ? contactEdge.Contact.FixtureB : contactEdge.Contact.FixtureA; if (otherFixture != separatingFixture) { var otherEntity = GetEntity(otherFixture); if (otherEntity == separatingEntity) { return true; } } } contactEdge = contactEdge.Next; } } return false; } /// /// Are there any active contacts between the physics body and the target entity /// public static bool CheckContactsForEntity(PhysicsBody triggerBody, Entity targetEntity) { foreach (Fixture fixture in triggerBody.FarseerBody.FixtureList) { ContactEdge contactEdge = fixture.Body.ContactList == null ? null: fixture.Body.ContactList.CreateCopy(); while (contactEdge != null) { if (contactEdge.Contact != null && contactEdge.Contact.Enabled && contactEdge.Contact.IsTouching) { if ((contactEdge.Contact.FixtureA.Body == triggerBody.FarseerBody && GetEntity(contactEdge.Contact.FixtureB) == targetEntity) || (contactEdge.Contact.FixtureB.Body == triggerBody.FarseerBody && GetEntity(contactEdge.Contact.FixtureA) == targetEntity)) { return true; } } contactEdge = contactEdge.Next; } } return false; } public static Entity GetEntity(Fixture fixture) { if (fixture.Body == null || fixture.Body.UserData == null) { return null; } if (fixture.Body.UserData is Entity entity) { return entity; } if (fixture.Body.UserData is Limb limb) { return limb.character; } if (fixture.Body.UserData is SubmarineBody subBody) { return subBody.Submarine; } return null; } /// /// Another trigger was triggered, check if this one should react to it /// public void OtherTriggered(LevelTrigger otherTrigger, Entity triggerer) { if (!triggeredBy.HasFlag(TriggererType.OtherTrigger) || stayTriggeredDelay <= 0.0f) { return; } //check if the other trigger has appropriate tags if (allowedOtherTriggerTags.Count > 0) { if (!allowedOtherTriggerTags.Any(t => otherTrigger.tags.Contains(t))) { return; } } if (Vector2.DistanceSquared(WorldPosition, otherTrigger.WorldPosition) <= otherTrigger.TriggerOthersDistance * otherTrigger.TriggerOthersDistance) { bool wasAlreadyTriggered = IsTriggered; triggeredTimer = stayTriggeredDelay; if (!wasAlreadyTriggered) { if (!IsTriggeredByEntity(triggerer, triggeredBy, triggerSpeciesOrGroup, conditionals, mustBeOutside: true)) { return; } if (!triggerers.Contains(triggerer)) { if (!IsTriggered) { OnTriggered?.Invoke(this, triggerer); } TriggererPosition[triggerer] = triggerer.WorldPosition; triggerers.Add(triggerer); } } } } private readonly List targets = new List(); public void Update(float deltaTime) { if (ParentTrigger != null && !ParentTrigger.IsTriggered) { return; } bool isNotClient = true; #if CLIENT isNotClient = GameMain.Client == null; #endif if (!UseNetworkSyncing || isNotClient) { if (GlobalForceDecreaseInterval > 0.0f && Level.Loaded?.LevelObjectManager != null && Level.Loaded.LevelObjectManager.GlobalForceDecreaseTimer % (GlobalForceDecreaseInterval * 2) < GlobalForceDecreaseInterval) { NeedsNetworkSyncing |= currentForceFluctuation > 0.0f; currentForceFluctuation = 0.0f; } else if (ForceFluctuationStrength > 0.0f) { //no need for force fluctuation (or network updates) if the trigger limits velocity and there are no triggerers if (forceMode != TriggerForceMode.LimitVelocity || triggerers.Any()) { forceFluctuationTimer += deltaTime; if (forceFluctuationTimer > ForceFluctuationInterval) { NeedsNetworkSyncing = true; currentForceFluctuation = Rand.Range(1.0f - ForceFluctuationStrength, 1.0f); forceFluctuationTimer = 0.0f; } } } if (randomTriggerProbability > 0.0f) { randomTriggerTimer += deltaTime; if (randomTriggerTimer > randomTriggerInterval) { if (Rand.Range(0.0f, 1.0f) < randomTriggerProbability) { NeedsNetworkSyncing = true; triggeredTimer = stayTriggeredDelay; } randomTriggerTimer = 0.0f; } } } RemoveInActiveTriggerers(PhysicsBody, triggerers); if (stayTriggeredDelay > 0.0f) { if (triggerers.Count == 0) { triggeredTimer -= deltaTime; } else { triggeredTimer = stayTriggeredDelay; } } if (triggerOnce && triggeredOnce) { return; } if (PhysicsBody != null) { if (currentForceFluctuation <= 0.0f && statusEffects.None() && attacks.None()) { //no force atm, and no status effects or attacks the trigger could apply // -> we can disable the collider and get a minor physics performance improvement PhysicsBody.Enabled = false; return; } else { PhysicsBody.Enabled = true; } } foreach (Entity triggerer in triggerers) { if (triggerer.Removed) { continue; } ApplyStatusEffects(statusEffects, worldPosition, triggerer, deltaTime, targets); if (triggerer is IDamageable damageable) { ApplyAttacks(attacks, damageable, worldPosition, deltaTime); } else if (triggerer is Submarine submarine) { ApplyAttacks(attacks, worldPosition, deltaTime); if (!InfectIdentifier.IsEmpty) { submarine.AttemptBallastFloraInfection(InfectIdentifier, deltaTime, InfectionChance); } } if (Force.LengthSquared() > 0.01f) { if (triggerer is Character character) { ApplyForce(character.AnimController.Collider); foreach (Limb limb in character.AnimController.Limbs) { if (limb.IsSevered) { continue; } ApplyForce(limb.body); } } else if (triggerer is Submarine submarine) { ApplyForce(submarine.SubBody.Body); } } if (triggerer == Character.Controlled || triggerer == Character.Controlled?.Submarine) { GameMain.GameScreen.Cam.Shake = Math.Max(GameMain.GameScreen.Cam.Shake, cameraShake); } } if (triggerOnce && triggerers.Count > 0) { PhysicsBody.Enabled = false; triggeredOnce = true; } } private static readonly List triggerersToRemove = new List(); public static void RemoveInActiveTriggerers(PhysicsBody physicsBody, HashSet triggerers) { if (physicsBody == null) { return; } triggerersToRemove.Clear(); foreach (var triggerer in triggerers) { if (triggerer.Removed) { triggerersToRemove.Add(triggerer); } else if (!CheckContactsForEntity(physicsBody, triggerer)) { triggerersToRemove.Add(triggerer); } } foreach (var triggerer in triggerersToRemove) { triggerers.Remove(triggerer); } } public static void ApplyStatusEffects(List statusEffects, Vector2 worldPosition, Entity triggerer, float deltaTime, List targets, Item targetItem = null) { foreach (StatusEffect effect in statusEffects) { if (effect.type == ActionType.OnBroken) { return; } Vector2? position = null; if (effect.HasTargetType(StatusEffect.TargetType.This)) { position = worldPosition; if (targetItem != null) { effect.Apply(effect.type, deltaTime, triggerer, targetItem.AllPropertyObjects, position); } } if (triggerer is Character character) { effect.Apply(effect.type, deltaTime, triggerer, character, position); if (effect.HasTargetType(StatusEffect.TargetType.Contained) && character.Inventory != null) { foreach (Item item in character.Inventory.AllItemsMod) { if (item.ContainedItems == null) { continue; } foreach (Item containedItem in item.ContainedItems) { effect.Apply(effect.type, deltaTime, triggerer, containedItem.AllPropertyObjects, position); } } } } else if (triggerer is Item item) { effect.Apply(effect.type, deltaTime, triggerer, item.AllPropertyObjects, position); } else if (triggerer is Submarine sub) { effect.Apply(effect.type, deltaTime, sub, Array.Empty(), position); } if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) { targets.Clear(); effect.AddNearbyTargets(worldPosition, targets); effect.Apply(effect.type, deltaTime, triggerer, targets); } } } /// /// Applies attacks to a damageable. /// public static void ApplyAttacks(List attacks, IDamageable damageable, Vector2 worldPosition, float deltaTime) { foreach (Attack attack in attacks) { attack.DoDamage(null, damageable, worldPosition, deltaTime, false); } } /// /// Applies attacks to structures. /// public static void ApplyAttacks(List attacks, Vector2 worldPosition, float deltaTime) { foreach (Attack attack in attacks) { float structureDamage = attack.GetStructureDamage(deltaTime); if (structureDamage > 0.0f) { Explosion.RangedStructureDamage(worldPosition, attack.DamageRange, structureDamage, levelWallDamage: 0.0f, emitWallDamageParticles: attack.EmitStructureDamageParticles); } } } private void ApplyForce(PhysicsBody body) { if (body == null) { return; } float distFactor = 1.0f; if (ForceFalloff) { distFactor = GetDistanceFactor(body, PhysicsBody, ColliderRadius); if (distFactor < 0.0f) return; } if (MathUtils.NearlyEqual(currentForceFluctuation, 0.0f)) { return; } switch (ForceMode) { case TriggerForceMode.Force: if (ForceVelocityLimit < 1000.0f) body.ApplyForce(Force * currentForceFluctuation * distFactor, ForceVelocityLimit); else body.ApplyForce(Force * currentForceFluctuation * distFactor); break; case TriggerForceMode.Acceleration: if (ForceVelocityLimit < 1000.0f) body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor, ForceVelocityLimit); else body.ApplyForce(Force * body.Mass * currentForceFluctuation * distFactor); break; case TriggerForceMode.Impulse: if (ForceVelocityLimit < 1000.0f) body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor, maxVelocity: ForceVelocityLimit); else body.ApplyLinearImpulse(Force * currentForceFluctuation * distFactor); break; case TriggerForceMode.LimitVelocity: float maxVel = ForceVelocityLimit * currentForceFluctuation * distFactor; if (body.LinearVelocity.LengthSquared() > maxVel * maxVel) { body.ApplyForce( Vector2.Normalize(-body.LinearVelocity) * Force.Length() * body.Mass * currentForceFluctuation * distFactor, maxVelocity: NetConfig.MaxPhysicsBodyVelocity); } break; } } public static float GetDistanceFactor(PhysicsBody triggererBody, PhysicsBody triggerBody, float colliderRadius) { return 1.0f - ConvertUnits.ToDisplayUnits(Vector2.Distance(triggererBody.SimPosition, triggerBody.SimPosition)) / colliderRadius; } public Vector2 GetWaterFlowVelocity(Vector2 viewPosition) { Vector2 baseVel = GetWaterFlowVelocity(); if (baseVel.LengthSquared() < 0.1f) return Vector2.Zero; float triggerSize = ConvertUnits.ToDisplayUnits(Math.Max(Math.Max(PhysicsBody.Radius, PhysicsBody.Width / 2.0f), PhysicsBody.Height / 2.0f)); float dist = Vector2.Distance(viewPosition, WorldPosition); if (dist > triggerSize) return Vector2.Zero; return baseVel * (1.0f - dist / triggerSize); } public Vector2 GetWaterFlowVelocity() { if (Force == Vector2.Zero || ForceMode == TriggerForceMode.LimitVelocity) { return Vector2.Zero; } Vector2 vel = Force; if (ForceMode == TriggerForceMode.Acceleration) { vel *= 1000.0f; } else if (ForceMode == TriggerForceMode.Impulse) { vel /= (float)Timing.Step; } return vel.ClampLength(ConvertUnits.ToDisplayUnits(ForceVelocityLimit)) * currentForceFluctuation; } public void ServerWrite(IWriteMessage msg, Client c) { if (ForceFluctuationStrength > 0.0f) { msg.WriteRangedSingle(MathHelper.Clamp(currentForceFluctuation, 0.0f, 1.0f), 0.0f, 1.0f, 8); } if (stayTriggeredDelay > 0.0f) { msg.WriteRangedSingle(MathHelper.Clamp(triggeredTimer, 0.0f, stayTriggeredDelay), 0.0f, stayTriggeredDelay, 16); } } } }