using Barotrauma.Extensions; using Barotrauma.Lights; using Barotrauma.Networking; using FarseerPhysics; using FarseerPhysics.Dynamics; using FarseerPhysics.Dynamics.Contacts; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; namespace Barotrauma { partial class Structure : MapEntity, IDamageable, IServerSerializable { public static bool ShowWalls = true, ShowStructures = true; private List convexHulls; private readonly Dictionary spriteAnimState = new Dictionary(); public readonly List Lights = new List(); public override bool SelectableInEditor { get { if (GameMain.SubEditorScreen.IsSubcategoryHidden(Prefab.Subcategory)) { return false; } if (!SubEditorScreen.IsLayerVisible(this)) { return false; } return HasBody ? ShowWalls : ShowStructures; } } partial void InitProjSpecific() { Prefab.Sprite?.EnsureLazyLoaded(); Prefab.BackgroundSprite?.EnsureLazyLoaded(); foreach (var decorativeSprite in Prefab.DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); } UpdateSpriteStates(0.0f); } public static Vector2 UpgradeTextureOffset( Vector2 targetSize, Vector2 originalTextureOffset, SubmarineInfo submarineInfo, Rectangle sourceRect, Vector2 scale, bool flippedX, bool flippedY) { if (submarineInfo.GameVersion <= Sprite.LastBrokenTiledSpriteGameVersion) { // Tiled sprite rendering was significantly changed after v1.2.3.0: // Rendering flipped, scaled and offset textures was completely broken, // but some existing community submarines depend on that old behavior, // so let's redo some of the broken logic here if the sub is old enough Vector2 flipper = (flippedX ? -1f : 1f, flippedY ? -1f : 1f); var textureOffset = originalTextureOffset * flipper; textureOffset = new Vector2( MathUtils.PositiveModulo((int)-textureOffset.X, sourceRect.Width), MathUtils.PositiveModulo((int)-textureOffset.Y, sourceRect.Height)); textureOffset.X = (textureOffset.X / scale.X) % sourceRect.Width; textureOffset.Y = (textureOffset.Y / scale.Y) % sourceRect.Height; Vector2 flippedDrawOffset = Vector2.Zero; if (flippedX) { float diff = targetSize.X % (sourceRect.Width * scale.X); flippedDrawOffset.X = (sourceRect.Width * scale.X - diff) / scale.X; flippedDrawOffset.X = MathUtils.NearlyEqual(flippedDrawOffset.X, MathF.Round(flippedDrawOffset.X)) ? MathF.Round(flippedDrawOffset.X) : flippedDrawOffset.X; } if (flippedY) { float diff = targetSize.Y % (sourceRect.Height * scale.Y); flippedDrawOffset.Y = (sourceRect.Height * scale.Y - diff) / scale.Y; flippedDrawOffset.Y = MathUtils.NearlyEqual(flippedDrawOffset.Y, MathF.Round(flippedDrawOffset.Y)) ? MathF.Round(flippedDrawOffset.Y) : flippedDrawOffset.Y; } var textureOffsetPlusFlipBs = textureOffset + flippedDrawOffset; if (textureOffsetPlusFlipBs.X > sourceRect.Width) { var diff = textureOffsetPlusFlipBs.X - sourceRect.Width; textureOffset.X = (textureOffset.X + diff * (scale.X - 1f)) % sourceRect.Width; } if (textureOffsetPlusFlipBs.Y > sourceRect.Height) { var diff = textureOffsetPlusFlipBs.Y - sourceRect.Height; textureOffset.Y = (textureOffset.Y + diff * (scale.Y - 1f)) % sourceRect.Height; } textureOffset *= scale * flipper; return -textureOffset; } return originalTextureOffset; } partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation) { if (!CastShadow) { return; } convexHulls ??= new List(); //if the convex hull is longer than this, we need to split it to multiple parts //very large convex hulls don't play nicely with the lighting or LOS, because the shadow cast //by the convex hull would need to be extruded very far to cover the whole screen const float MaxConvexHullLength = 1024.0f; float length = IsHorizontal ? size.X : size.Y; int convexHullCount = (int)Math.Max(1, Math.Ceiling(length / MaxConvexHullLength)); Vector2 sectionSize = size; if (convexHullCount > 1) { if (IsHorizontal) { sectionSize.X = length / convexHullCount; } else { sectionSize.Y = length / convexHullCount; } } for (int i = 0; i < convexHullCount; i++) { Vector2 offset = (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * (i * length / convexHullCount); var h = new ConvexHull( new Rectangle((position - size / 2 + offset).ToPoint(), sectionSize.ToPoint()), IsHorizontal, this); if (Math.Abs(rotation) > 0.001f) { h.Rotate(position, rotation); } convexHulls.Add(h); } } public override void UpdateEditing(Camera cam, float deltaTime) { if (editingHUD == null || editingHUD.UserData as Structure != this) { editingHUD = CreateEditingHUD(Screen.Selected != GameMain.SubEditorScreen); } } private void SetLightTextureOffset() { Vector2 textOffset = textureOffset; if (FlippedX) { textOffset.X = -textOffset.X; } if (FlippedY) { textOffset.Y = -textOffset.Y; } foreach (LightSource light in Lights) { Vector2 bgOffset = new Vector2( MathUtils.PositiveModulo(-textOffset.X, light.texture.Width), MathUtils.PositiveModulo(-textOffset.Y, light.texture.Height)); light.LightTextureOffset = bgOffset; } } public GUIComponent CreateEditingHUD(bool inGame = false) { int heightScaled = (int)(20 * GUI.Scale); editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.25f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) }) { UserData = this }; GUIListBox listBox = new GUIListBox(new RectTransform(new Vector2(0.95f, 0.8f), editingHUD.RectTransform, Anchor.Center), style: null) { CanTakeKeyBoardFocus = false }; var editor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont, dimOutDefaultValues: false) { UserData = this }; if (editor.Fields.TryGetValue(nameof(Scale).ToIdentifier(), out GUIComponent[] scaleFields) && scaleFields.FirstOrDefault() is GUINumberInput scaleInput) { //texture offset needs to be adjusted when scaling the entity to keep the look of the entity unchanged scaleInput.OnValueChanged += (GUINumberInput numberInput) => { TextureOffset *= (Scale / ScaleWhenTextureOffsetSet); }; } if (Submarine.MainSub?.Info?.Type == SubmarineType.OutpostModule) { GUITickBox tickBox = new GUITickBox(new RectTransform(new Point(listBox.Content.Rect.Width, 10)), TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.name")) { Font = GUIStyle.SmallFont, Selected = RemoveIfLinkedOutpostDoorInUse, ToolTip = TextManager.Get("sp.structure.removeiflinkedoutpostdoorinuse.description"), OnSelected = (tickBox) => { RemoveIfLinkedOutpostDoorInUse = tickBox.Selected; return true; } }; editor.AddCustomContent(tickBox, 1); } if (!Layer.IsNullOrEmpty()) { var layerText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)) { MinSize = new Point(0, heightScaled) }, TextManager.AddPunctuation(':', TextManager.Get("editor.layer"), Layer)); editor.AddCustomContent(layerText, 1); } var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.01f }; var mirrorX = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityXToolTip"), OnClicked = (button, data) => { foreach (MapEntity me in SelectedList) { me.FlipX(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } ColorFlipButton(button, FlippedX); return true; } }; ColorFlipButton(mirrorX, FlippedX); var mirrorY = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityY"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityYToolTip"), OnClicked = (button, data) => { foreach (MapEntity me in SelectedList) { me.FlipY(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } ColorFlipButton(button, FlippedY); return true; } }; ColorFlipButton(mirrorY, FlippedY); new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite"), style: "GUIButtonSmall") { OnClicked = (button, data) => { Sprite.ReloadXML(); Sprite.ReloadTexture(); return true; } }; new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ResetToPrefab"), style: "GUIButtonSmall") { OnClicked = (button, data) => { foreach (MapEntity me in SelectedList) { (me as Item)?.Reset(); (me as Structure)?.Reset(); } if (!SelectedList.Contains(this)) { Reset(); } CreateEditingHUD(); return true; } }; buttonContainer.RectTransform.Resize(new Point(buttonContainer.Rect.Width, buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y))); buttonContainer.RectTransform.IsFixedSize = true; GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(b => ((GUIButton)b).TextBlock)); editor.AddCustomContent(buttonContainer, editor.ContentCount); PositionEditingHUD(); return editingHUD; } partial void OnImpactProjSpecific(Fixture f1, Fixture f2, Contact contact) { if (!Prefab.Platform && Prefab.StairDirection == Direction.None) { Vector2 pos = ConvertUnits.ToDisplayUnits(f2.Body.Position); int section = FindSectionIndex(pos); if (section > -1) { Vector2 normal = contact.Manifold.LocalNormal; float impact = Vector2.Dot(f2.Body.LinearVelocity, -normal) * f2.Body.Mass * 0.1f; if (impact > 10.0f) { SoundPlayer.PlayDamageSound("StructureBlunt", impact, SectionPosition(section, true), tags: Tags); } } } } public override bool IsVisible(Rectangle worldView) { RectangleF worldRect = Quad2D.FromSubmarineRectangle(WorldRect).Rotated( FlippedX != FlippedY ? RotationRad : -RotationRad).BoundingAxisAlignedRectangle; Vector2 worldPos = WorldPosition; Vector2 min = new Vector2(worldRect.X, worldRect.Y); Vector2 max = new Vector2(worldRect.Right, worldRect.Y + worldRect.Height); foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) { Vector2 scale = decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; min.X = Math.Min(worldPos.X - decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale.X, min.X); max.X = Math.Max(worldPos.X + decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale.X, max.X); min.Y = Math.Min(worldPos.Y - decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale.Y, min.Y); max.Y = Math.Max(worldPos.Y + decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale.Y, max.Y); } Vector2 offset = GetCollapseEffectOffset(); min += offset; max += offset; if (min.X > worldView.Right || max.X < worldView.X) { return false; } if (min.Y > worldView.Y || max.Y < worldView.Y - worldView.Height) { return false; } Vector2 extents = max - min; if (extents.X * Screen.Selected.Cam.Zoom < 1.0f) { return false; } if (extents.Y * Screen.Selected.Cam.Zoom < 1.0f) { return false; } return true; } public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { if (Prefab.Sprite == null) { return; } if (editing) { if (!SubEditorScreen.IsLayerVisible(this)) { return; } if (!HasBody && !ShowStructures) { return; } if (HasBody && !ShowWalls) { return; } } Draw(spriteBatch, editing, back, null); } public void DrawDamage(SpriteBatch spriteBatch, Effect damageEffect, bool editing) { Draw(spriteBatch, editing, false, damageEffect); } private float GetRealDepth() { return SpriteDepthOverrideIsSet ? SpriteOverrideDepth : Prefab.Sprite.Depth; } public override float GetDrawDepth() { return GetDrawDepth(GetRealDepth(), Prefab.Sprite); } private void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Effect damageEffect = null) { if (Prefab.Sprite == null) { return; } if (editing) { if (!SubEditorScreen.IsLayerVisible(this)) { return; } if (!HasBody && !ShowStructures) { return; } if (HasBody && !ShowWalls) { return; } } else if (IsHidden) { return; } Color color = IsIncludedInSelection && editing ? GUIStyle.Blue : IsHighlighted ? GUIStyle.Orange * Math.Max(spriteColor.A / (float) byte.MaxValue, 0.1f) : spriteColor; if (IsSelected && editing) { //color = Color.Lerp(color, Color.Gold, 0.5f); color = spriteColor; Vector2 rectSize = rect.Size.ToVector2(); if (BodyWidth > 0.0f) { rectSize.X = BodyWidth; } if (BodyHeight > 0.0f) { rectSize.Y = BodyHeight; } Vector2 bodyPos = WorldPosition + BodyOffset * Scale; GUI.DrawRectangle(sb: spriteBatch, center: new Vector2(bodyPos.X, -bodyPos.Y), width: rectSize.X, height: rectSize.Y, rotation: BodyRotation, clr: Color.White, thickness: Math.Max(1, (int)(2 / Screen.Selected.Cam.Zoom))); } bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode(); if (isWiringMode) { color *= 0.15f; } Vector2 drawOffset = Submarine == null ? Vector2.Zero : Submarine.DrawPosition; drawOffset += GetCollapseEffectOffset(); float depth = GetDrawDepth(); Vector2 textureOffset = this.textureOffset; if (back && damageEffect == null && !isWiringMode) { if (Prefab.BackgroundSprite != null) { Vector2 dropShadowOffset = Vector2.Zero; if (UseDropShadow) { dropShadowOffset = DropShadowOffset; if (dropShadowOffset == Vector2.Zero) { if (Submarine == null) { dropShadowOffset = Vector2.UnitY * 10.0f; } else { dropShadowOffset = IsHorizontal ? new Vector2(0.0f, Math.Sign(Submarine.HiddenSubPosition.Y - Position.Y) * 10.0f) : new Vector2(Math.Sign(Submarine.HiddenSubPosition.X - Position.X) * 10.0f, 0.0f); } } dropShadowOffset.Y = -dropShadowOffset.Y; } Vector2 backGroundOffset = new Vector2( MathUtils.PositiveModulo(-textureOffset.X, Prefab.BackgroundSprite.SourceRect.Width * TextureScale.X * Scale), MathUtils.PositiveModulo(-textureOffset.Y, Prefab.BackgroundSprite.SourceRect.Height * TextureScale.Y * Scale)); float rotationRad = GetRotationForSprite(RotationRad, Prefab.BackgroundSprite); Prefab.BackgroundSprite.DrawTiled( spriteBatch, new Vector2(rect.X + rect.Width / 2 + drawOffset.X, -(rect.Y - rect.Height / 2 + drawOffset.Y)), new Vector2(rect.Width, rect.Height), rotation: rotationRad, origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: Prefab.BackgroundSpriteColor, textureScale: TextureScale * Scale, startOffset: backGroundOffset, depth: Math.Max(GetDrawDepth(Prefab.BackgroundSprite.Depth, Prefab.BackgroundSprite), depth + 0.000001f), spriteEffects: Prefab.BackgroundSprite.effects ^ SpriteEffects); if (UseDropShadow) { Prefab.BackgroundSprite.DrawTiled( spriteBatch, new Vector2(rect.X + rect.Width / 2 + drawOffset.X, -(rect.Y - rect.Height / 2 + drawOffset.Y)) + dropShadowOffset, new Vector2(rect.Width, rect.Height), rotation: rotationRad, origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: Color.Black * 0.5f, textureScale: TextureScale * Scale, startOffset: backGroundOffset, depth: (depth + Prefab.BackgroundSprite.Depth) / 2.0f, spriteEffects: Prefab.BackgroundSprite.effects ^ SpriteEffects); } } } if (back == GetRealDepth() > 0.5f) { Vector2 advanceX = MathUtils.RotatedUnitXRadians(RotationRad).FlipY(); Vector2 advanceY = advanceX.YX().FlipX(); if (FlippedX != FlippedY) { advanceX = advanceX.FlipY(); advanceY = advanceY.FlipX(); } float sectionSpriteRotationRad = GetRotationForSprite(RotationRad, Prefab.Sprite); for (int i = 0; i < Sections.Length; i++) { Rectangle drawSection = Sections[i].rect; if (damageEffect != null) { float newCutoff = MathHelper.Lerp(0.0f, 0.65f, Sections[i].damage / MaxHealth); //change the parameters of the damage effect and start a new sprite batch if the damage is different by 5% or more if (Math.Abs(newCutoff - Submarine.DamageEffectCutoff) > 0.01f || //if we were previously rendering some small amount of damage but now 0 damage, make sure we update the parameters //"no damage" vs "just a tiny fraction of damage" makes a difference, even though normally 5% differences in damage aren't noticeable MathUtils.NearlyEqual(newCutoff, 0.0f) != MathUtils.NearlyEqual(Submarine.DamageEffectCutoff, 0.0f)) { spriteBatch.End(); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, null, null, damageEffect, Screen.Selected.Cam.Transform); damageEffect.Parameters["aCutoff"].SetValue(newCutoff); damageEffect.Parameters["cCutoff"].SetValue(newCutoff * 1.2f); damageEffect.CurrentTechnique.Passes[0].Apply(); Submarine.DamageEffectCutoff = newCutoff; } } if (!HasDamage && i == 0) { drawSection = new Rectangle( drawSection.X, drawSection.Y, Sections[Sections.Length - 1].rect.Right - drawSection.X, drawSection.Y - (Sections[Sections.Length - 1].rect.Y - Sections[Sections.Length - 1].rect.Height)); i = Sections.Length; } Vector2 sectionOffset = new Vector2( Math.Abs(rect.Location.X - drawSection.Location.X), Math.Abs(rect.Location.Y - drawSection.Location.Y)); if (FlippedX && IsHorizontal) { sectionOffset.X = rect.Right - drawSection.Right; } if (FlippedY && !IsHorizontal) { sectionOffset.Y = (drawSection.Y - drawSection.Height) - (rect.Y - rect.Height); } sectionOffset.X += MathUtils.PositiveModulo(-textureOffset.X, Prefab.Sprite.SourceRect.Width * TextureScale.X * Scale); sectionOffset.Y += MathUtils.PositiveModulo(-textureOffset.Y, Prefab.Sprite.SourceRect.Height * TextureScale.Y * Scale); Vector2 pos = new Vector2(drawSection.X, drawSection.Y); pos -= rect.Location.ToVector2(); pos = advanceX * pos.X + advanceY * pos.Y; pos += rect.Location.ToVector2(); pos = new Vector2(pos.X + rect.Width / 2 + drawOffset.X, -(pos.Y - rect.Height / 2 + drawOffset.Y)); Prefab.Sprite.DrawTiled( spriteBatch, pos, new Vector2(drawSection.Width, drawSection.Height), rotation: sectionSpriteRotationRad, origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: color, startOffset: sectionOffset, depth: depth, textureScale: TextureScale * Scale, spriteEffects: Prefab.Sprite.effects ^ SpriteEffects); } foreach (var decorativeSprite in Prefab.DecorativeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor) + RotationRad; Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier) * Scale; if (FlippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (FlippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } Vector2 drawPos = DrawPosition + MathUtils.RotatePoint(offset, -this.RotationRad); decorativeSprite.Sprite.Draw( spriteBatch: spriteBatch, pos: drawPos.FlipY(), color: color, rotate: rotation, origin: decorativeSprite.Sprite.Origin, scale: decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffect: Prefab.Sprite.effects ^ SpriteEffects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - Prefab.Sprite.Depth), 0.999f)); } } static float GetRotationForSprite(float rotationRad, Sprite sprite) { // Use bitwise operations instead of HasFlag to avoid boxing, as this is performance-sensitive code. bool flipHorizontally = (sprite.effects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally; bool flipVertically = (sprite.effects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically; if (flipHorizontally != flipVertically) { rotationRad = -rotationRad; } return rotationRad; } if (GameMain.DebugDraw && Screen.Selected.Cam.Zoom > 0.5f) { if (Bodies != null) { foreach (var body in Bodies) { Vector2 pos = ConvertUnits.ToDisplayUnits(body.Position); if (Submarine != null) { pos += Submarine.DrawPosition; } pos.Y = -pos.Y; var dimensions = bodyDimensions[body]; GUI.DrawRectangle(spriteBatch, pos, ConvertUnits.ToDisplayUnits(dimensions.X), ConvertUnits.ToDisplayUnits(dimensions.Y), -body.Rotation, Color.White); } } if (SectionCount > 0 && HasBody) { for (int i = 0; i < SectionCount; i++) { if (GetSection(i).damage > 0) { var textPos = SectionPosition(i, true); if (Submarine != null) { textPos += (Submarine.DrawPosition - Submarine.Position); } textPos.Y = -textPos.Y; GUI.DrawString(spriteBatch, textPos, "Damage: " + (int)((GetSection(i).damage / MaxHealth) * 100f) + "%", Color.Yellow); } } } } } public void UpdateSpriteStates(float deltaTime) { if (Prefab.DecorativeSpriteGroups.Count == 0) { return; } DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); foreach (int spriteGroup in Prefab.DecorativeSpriteGroups.Keys) { for (int i = 0; i < Prefab.DecorativeSpriteGroups[spriteGroup].Length; i++) { var decorativeSprite = Prefab.DecorativeSpriteGroups[spriteGroup][i]; if (decorativeSprite == null) { continue; } if (spriteGroup > 0) { int activeSpriteIndex = ID % Prefab.DecorativeSpriteGroups[spriteGroup].Length; if (i != activeSpriteIndex) { spriteAnimState[decorativeSprite].IsActive = false; continue; } } //check if the sprite is active (whether it should be drawn or not) var spriteState = spriteAnimState[decorativeSprite]; spriteState.IsActive = true; foreach (PropertyConditional conditional in decorativeSprite.IsActiveConditionals) { if (!ConditionalMatches(conditional)) { spriteState.IsActive = false; break; } } if (!spriteState.IsActive) { continue; } //check if the sprite should be animated bool animate = true; foreach (PropertyConditional conditional in decorativeSprite.AnimationConditionals) { if (!ConditionalMatches(conditional)) { animate = false; break; } } if (!animate) { continue; } spriteState.OffsetState += deltaTime; spriteState.RotationState += deltaTime; } } } private bool ConditionalMatches(PropertyConditional conditional) { if (!string.IsNullOrEmpty(conditional.TargetItemComponent)) { return false; } else { if (!conditional.Matches(this)) { return false; } } return true; } public void ClientEventRead(IReadMessage msg, float sendingTime) { byte sectionCount = msg.ReadByte(); bool invalidMessage = false; if (sectionCount != Sections.Length) { invalidMessage = true; string errorMsg = $"Error while reading a network event for the structure \"{Name} ({ID})\". Section count does not match (server: {sectionCount} client: {Sections.Length})"; GameAnalyticsManager.AddErrorEventOnce("Structure.ClientRead:SectionCountMismatch", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } for (int i = 0; i < sectionCount; i++) { float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * MaxHealth; if (!invalidMessage && i < Sections.Length) { SetDamage(i, damage, isNetworkEvent: true); } } } } }