using Barotrauma.Extensions; using Barotrauma.Items.Components; using Barotrauma.MapCreatures.Behavior; using Barotrauma.Networking; using FarseerPhysics; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text; namespace Barotrauma { partial class Item : MapEntity, IDamageable, ISerializableEntity, IServerSerializable, IClientSerializable { public static bool ShowItems = true, ShowWires = true; private readonly List positionBuffer = new List(); private readonly List activeHUDs = new List(); private readonly List activeEditors = new List(); private GUIComponentStyle iconStyle; public GUIComponentStyle IconStyle { get { return iconStyle; } private set { if (IconStyle != value) { iconStyle = value; CheckIsHighlighted(); } } } partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable targetClients) { if (interactionType == CampaignMode.InteractionType.None) { IconStyle = null; } else { IconStyle = GUIStyle.GetComponentStyle($"CampaignInteractionIcon.{interactionType}"); } } public IEnumerable ActiveHUDs => activeHUDs; public float LastImpactSoundTime; public const float ImpactSoundInterval = 0.2f; private float editingHUDRefreshTimer; private ContainedItemSprite activeContainedSprite; private readonly Dictionary spriteAnimState = new Dictionary(); public float DrawDepthOffset; private bool fakeBroken; public bool FakeBroken { get { return fakeBroken; } set { if (value != fakeBroken) { fakeBroken = value; SetActiveSprite(); } } } private Sprite activeSprite; public override Sprite Sprite { get { return activeSprite; } } public override Rectangle Rect { get { return base.Rect; } set { cachedVisibleExtents = null; base.Rect = value; } } public override bool DrawBelowWater => (!(Screen.Selected is SubEditorScreen editor) || !editor.WiringMode || !isWire || !isLogic) && (base.DrawBelowWater || ParentInventory is CharacterInventory); public override bool DrawOverWater => base.DrawOverWater || (IsSelected || Screen.Selected is SubEditorScreen editor && editor.WiringMode) && (isWire || isLogic); private GUITextBlock itemInUseWarning; private GUITextBlock ItemInUseWarning { get { if (itemInUseWarning == null) { itemInUseWarning = new GUITextBlock(new RectTransform(new Point(10), GUI.Canvas), "", textColor: GUIStyle.Orange, color: Color.Black, textAlignment: Alignment.Center, style: "OuterGlow"); } return itemInUseWarning; } } public override bool SelectableInEditor { get { if (GameMain.SubEditorScreen.IsSubcategoryHidden(Prefab.Subcategory)) { return false; } if (!SubEditorScreen.IsLayerVisible(this)) { return false;} return parentInventory == null && (body == null || body.Enabled) && ShowItems; } } public override float GetDrawDepth() { return GetDrawDepth(SpriteDepth + DrawDepthOffset, Sprite); } public Color GetSpriteColor(Color? defaultColor = null, bool withHighlight = false) { Color color = defaultColor ?? spriteColor; if (Prefab.UseContainedSpriteColor && ownInventory != null) { foreach (Item item in ContainedItems) { color = item.ContainerColor; break; } } if (withHighlight) { if (IsHighlighted && !GUI.DisableItemHighlights && Screen.Selected != GameMain.GameScreen) { color = GUIStyle.Orange * Math.Max(GetSpriteColor().A / (float)byte.MaxValue, 0.1f); } else if (IsHighlighted && HighlightColor.HasValue) { color = Color.Lerp(color, HighlightColor.Value, (MathF.Sin((float)Timing.TotalTime * 3.0f) + 1.0f) / 2.0f); } } return color; } protected override void CheckIsHighlighted() { if (IsHighlighted || ExternalHighlight || IconStyle != null) { highlightedEntities.Add(this); } else { highlightedEntities.Remove(this); } } public Color GetInventoryIconColor() { Color color = InventoryIconColor; if (Prefab.UseContainedInventoryIconColor && ownInventory != null) { foreach (Item item in ContainedItems) { color = item.ContainerColor; break; } } return color; } partial void SetActiveSpriteProjSpecific() { activeSprite = Prefab.Sprite; activeContainedSprite = null; Holdable holdable = GetComponent(); if (holdable != null && holdable.Attached) { foreach (ContainedItemSprite containedSprite in Prefab.ContainedSprites) { if (containedSprite.UseWhenAttached) { activeContainedSprite = containedSprite; activeSprite = containedSprite.Sprite; UpdateSpriteStates(0.0f); return; } } } if (Container != null) { foreach (ContainedItemSprite containedSprite in Prefab.ContainedSprites) { if (containedSprite.MatchesContainer(Container)) { activeContainedSprite = containedSprite; activeSprite = containedSprite.Sprite; UpdateSpriteStates(0.0f); return; } } } float displayCondition = FakeBroken ? 0.0f : ConditionPercentageRelativeToDefaultMaxCondition; for (int i = 0; i < Prefab.BrokenSprites.Length;i++) { if (Prefab.BrokenSprites[i].FadeIn) { continue; } float minCondition = i > 0 ? Prefab.BrokenSprites[i - i].MaxConditionPercentage : 0.0f; if (displayCondition <= minCondition || displayCondition <= Prefab.BrokenSprites[i].MaxConditionPercentage) { activeSprite = Prefab.BrokenSprites[i].Sprite; break; } } } public void InitSpriteStates() { Prefab.Sprite?.EnsureLazyLoaded(); Prefab.InventoryIcon?.EnsureLazyLoaded(); foreach (BrokenItemSprite brokenSprite in Prefab.BrokenSprites) { brokenSprite.Sprite.EnsureLazyLoaded(); } foreach (var decorativeSprite in Prefab.DecorativeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); } SetActiveSprite(); UpdateSpriteStates(0.0f); } partial void InitProjSpecific() { InitSpriteStates(); } private Rectangle? cachedVisibleExtents; public void ResetCachedVisibleSize() { cachedVisibleExtents = null; } public override bool IsVisible(Rectangle worldView) { // Inside of a container if (container != null) { return false; } //no drawable components and the body has been disabled = nothing to draw if (!hasComponentsToDraw && body != null && !body.Enabled) { return false; } if (parentInventory?.Owner is Character character && character.InvisibleTimer > 0.0f) { return false; } Rectangle extents; if (cachedVisibleExtents.HasValue) { extents = cachedVisibleExtents.Value; } else { int padding = 0; RectangleF boundingBox = GetTransformedQuad().BoundingAxisAlignedRectangle; Vector2 min = new Vector2(-boundingBox.Width / 2 - padding, -boundingBox.Height / 2 - padding); Vector2 max = -min; foreach (IDrawableComponent drawable in drawableComponents) { min.X = Math.Min(min.X, -drawable.DrawSize.X / 2); min.Y = Math.Min(min.Y, -drawable.DrawSize.Y / 2); max.X = Math.Max(max.X, drawable.DrawSize.X / 2); max.Y = Math.Max(max.Y, drawable.DrawSize.Y / 2); } foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) { Vector2 scale = decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale; min.X = Math.Min(-decorativeSprite.Sprite.size.X * decorativeSprite.Sprite.RelativeOrigin.X * scale.X, min.X); min.Y = Math.Min(-decorativeSprite.Sprite.size.Y * (1.0f - decorativeSprite.Sprite.RelativeOrigin.Y) * scale.Y, min.Y); max.X = Math.Max(decorativeSprite.Sprite.size.X * (1.0f - decorativeSprite.Sprite.RelativeOrigin.X) * scale.X, max.X); max.Y = Math.Max(decorativeSprite.Sprite.size.Y * decorativeSprite.Sprite.RelativeOrigin.Y * scale.Y, max.Y); } cachedVisibleExtents = extents = new Rectangle(min.ToPoint(), max.ToPoint()); } Vector2 worldPosition = WorldPosition + GetCollapseEffectOffset(); if (worldPosition.X + extents.X > worldView.Right || worldPosition.X + extents.Width < worldView.X) { return false; } if (worldPosition.Y + extents.Height < worldView.Y - worldView.Height || worldPosition.Y + extents.Y > worldView.Y) { return false; } if (extents.Width * Screen.Selected.Cam.Zoom < 1.0f) { return false; } if (extents.Height * Screen.Selected.Cam.Zoom < 1.0f) { return false; } return true; } public override void Draw(SpriteBatch spriteBatch, bool editing, bool back = true) { Draw(spriteBatch, editing, back, overrideColor: null); } public void Draw(SpriteBatch spriteBatch, bool editing, bool back = true, Color? overrideColor = null) { if (!Visible || (!editing && IsHidden) || !SubEditorScreen.IsLayerVisible(this)) { return; } if (editing) { if (isWire) { if (!ShowWires) { return; } } else if (!ShowItems) { return; } } Color color = GetSpriteColor(spriteColor); bool isWiringMode = editing && SubEditorScreen.TransparentWiringMode && SubEditorScreen.IsWiringMode() && !isWire && parentInventory == null; bool renderTransparent = isWiringMode && GetComponent() == null; if (renderTransparent) { color *= 0.15f; } if (Character.Controlled != null && Character.DebugDrawInteract) { color = Color.Red; foreach (var ic in components) { var interactionType = GetComponentInteractionVisibility(Character.Controlled, ic); if (interactionType == InteractionVisibility.MissingRequirement) { color = Color.Orange; } else if (interactionType == InteractionVisibility.Visible) { color = Color.LightGreen; break; } } } BrokenItemSprite fadeInBrokenSprite = null; float fadeInBrokenSpriteAlpha = 0.0f; float displayCondition = FakeBroken ? 0.0f : ConditionPercentageRelativeToDefaultMaxCondition; Vector2 drawOffset = GetCollapseEffectOffset(); drawOffset.Y = -drawOffset.Y; if (displayCondition < MaxCondition) { for (int i = 0; i < Prefab.BrokenSprites.Length; i++) { if (Prefab.BrokenSprites[i].FadeIn) { float min = i > 0 ? Prefab.BrokenSprites[i - i].MaxConditionPercentage : 0.0f; float max = Prefab.BrokenSprites[i].MaxConditionPercentage; fadeInBrokenSpriteAlpha = 1.0f - ((displayCondition - min) / (max - min)); if (fadeInBrokenSpriteAlpha > 0.0f && fadeInBrokenSpriteAlpha <= 1.0f) { fadeInBrokenSprite = Prefab.BrokenSprites[i]; } continue; } if (displayCondition <= Prefab.BrokenSprites[i].MaxConditionPercentage) { activeSprite = Prefab.BrokenSprites[i].Sprite; drawOffset = Prefab.BrokenSprites[i].Offset.ToVector2() * Scale; break; } } } float depth = GetDrawDepth(); if (isWiringMode && isLogic && !PlayerInput.IsShiftDown()) { depth = 0.01f; } if (activeSprite != null) { SpriteEffects oldEffects = activeSprite.effects; activeSprite.effects ^= SpriteEffects; SpriteEffects oldBrokenSpriteEffects = SpriteEffects.None; if (fadeInBrokenSprite != null && fadeInBrokenSprite.Sprite != activeSprite) { oldBrokenSpriteEffects = fadeInBrokenSprite.Sprite.effects; fadeInBrokenSprite.Sprite.effects ^= SpriteEffects; } if (body == null || body.BodyType == BodyType.Static) { if (Prefab.ResizeHorizontal || Prefab.ResizeVertical) { if (color.A > 0) { Vector2 size = new Vector2(rect.Width, rect.Height); activeSprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + drawOffset, size, color: color, textureScale: Vector2.One * Scale, depth: depth); if (fadeInBrokenSprite != null) { float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); fadeInBrokenSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)) + fadeInBrokenSprite.Offset.ToVector2() * Scale, size, color: color * fadeInBrokenSpriteAlpha, textureScale: Vector2.One * Scale, depth: d); } DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, rotation: 0, depth, overrideColor); } } else { Vector2 origin = GetSpriteOrigin(activeSprite); if (color.A > 0) { activeSprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, origin, RotationRad, Scale, activeSprite.effects, depth); if (fadeInBrokenSprite != null) { float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); fadeInBrokenSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + fadeInBrokenSprite.Offset.ToVector2() * Scale, color * fadeInBrokenSpriteAlpha, origin, RotationRad, Scale, activeSprite.effects, d); } } if (Infector != null && (Infector.ParentBallastFlora.HasBrokenThrough || BallastFloraBehavior.AlwaysShowBallastFloraSprite)) { Prefab.InfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, color, Prefab.InfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.001f); Prefab.DamagedInfectedSprite?.Draw(spriteBatch, new Vector2(DrawPosition.X, -DrawPosition.Y) + drawOffset, Infector.HealthColor, Prefab.DamagedInfectedSprite.Origin, RotationRad, Scale, activeSprite.effects, depth - 0.002f); } DrawDecorativeSprites(spriteBatch, DrawPosition, flippedX && Prefab.CanSpriteFlipX, flippedY && Prefab.CanSpriteFlipY, -RotationRad, depth, overrideColor); } } else if (body.Enabled) { var holdable = GetComponent(); if (holdable != null && holdable.Picker?.AnimController != null) { //don't draw the item on hands if it's also being worn if (GetComponent() is { IsActive: true }) { return; } if (!back) { return; } if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.RightHand) == this) { depth = GetHeldItemDepth(LimbType.RightHand, holdable, depth); } else if (holdable.Picker.Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand) == this) { depth = GetHeldItemDepth(LimbType.LeftHand, holdable, depth); } static float GetHeldItemDepth(LimbType limb, Holdable holdable, float depth) { if (holdable?.Picker?.AnimController == null) { return depth; } //offset used to make sure the item draws just slightly behind the right hand, or slightly in front of the left hand float limbDepthOffset = 0.000001f; float depthOffset = holdable.Picker.AnimController.GetDepthOffset(); //use the upper arm as a reference, to ensure the item gets drawn behind / in front of the whole arm (not just the forearm) Limb holdLimb = holdable.Picker.AnimController.GetLimb(limb == LimbType.RightHand ? LimbType.RightArm : LimbType.LeftArm); if (holdLimb?.ActiveSprite != null) { depth = holdLimb.ActiveSprite.Depth + depthOffset + limbDepthOffset * 2 * (limb == LimbType.RightHand ? 1 : -1); foreach (WearableSprite wearableSprite in holdLimb.WearingItems) { if (!wearableSprite.InheritLimbDepth && wearableSprite.Sprite != null) { depth = limb == LimbType.RightHand ? Math.Max(wearableSprite.Sprite.Depth + limbDepthOffset, depth) : Math.Min(wearableSprite.Sprite.Depth - limbDepthOffset, depth); } } var head = holdable.Picker.AnimController.GetLimb(LimbType.Head); if (head?.Sprite != null) { //ensure the holdable item is always drawn in front of the head no matter what the wearables or whatnot do with the sprite depths depth = limb == LimbType.RightHand ? Math.Min(head.Sprite.Depth + depthOffset - limbDepthOffset, depth) : Math.Max(head.Sprite.Depth + depthOffset + limbDepthOffset, depth); } } return depth; } } Vector2 origin = GetSpriteOrigin(activeSprite); body.Draw(spriteBatch, activeSprite, color, depth, Scale, origin: origin); if (fadeInBrokenSprite != null) { float d = Math.Min(depth + (fadeInBrokenSprite.Sprite.Depth - activeSprite.Depth - 0.000001f), 0.999f); body.Draw(spriteBatch, fadeInBrokenSprite.Sprite, color * fadeInBrokenSpriteAlpha, d, Scale); } DrawDecorativeSprites(spriteBatch, body.DrawPosition, flipX: body.Dir < 0, flipY: false, rotation: body.Rotation, depth, overrideColor); } foreach (var upgrade in Upgrades) { var upgradeSprites = GetUpgradeSprites(upgrade); foreach (var decorativeSprite in upgradeSprites) { if (!spriteAnimState[decorativeSprite].IsActive) { continue; } float rotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, -RotationRad) * Scale; if (flippedX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; } if (flippedY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(DrawPosition.X + offset.X, -(DrawPosition.Y + offset.Y)), color, decorativeSprite.Sprite.Origin, rotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, activeSprite.effects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } activeSprite.effects = oldEffects; if (fadeInBrokenSprite != null && fadeInBrokenSprite.Sprite != activeSprite) { fadeInBrokenSprite.Sprite.effects = oldBrokenSpriteEffects; } } //use a backwards for loop because the drawable components may disable drawing, //causing them to be removed from the list for (int i = drawableComponents.Count - 1; i >= 0; i--) { drawableComponents[i].Draw(spriteBatch, editing, depth, overrideColor); } if (GameMain.DebugDraw) { body?.DebugDraw(spriteBatch, Color.White); if (GetComponent()?.PhysicsBody is PhysicsBody triggerBody) { triggerBody.UpdateDrawPosition(); triggerBody.DebugDraw(spriteBatch, Color.White); } } if (editing && IsSelected && PlayerInput.KeyDown(Keys.Space)) { if (GetComponent() is { } discharger) { discharger.DrawElectricity(spriteBatch); } } if (!editing || (body != null && !body.Enabled)) { return; } if (IsSelected || IsHighlighted) { Vector2 drawPos = new Vector2(DrawPosition.X - rect.Width / 2, -(DrawPosition.Y + rect.Height / 2)); Vector2 drawSize = new Vector2(MathF.Ceiling(rect.Width + Math.Abs(drawPos.X - (int)drawPos.X)), MathF.Ceiling(rect.Height + Math.Abs(drawPos.Y - (int)drawPos.Y))); drawPos = new Vector2(MathF.Floor(drawPos.X), MathF.Floor(drawPos.Y)); GUI.DrawRectangle(sb: spriteBatch, center: drawPos + drawSize * 0.5f, width: drawSize.X, height: drawSize.Y, rotation: RotationRad, clr: Color.White, depth: 0, thickness: Math.Max(2f / Screen.Selected.Cam.Zoom, 1)); foreach (Rectangle t in Prefab.Triggers) { Rectangle transformedTrigger = TransformTrigger(t); Vector2 rectWorldPos = new Vector2(transformedTrigger.X, transformedTrigger.Y); if (Submarine != null) rectWorldPos += Submarine.Position; rectWorldPos.Y = -rectWorldPos.Y; GUI.DrawRectangle(spriteBatch, rectWorldPos, new Vector2(transformedTrigger.Width, transformedTrigger.Height), GUIStyle.Green, false, 0, (int)Math.Max((1.5f / GameScreen.Selected.Cam.Zoom), 1.0f)); } } if (!ShowLinks || GUI.DisableHUD) { return; } foreach (MapEntity e in linkedTo) { bool isLinkAllowed = Prefab.IsLinkAllowed(e.Prefab); Color lineColor = GUIStyle.Red * 0.5f; if (isLinkAllowed) { lineColor = e is Item i && (DisplaySideBySideWhenLinked || i.DisplaySideBySideWhenLinked) ? Color.Purple * 0.5f : Color.LightGreen * 0.5f; } Vector2 from = new Vector2(WorldPosition.X, -WorldPosition.Y); Vector2 to = new Vector2(e.WorldPosition.X, -e.WorldPosition.Y); GUI.DrawLine(spriteBatch, from, to, lineColor * 0.25f, width: 3); GUI.DrawLine(spriteBatch, from, to, lineColor, width: 1); //GUI.DrawString(spriteBatch, from, $"Linked to {e.Name}", lineColor, Color.Black * 0.5f); } Vector2 GetSpriteOrigin(Sprite sprite) { Vector2 origin = sprite.Origin; if ((sprite.effects & SpriteEffects.FlipHorizontally) == SpriteEffects.FlipHorizontally) { origin.X = sprite.SourceRect.Width - origin.X; } if ((sprite.effects & SpriteEffects.FlipVertically) == SpriteEffects.FlipVertically) { origin.Y = sprite.SourceRect.Height - origin.Y; } return origin; } Color GetSpriteColor(Color defaultColor) { return overrideColor ?? (IsIncludedInSelection && editing ? GUIStyle.Blue : this.GetSpriteColor(defaultColor: defaultColor, withHighlight: true)); } } public void DrawDecorativeSprites(SpriteBatch spriteBatch, Vector2 drawPos, bool flipX, bool flipY, float rotation, float depth, Color? overrideColor = null) { foreach (var decorativeSprite in Prefab.DecorativeSprites) { Color decorativeSpriteColor = overrideColor ?? GetSpriteColor(decorativeSprite.Color).Multiply(GetSpriteColor(spriteColor)); if (!spriteAnimState[decorativeSprite].IsActive) { continue; } Vector2 offset = decorativeSprite.GetOffset(ref spriteAnimState[decorativeSprite].OffsetState, spriteAnimState[decorativeSprite].RandomOffsetMultiplier, flipX ^ flipY ? -rotation : rotation) * Scale; if (ResizeHorizontal || ResizeVertical) { decorativeSprite.Sprite.DrawTiled(spriteBatch, new Vector2(DrawPosition.X + offset.X - rect.Width / 2, -(DrawPosition.Y + offset.Y + rect.Height / 2)), new Vector2(rect.Width, rect.Height), color: decorativeSpriteColor, textureScale: Vector2.One * Scale, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth), 0.999f)); } else { float spriteRotation = decorativeSprite.GetRotation(ref spriteAnimState[decorativeSprite].RotationState, spriteAnimState[decorativeSprite].RandomRotationFactor); Vector2 origin = decorativeSprite.Sprite.Origin; SpriteEffects spriteEffects = SpriteEffects.None; if (flipX && Prefab.CanSpriteFlipX) { offset.X = -offset.X; origin.X = -origin.X + decorativeSprite.Sprite.size.X; spriteEffects = SpriteEffects.FlipHorizontally; } if (flipY && Prefab.CanSpriteFlipY) { offset.Y = -offset.Y; origin.Y = -origin.Y + decorativeSprite.Sprite.size.Y; spriteEffects |= SpriteEffects.FlipVertically; } decorativeSprite.Sprite.Draw(spriteBatch, new Vector2(drawPos.X + offset.X, -(drawPos.Y + offset.Y)), decorativeSpriteColor, origin, -rotation + spriteRotation, decorativeSprite.GetScale(ref spriteAnimState[decorativeSprite].ScaleState, spriteAnimState[decorativeSprite].RandomScaleFactor) * Scale, spriteEffects, depth: depth + (decorativeSprite.Sprite.Depth - activeSprite.Depth)); } } } partial void OnCollisionProjSpecific(float impact) { if (impact > 1.0f && Container == null && !string.IsNullOrEmpty(Prefab.ImpactSoundTag) && Timing.TotalTime > LastImpactSoundTime + ImpactSoundInterval) { LastImpactSoundTime = (float)Timing.TotalTime; SoundPlayer.PlaySound(Prefab.ImpactSoundTag, WorldPosition, hullGuess: CurrentHull); } } partial void Splash() { if (body == null || CurrentHull == null) { return; } //create a splash particle float massFactor = MathHelper.Clamp(body.Mass, 0.5f, 20.0f); for (int i = 0; i < MathHelper.Clamp(Math.Abs(body.LinearVelocity.Y), 1.0f, 10.0f); i++) { var splash = GameMain.ParticleManager.CreateParticle("watersplash", new Vector2(WorldPosition.X, CurrentHull.WorldSurface), new Vector2(0.0f, Math.Abs(-body.LinearVelocity.Y * massFactor)) + Rand.Vector(Math.Abs(body.LinearVelocity.Y * 10)), Rand.Range(0.0f, MathHelper.TwoPi), CurrentHull); if (splash != null) { splash.Size *= MathHelper.Clamp(Math.Abs(body.LinearVelocity.Y) * 0.1f * massFactor, 1.0f, 4.0f); } } GameMain.ParticleManager.CreateParticle("bubbles", new Vector2(WorldPosition.X, CurrentHull.WorldSurface), body.LinearVelocity * massFactor, 0.0f, CurrentHull); //create a wave if (body.LinearVelocity.Y < 0.0f) { int n = (int)((Position.X - CurrentHull.Rect.X) / Hull.WaveWidth); if (n >= 0 && n < currentHull.WaveVel.Length) { CurrentHull.WaveVel[n] += MathHelper.Clamp(body.LinearVelocity.Y * massFactor, -5.0f, 5.0f); } } SoundPlayer.PlaySplashSound(WorldPosition, Math.Abs(body.LinearVelocity.Y) + Rand.Range(-10.0f, -5.0f)); } public void CheckNeedsSoundUpdate(ItemComponent ic) { if (ic.NeedsSoundUpdate()) { if (!updateableComponents.Contains(ic)) { updateableComponents.Add(ic); } isActive = true; } } public void UpdateSpriteStates(float deltaTime) { if (activeContainedSprite != null) { if (activeContainedSprite.DecorativeSpriteBehavior == ContainedItemSprite.DecorativeSpriteBehaviorType.HideWhenVisible) { foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) { var spriteState = spriteAnimState[decorativeSprite]; spriteState.IsActive = false; } return; } } else { foreach (var containedSprite in Prefab.ContainedSprites) { if (containedSprite.Sprite != activeSprite && containedSprite.DecorativeSpriteBehavior == ContainedItemSprite.DecorativeSpriteBehaviorType.HideWhenNotVisible) { foreach (DecorativeSprite decorativeSprite in Prefab.DecorativeSprites) { var spriteState = spriteAnimState[decorativeSprite]; spriteState.IsActive = false; } return; } } } if (Prefab.DecorativeSpriteGroups.Count > 0) { DecorativeSprite.UpdateSpriteStates(Prefab.DecorativeSpriteGroups, spriteAnimState, ID, deltaTime, ConditionalMatches); } foreach (var upgrade in Upgrades) { var upgradeSprites = GetUpgradeSprites(upgrade); foreach (var decorativeSprite in upgradeSprites) { var spriteState = spriteAnimState[decorativeSprite]; spriteState.IsActive = true; foreach (var conditional in decorativeSprite.IsActiveConditionals) { if (!ConditionalMatches(conditional)) { spriteState.IsActive = false; break; } } } } foreach (var containedItem in ContainedItems) { containedItem.UpdateSpriteStates(deltaTime); } } public override void UpdateEditing(Camera cam, float deltaTime) { if (editingHUD == null || editingHUD.UserData as Item != this) { editingHUD = CreateEditingHUD(Screen.Selected != GameMain.SubEditorScreen); editingHUDRefreshTimer = 1.0f; } if (editingHUDRefreshTimer <= 0.0f) { activeEditors.ForEach(e => e?.RefreshValues()); editingHUDRefreshTimer = 1.0f; } if (Screen.Selected != GameMain.SubEditorScreen) { return; } if (GetComponent() is { } discharger) { if (PlayerInput.KeyDown(Keys.Space)) { discharger.FindNodes(WorldPosition, discharger.Range); } else { discharger.IsActive = false; } } if (Character.Controlled == null) { activeHUDs.Clear(); } foreach (ItemComponent ic in components) { ic.UpdateEditing(deltaTime); } if (!Linkable) { return; } if (!PlayerInput.KeyDown(Keys.Space)) { return; } bool lClick = PlayerInput.PrimaryMouseButtonClicked(); bool rClick = PlayerInput.SecondaryMouseButtonClicked(); if (!lClick && !rClick) { return; } Vector2 position = cam.ScreenToWorld(PlayerInput.MousePosition); var otherEntity = highlightedEntities.FirstOrDefault(e => e != this && e.IsMouseOn(position)); if (otherEntity != null) { if (linkedTo.Contains(otherEntity)) { linkedTo.Remove(otherEntity); if (otherEntity.linkedTo != null && otherEntity.linkedTo.Contains(this)) { otherEntity.linkedTo.Remove(this); } } else { linkedTo.Add(otherEntity); if (otherEntity.Linkable && otherEntity.linkedTo != null) { otherEntity.linkedTo.Add(this); } } } } public override bool IsMouseOn(Vector2 position) { Vector2 rectSize = rect.Size.ToVector2(); Vector2 bodyPos = WorldPosition; Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, RotationRad); return Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f && Math.Abs(transformedMousePos.Y - bodyPos.Y) < rectSize.Y / 2.0f; } public GUIComponent CreateEditingHUD(bool inGame = false) { activeEditors.Clear(); 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, Spacing = (int)(25 * GUI.Scale) }; var itemEditor = new SerializableEntityEditor(listBox.Content.RectTransform, this, inGame, showName: true, titleFont: GUIStyle.LargeFont) { UserData = this }; activeEditors.Add(itemEditor); itemEditor.Children.First().Color = Color.Black * 0.7f; if (!inGame) { if (Linkable) { var linkText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("HoldToLink"), font: GUIStyle.SmallFont); var itemsText = new GUITextBlock(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), TextManager.Get("AllowedLinks"), font: GUIStyle.SmallFont); LocalizedString allowedItems = AllowedLinks.None() ? TextManager.Get("None") : string.Join(", ", AllowedLinks); itemsText.Text = TextManager.AddPunctuation(':', itemsText.Text, allowedItems); itemEditor.AddCustomContent(linkText, 1); itemEditor.AddCustomContent(itemsText, 2); linkText.TextColor = GUIStyle.Orange; itemsText.TextColor = GUIStyle.Orange; } //create a tag picker for item containers to make it easier to pick relevant tags for PreferredContainers var itemContainer = GetComponent(); if (itemContainer != null) { var tagBox = itemEditor.Fields["Tags".ToIdentifier()].First() as GUITextBox; var tagsField = tagBox?.Parent; var containerTagLayout = new GUILayoutGroup(new RectTransform(new Point(editingHUD.Rect.Width, heightScaled), isFixedSize: true), isHorizontal: true); var containerTagButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.25f, 1), containerTagLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterRight); new GUIButton(new RectTransform(new Vector2(0.95f, 1), containerTagButtonLayout.RectTransform), text: TextManager.Get("containertaguibutton"), style: "GUIButtonSmall") { OnClicked = (_, _) => { CreateContainerTagPicker(tagBox); return true; }, TextBlock = { AutoScaleHorizontal = true } }; var containerTagText = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1), containerTagLayout.RectTransform), TextManager.Get("containertaguibuttondescription"), font: GUIStyle.SmallFont) { TextColor = GUIStyle.Orange }; var limitedString = ToolBox.LimitString(containerTagText.Text, containerTagText.Font, itemEditor.Rect.Width - containerTagButtonLayout.Rect.Width); if (limitedString != containerTagText.Text) { containerTagText.ToolTip = containerTagText.Text; containerTagText.Text = limitedString; } itemEditor.AddCustomContent(containerTagLayout, 3); } var buttonContainer = new GUILayoutGroup(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.02f, CanBeFocused = true }; GUINumberInput rotationField = itemEditor.Fields.TryGetValue("Rotation".ToIdentifier(), out var rotationFieldComponents) ? rotationFieldComponents.OfType().FirstOrDefault() : null; var mirrorX = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("MirrorEntityX"), style: "GUIButtonSmall") { ToolTip = TextManager.Get("MirrorEntityXToolTip"), Enabled = Prefab.CanFlipX, OnClicked = (button, data) => { foreach (MapEntity me in SelectedList) { me.FlipX(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipX(relativeToSub: false); } ColorFlipButton(button, FlippedX); if (rotationField != null) { rotationField.FloatValue = Rotation; } 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"), Enabled = Prefab.CanFlipY, OnClicked = (button, data) => { foreach (MapEntity me in SelectedList) { me.FlipY(relativeToSub: false); } if (!SelectedList.Contains(this)) { FlipY(relativeToSub: false); } ColorFlipButton(button, FlippedY); if (rotationField != null) { rotationField.FloatValue = Rotation; } return true; } }; ColorFlipButton(mirrorY, FlippedY); if (Sprite != null) { var reloadTextureButton = new GUIButton(new RectTransform(new Vector2(0.23f, 1.0f), buttonContainer.RectTransform), TextManager.Get("ReloadSprite"), style: "GUIButtonSmall"); reloadTextureButton.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.MinSize = new Point(0, buttonContainer.RectTransform.Children.Max(c => c.MinSize.Y)); buttonContainer.RectTransform.IsFixedSize = true; itemEditor.AddCustomContent(buttonContainer, itemEditor.ContentCount); GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Select(b => ((GUIButton)b).TextBlock)); 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; } }; itemEditor.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)); itemEditor.AddCustomContent(layerText, 1); } } foreach (ItemComponent ic in components) { if (inGame) { if (!ic.AllowInGameEditing) { continue; } if (SerializableProperty.GetProperties(ic).Count == 0 && !SerializableProperty.GetProperties(ic).Any(p => p.GetAttribute().IsEditable(ic))) { continue; } } else { if (ic.RequiredItems.Count == 0 && ic.DisabledRequiredItems.Count == 0 && SerializableProperty.GetProperties(ic).Count == 0) { continue; } } new GUIFrame(new RectTransform(new Vector2(1.0f, 0.02f), listBox.Content.RectTransform), style: "HorizontalLine"); var componentEditor = new SerializableEntityEditor(listBox.Content.RectTransform, ic, inGame, showName: !inGame, titleFont: GUIStyle.SubHeadingFont) { UserData = ic }; componentEditor.Children.First().Color = Color.Black * 0.7f; activeEditors.Add(componentEditor); if (inGame) { ic.CreateEditingHUD(componentEditor); componentEditor.Recalculate(); continue; } List requiredItems = new List(); foreach (var kvp in ic.RequiredItems) { foreach (RelatedItem relatedItem in kvp.Value) { requiredItems.Add(relatedItem); } } requiredItems.AddRange(ic.DisabledRequiredItems); foreach (RelatedItem relatedItem in requiredItems) { var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, heightScaled)), TextManager.Get($"{relatedItem.Type}.required").Fallback($"{relatedItem.Type} required"), font: GUIStyle.SmallFont) { Padding = new Vector4(10.0f, 0.0f, 10.0f, 0.0f) }; var tooltip = TextManager.Get($"{relatedItem.Type}.required.tooltip").Fallback(LocalizedString.EmptyString); if (!tooltip.IsNullOrWhiteSpace()) { textBlock.ToolTip = tooltip; } textBlock.RectTransform.IsFixedSize = true; componentEditor.AddCustomContent(textBlock, 1); GUITextBox namesBox = new GUITextBox(new RectTransform(new Vector2(0.5f, 1.0f), textBlock.RectTransform, Anchor.CenterRight)) { Font = GUIStyle.SmallFont, Text = relatedItem.JoinedIdentifiers, OverflowClip = true }; textBlock.RectTransform.Resize(new Point(textBlock.Rect.Width, namesBox.RectTransform.MinSize.Y)); namesBox.OnDeselected += (textBox, key) => { relatedItem.JoinedIdentifiers = textBox.Text; textBox.Text = relatedItem.JoinedIdentifiers; }; namesBox.OnEnterPressed += (textBox, text) => { relatedItem.JoinedIdentifiers = text; textBox.Text = relatedItem.JoinedIdentifiers; return true; }; } ic.CreateEditingHUD(componentEditor); componentEditor.Recalculate(); } PositionEditingHUD(); SetHUDLayout(); return editingHUD; } private ImmutableArray GetUpgradeSprites(Upgrade upgrade) { var upgradeSprites = upgrade.Prefab.DecorativeSprites; if (Prefab.UpgradeOverrideSprites.ContainsKey(upgrade.Prefab.Identifier)) { upgradeSprites = Prefab.UpgradeOverrideSprites[upgrade.Prefab.Identifier]; } return upgradeSprites; } public override bool AddUpgrade(Upgrade upgrade, bool createNetworkEvent = false) { if (upgrade.Prefab.IsWallUpgrade) { return false; } bool result = base.AddUpgrade(upgrade, createNetworkEvent); if (result && !upgrade.Disposed) { var upgradeSprites = GetUpgradeSprites(upgrade); if (upgradeSprites.Any()) { foreach (DecorativeSprite decorativeSprite in upgradeSprites) { decorativeSprite.Sprite.EnsureLazyLoaded(); spriteAnimState.Add(decorativeSprite, new DecorativeSprite.State()); } UpdateSpriteStates(0.0f); } } return result; } public void CreateContainerTagPicker([MaybeNull] GUITextBox tagTextBox) { var msgBox = new GUIMessageBox(string.Empty, string.Empty, new[] { TextManager.Get("Ok") }, new Vector2(0.35f, 0.6f), new Point(400, 400)); msgBox.Buttons[0].OnClicked = msgBox.Close; var infoIcon = new GUIImage(new RectTransform(new Vector2(0.066f), msgBox.InnerFrame.RectTransform) { RelativeOffset = new Vector2(0.015f) }, style: "GUIButtonInfo") { ToolTip = TextManager.Get("containertagui.tutorial"), IgnoreLayoutGroups = true }; var layout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.85f), msgBox.Content.RectTransform)); var list = new GUIListBox(new RectTransform(new Vector2(1f, 1f), layout.RectTransform)); const float NameSize = 0.4f; const float ItemSize = 0.5f; const float CountSize = 0.1f; var headerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.075f), list.Content.RectTransform), isHorizontal: true); new GUIButton(new RectTransform(new Vector2(NameSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.tag"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; new GUIButton(new RectTransform(new Vector2(ItemSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.items"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; new GUIButton(new RectTransform(new Vector2(CountSize, 1f), headerLayout.RectTransform), TextManager.Get("tagheader.count"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; var itemsByTag = ContainerTagPrefab.Prefabs .ToImmutableDictionary( ct => ct, ct => ct.GetItemsAndSpawnProbabilities()); // Group the prefabs by category and turn them into a dictionary where the key is the category and value is the list of identifiers of the prefabs. // LINQ GroupBy returns GroupedEnumerable where the enumerable is the list of prefabs and key is what we grouped by. var tagCategories = ContainerTagPrefab.Prefabs .GroupBy(ct => ct.Category) .ToImmutableDictionary( g => g.Key, g => g.Select(ct => ct.Identifier).ToImmutableArray()); foreach (var (category, categoryTags) in tagCategories) { var categoryButton = new GUIButton(new RectTransform(new Vector2(1f, 0.075f), list.Content.RectTransform), style: "GUIButtonSmallFreeScale"); categoryButton.Color *= 0.66f; var categoryLayout = new GUILayoutGroup(new RectTransform(Vector2.One, categoryButton.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; var categoryText = new GUITextBlock(new RectTransform(Vector2.One, categoryLayout.RectTransform), TextManager.Get($"tagcategory.{category}"), font: GUIStyle.SubHeadingFont); var arrowImage = new GUIImage(new RectTransform(new Vector2(1f, 0.5f), categoryLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUIButtonVerticalArrowFreeScale"); var arrowPadding = new GUIFrame(new RectTransform(new Vector2(0.025f, 1f), categoryLayout.RectTransform), style: null); bool hasHiddenCategories = false; foreach (var categoryTag in categoryTags.OrderBy(t => t.Value)) { var found = itemsByTag.FirstOrNull(kvp => kvp.Key.Identifier == categoryTag); if (found is null) { DebugConsole.ThrowError($"Failed to find tag with identifier {categoryTag} in itemsByTag"); continue; } var (tag, prefabsAndProbabilities) = found.Value; bool isCorrectSubType = tag.IsRecommendedForSub(Submarine); var tagLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), list.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = category, Visible = isCorrectSubType }; if (!isCorrectSubType) { hasHiddenCategories = true; } var checkBoxLayout = new GUILayoutGroup(new RectTransform(new Vector2(NameSize, 1f), tagLayout.RectTransform), childAnchor: Anchor.Center); var enabledCheckBox = new GUITickBox(new RectTransform(Vector2.One, checkBoxLayout.RectTransform, Anchor.Center), tag.Name, font: GUIStyle.SmallFont) { Selected = tags.Contains(tag.Identifier), ToolTip = tag.Description }; var tickBoxText = enabledCheckBox.TextBlock; tickBoxText.Text = ToolBox.LimitString(tickBoxText.Text, tickBoxText.Font, tickBoxText.Rect.Width); var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(ItemSize, 1f), tagLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); var itemLayoutScissor = new GUIScissorComponent(new RectTransform(new Vector2(0.8f, 1f), itemLayout.RectTransform)) { CanBeFocused = false }; var itemLayoutButtonLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.2f, 1), itemLayout.RectTransform), isHorizontal: true, childAnchor: Anchor.Center); var itemLayoutButton = new GUIButton(new RectTransform(new Vector2(0.8f), itemLayoutButtonLayout.RectTransform), text: "...", style: "GUICharacterInfoButton") { UserData = tag, ToolTip = TextManager.Get("containertagui.viewprobabilities") }; itemLayoutButtonLayout.Recalculate(); float scroll = 0f; float localScroll = 0f; int lastSkippedItems = 0; int skippedItems = 0; var itemLayoutDraw = new GUICustomComponent(new RectTransform(new Vector2(1f, 0.9f), itemLayoutScissor.Content.RectTransform, Anchor.CenterLeft), onDraw: (spriteBatch, component) => { component.ToolTip = string.Empty; const float padding = 8f; float offset = 0f; float size = component.Rect.Height; int start = (int)Math.Floor(scroll); int amountToDraw = (int)Math.Ceiling(component.Rect.Width / size) + 1; // +1 just to be on the safe side bool shouldIncrementOnSkip = true; float toDrawWidth = prefabsAndProbabilities.Length * (size + padding); // if the width is less than the component width we need to limit how many items we draw or it looks weird if (toDrawWidth < component.Rect.Width) { shouldIncrementOnSkip = false; amountToDraw = prefabsAndProbabilities.Length; } for (int i = start; i < start + amountToDraw; i++) { var (ip, probability, _) = prefabsAndProbabilities[i % prefabsAndProbabilities.Length]; var sprite = ip.InventoryIcon ?? ip.Sprite; if (sprite is null) { // I don't think this should happen but just in case if (shouldIncrementOnSkip) { amountToDraw++; skippedItems++; } continue; } if (ShouldHideItemPrefab(ip, probability)) { if (shouldIncrementOnSkip) { skippedItems++; amountToDraw++; } continue; } float partialScroll = localScroll * (size + padding); var drawRect = new RectangleF(itemLayoutScissor.Rect.X + offset - partialScroll, component.Rect.Y, size, size); var isMouseOver = drawRect.Contains(PlayerInput.MousePosition); if (isMouseOver) { component.ToolTip = ip.CreateTooltipText(); } var slotSprite = Inventory.SlotSpriteSmall; slotSprite?.Draw(spriteBatch, drawRect.Location, Color.White, origin: Vector2.Zero, rotate: 0f, scale: size / slotSprite.size.X * Inventory.SlotSpriteSmallScale); float iconScale = Math.Min(drawRect.Width / sprite.size.X, drawRect.Height / sprite.size.Y) * 0.9f; Color drawColor = ip.InventoryIconColor; sprite.Draw(spriteBatch, drawRect.Center, drawColor, origin: sprite.Origin, scale: iconScale); offset += size + padding; } // we need to compensate for the skipped items so that the scroll doesn't jump around if (skippedItems < lastSkippedItems) { scroll += lastSkippedItems - skippedItems; } lastSkippedItems = skippedItems; skippedItems = 0; }, onUpdate: (deltaTime, component) => { if (GUI.MouseOn != component && MathUtils.NearlyEqual(localScroll, 0, deltaTime * 2)) { localScroll = 0f; return; } float totalWidth = prefabsAndProbabilities.Length * (component.Rect.Height + 8f); if (totalWidth < component.Rect.Width) { return; } scroll += deltaTime; localScroll = scroll % 1f; }) { HoverCursor = CursorState.Default, AlwaysOverrideCursor = true }; var tooltip = TextManager.Get(tag.WarnIfLess ? "ContainerTagUI.RecommendedAmount" : "ContainerTagUI.SuggestedAmount"); var countBlock = new GUITextBlock(new RectTransform(new Vector2(CountSize, 1f), tagLayout.RectTransform), string.Empty, textAlignment: Alignment.Center) { ToolTip = tooltip }; UpdateCountBlock(countBlock, tag); enabledCheckBox.OnSelected += tickBox => { if (tickBox.Selected) { AddTag(tag.Identifier); } else { RemoveTag(tag.Identifier); } if (tagTextBox is not null) { tagTextBox.Text = string.Join(',', tags.Where(t => !Prefab.Tags.Contains(t))); } UpdateCountBlock(countBlock, tag); return true; }; itemLayoutButton.OnClicked = (button, _) => { CreateContainerTagItemListPopup(tag, button.Rect.Center, layout, prefabsAndProbabilities); return true; }; void UpdateCountBlock(GUITextBlock textBlock, ContainerTagPrefab containerTag) { if (textBlock is null) { return; } var tagCount = Submarine.GetItems(alsoFromConnectedSubs: true).Count(i => i.HasTag(containerTag.Identifier)); textBlock.Text = $"{tagCount} ({containerTag.RecommendedAmount})"; if (!isCorrectSubType || !containerTag.WarnIfLess || containerTag.RecommendedAmount <= 0) { return; } if (tagCount < containerTag.RecommendedAmount) { textBlock.TextColor = GUIStyle.Red; textBlock.Text += "*"; textBlock.ToolTip = RichString.Rich($"{tooltip}\n\n‖color:gui.red‖{TextManager.Get("ContainerTagUI.RecommendedAmountWarning")}‖color:end‖"); } else if (tagCount >= containerTag.RecommendedAmount) { textBlock.TextColor = GUIStyle.Green; textBlock.ToolTip = tooltip; } } } arrowImage.SpriteEffects = hasHiddenCategories ? SpriteEffects.None : SpriteEffects.FlipVertically; categoryButton.OnClicked = (_, _) => { arrowImage.SpriteEffects ^= SpriteEffects.FlipVertically; foreach (var child in list.Content.Children) { if (child.UserData is Identifier id && id == category) { child.Visible = !child.Visible; } } return true; }; } } private static void CreateContainerTagItemListPopup(ContainerTagPrefab tag, Point location, GUIComponent popupParent, ImmutableArray prefabAndProbabilities) { const string TooltipUserData = "tooltip"; const string ProbabilityUserData = "probability"; if (popupParent.GetChildByUserData(TooltipUserData) is { } existingTooltip) { popupParent.RemoveChild(existingTooltip); } var tooltip = new GUIFrame(new RectTransform(new Point(popupParent.Rect.Height), popupParent.RectTransform) { AbsoluteOffset = location - popupParent.Rect.Location }) { UserData = TooltipUserData, IgnoreLayoutGroups = true }; if (tooltip.Rect.Bottom > GameMain.GraphicsHeight) { int diffY = tooltip.Rect.Bottom - GameMain.GraphicsHeight; tooltip.RectTransform.AbsoluteOffset -= new Point(0, diffY); } if (tooltip.Rect.Right > GameMain.GraphicsWidth) { int diffX = tooltip.Rect.Right - GameMain.GraphicsWidth; tooltip.RectTransform.AbsoluteOffset -= new Point(diffX, 0); } var tooltipLayout = new GUILayoutGroup(new RectTransform(ToolBox.PaddingSizeParentRelative(tooltip.RectTransform, 0.9f), tooltip.RectTransform, Anchor.Center)); var tooltipHeader = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), tag.Name, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont); var tooltipList = new GUIListBox(new RectTransform(new Vector2(1f, 0.7f), tooltipLayout.RectTransform)); var tooltipHeaderLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), tooltipList.Content.RectTransform), isHorizontal: true); new GUIButton(new RectTransform(new Vector2(0.66f, 1f), tooltipHeaderLayout.RectTransform), TextManager.Get("tagheader.item"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; new GUIButton(new RectTransform(new Vector2(0.33f, 1f), tooltipHeaderLayout.RectTransform), TextManager.Get("tagheader.probability"), style: "GUIButtonSmallFreeScale") { ForceUpperCase = ForceUpperCase.Yes, CanBeFocused = false }; foreach (var itemAndProbability in prefabAndProbabilities.OrderByDescending(p => p.Probability)) { var (ip, probability, campaignOnlyProbability) = itemAndProbability; if (ShouldHideItemPrefab(ip, probability)) { continue; } var itemLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.1f), tooltipList.Content.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft) { UserData = itemAndProbability }; var itemNameLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.66f, 1f), itemLayout.RectTransform), childAnchor: Anchor.CenterLeft, isHorizontal: true) { Stretch = true }; var itemIcon = new GUIImage(new RectTransform(Vector2.One, itemNameLayout.RectTransform, scaleBasis: ScaleBasis.BothHeight), ip.InventoryIcon ?? ip.Sprite, scaleToFit: true) { Color = ip.InventoryIconColor }; var itemName = new GUITextBlock(new RectTransform(Vector2.One, itemNameLayout.RectTransform), ip.Name); itemName.Text = ToolBox.LimitString(ip.Name, itemName.Font, itemName.Rect.Width); var toolTipContainer = new GUIFrame(new RectTransform(Vector2.One, itemNameLayout.RectTransform), style: null) { IgnoreLayoutGroups = true, ToolTip = ip.CreateTooltipText() }; var probabilityText = new GUITextBlock(new RectTransform(new Vector2(0.33f, 1f), itemLayout.RectTransform), ProbabilityToPercentage(campaignOnlyProbability), textAlignment: Alignment.Right) { UserData = ProbabilityUserData }; if (MathUtils.NearlyEqual(campaignOnlyProbability, 0f)) { probabilityText.TextColor = GUIStyle.Red; } } var campaignCheckbox = new GUITickBox(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), label: TextManager.Get("containertagui.campaignonly")) { ToolTip = TextManager.Get("containertagui.campaignonlytooltip"), Selected = true, OnSelected = box => { foreach (var child in tooltipList.Content.Children) { if (child.UserData is not ContainerTagPrefab.ItemAndProbability data) { continue; } if (child.GetChildByUserData(ProbabilityUserData) is not GUITextBlock text) { continue; } float probability = box.Selected ? data.CampaignProbability : data.Probability; text.Text = ProbabilityToPercentage(probability); text.TextColor = MathUtils.NearlyEqual(probability, 0f) ? GUIStyle.Red : GUIStyle.TextColorNormal; } return true; } }; var tooltipClose = new GUIButton(new RectTransform(new Vector2(1f, 0.1f), tooltipLayout.RectTransform), TextManager.Get("Close")) { OnClicked = (_, _) => { popupParent.RemoveChild(tooltip); return true; } }; static LocalizedString ProbabilityToPercentage(float probability) => TextManager.GetWithVariable("percentageformat", "[value]", MathF.Round((probability * 100f), 1).ToString(CultureInfo.InvariantCulture)); } private static bool ShouldHideItemPrefab(ItemPrefab ip, float probability) => ip.HideInMenus && MathUtils.NearlyEqual(probability, 0f); /// /// Reposition currently active item interfaces to make sure they don't overlap with each other /// private void SetHUDLayout(bool ignoreLocking = false) { //reset positions first List elementsToMove = new List(); if (editingHUD != null && editingHUD.UserData == this && ((HasInGameEditableProperties && Character.Controlled?.SelectedItem == this) || Screen.Selected == GameMain.SubEditorScreen)) { elementsToMove.Add(editingHUD); } debugInitialHudPositions.Clear(); foreach (ItemComponent ic in activeHUDs) { if (ic.GuiFrame == null || ic.GetLinkUIToComponent() != null) { continue; } bool nearlyCoversScreen = ic.GuiFrame.Rect.Width >= GameMain.GraphicsWidth * 0.9f && ic.GuiFrame.Rect.Height >= GameMain.GraphicsHeight * 0.9f; // when we are not using overlap prevention, we still need to clamp the frame to the screen area to // prevent frames becoming inaccessible outside the screen for example after a resolution change if (ic.AllowUIOverlap || (!ignoreLocking && ic.LockGuiFramePosition) || nearlyCoversScreen) { ic.GuiFrame.ClampToArea(new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight)); continue; } ic.GuiFrame.RectTransform.ScreenSpaceOffset = ic.GuiFrameOffset; elementsToMove.Add(ic.GuiFrame); debugInitialHudPositions.Add(ic.GuiFrame.Rect); } List disallowedAreas = new List(); if (GameMain.GameSession?.CrewManager != null && Screen.Selected == GameMain.GameScreen) { int disallowedPadding = (int)(50 * GUI.Scale); disallowedAreas.Add(GameMain.GameSession.CrewManager.GetActiveCrewArea()); disallowedAreas.Add(new Rectangle( HUDLayoutSettings.ChatBoxArea.X - disallowedPadding, HUDLayoutSettings.ChatBoxArea.Y, HUDLayoutSettings.ChatBoxArea.Width + disallowedPadding, HUDLayoutSettings.ChatBoxArea.Height)); } if (Screen.Selected is SubEditorScreen editor) { disallowedAreas.Add(editor.EntityMenu.Rect); disallowedAreas.Add(editor.TopPanel.Rect); disallowedAreas.Add(editor.ToggleEntityMenuButton.Rect); } GUI.PreventElementOverlap(elementsToMove, disallowedAreas, clampArea: HUDLayoutSettings.ItemHUDArea); //System.Diagnostics.Debug.WriteLine("after: " + elementsToMove[0].Rect.ToString() + " " + elementsToMove[1].Rect.ToString()); foreach (ItemComponent ic in activeHUDs) { if (ic.GuiFrame == null) { continue; } var linkUIToComponent = ic.GetLinkUIToComponent(); if (linkUIToComponent == null) { continue; } ic.GuiFrame.RectTransform.ScreenSpaceOffset = linkUIToComponent.GuiFrame.RectTransform.ScreenSpaceOffset; } } private readonly List debugInitialHudPositions = new List(); private readonly List prevActiveHUDs = new List(); private readonly List activeComponents = new List(); private readonly List maxPriorityHUDs = new List(); public void UpdateHUD(Camera cam, Character character, float deltaTime) { bool editingHUDCreated = false; if ((HasInGameEditableProperties && (character.SelectedItem == this || EditableWhenEquipped)) || Screen.Selected == GameMain.SubEditorScreen) { GUIComponent prevEditingHUD = editingHUD; UpdateEditing(cam, deltaTime); editingHUDCreated = editingHUD != null && editingHUD != prevEditingHUD; } if (editingHUD == null || !(GUI.KeyboardDispatcher.Subscriber is GUITextBox textBox) || !editingHUD.IsParentOf(textBox)) { editingHUDRefreshTimer -= deltaTime; } prevActiveHUDs.Clear(); prevActiveHUDs.AddRange(activeHUDs); activeComponents.Clear(); activeComponents.AddRange(components); foreach (MapEntity entity in linkedTo) { if (Prefab.IsLinkAllowed(entity.Prefab) && entity is Item i) { if (!i.DisplaySideBySideWhenLinked) { continue; } activeComponents.AddRange(i.components); } } activeHUDs.Clear(); maxPriorityHUDs.Clear(); bool DrawHud(ItemComponent ic) { if (!ic.ShouldDrawHUD(character)) { return false; } if (character.HasEquippedItem(this)) { return ic.DrawHudWhenEquipped; } else { return ic.CanBeSelected && ic.HasRequiredItems(character, addMessage: false); } } //the HUD of the component with the highest priority will be drawn //if all components have a priority of 0, all of them are drawn foreach (ItemComponent ic in activeComponents) { if (ic.HudPriority > 0 && DrawHud(ic) && (maxPriorityHUDs.Count == 0 || ic.HudPriority >= maxPriorityHUDs[0].HudPriority)) { if (maxPriorityHUDs.Count > 0 && ic.HudPriority > maxPriorityHUDs[0].HudPriority) { maxPriorityHUDs.Clear(); } maxPriorityHUDs.Add(ic); } } if (maxPriorityHUDs.Count > 0) { activeHUDs.AddRange(maxPriorityHUDs); } else { foreach (ItemComponent ic in activeComponents) { if (DrawHud(ic)) { activeHUDs.Add(ic); } } } activeHUDs.Sort((h1, h2) => { return h2.HudLayer.CompareTo(h1.HudLayer); }); //active HUDs have changed, need to reposition if (!prevActiveHUDs.SequenceEqual(activeHUDs) || editingHUDCreated) { SetHUDLayout(); } Rectangle mergedHUDRect = Rectangle.Empty; foreach (ItemComponent ic in activeHUDs) { ic.UpdateHUD(character, deltaTime, cam); if (ic.GuiFrame != null && ic.GuiFrame.Rect.Height < GameMain.GraphicsHeight) { mergedHUDRect = mergedHUDRect == Rectangle.Empty ? ic.GuiFrame.Rect : Rectangle.Union(mergedHUDRect, ic.GuiFrame.Rect); } } if (mergedHUDRect != Rectangle.Empty) { if (itemInUseWarning != null) { itemInUseWarning.Visible = false; } foreach (Character otherCharacter in Character.CharacterList) { if (otherCharacter != character && otherCharacter.SelectedItem == this) { ItemInUseWarning.Visible = true; if (mergedHUDRect.Width > GameMain.GraphicsWidth / 2) { mergedHUDRect.Inflate(-GameMain.GraphicsWidth / 4, 0); } itemInUseWarning.RectTransform.ScreenSpaceOffset = new Point(mergedHUDRect.X, mergedHUDRect.Bottom); itemInUseWarning.RectTransform.NonScaledSize = new Point(mergedHUDRect.Width, (int)(50 * GUI.Scale)); if (itemInUseWarning.UserData != otherCharacter) { itemInUseWarning.Text = TextManager.GetWithVariable("ItemInUse", "[character]", otherCharacter.Name); itemInUseWarning.UserData = otherCharacter; } break; } } } } public void DrawHUD(SpriteBatch spriteBatch, Camera cam, Character character) { if (HasInGameEditableProperties && (character.SelectedItem == this || EditableWhenEquipped)) { DrawEditing(spriteBatch, cam); } foreach (ItemComponent ic in activeHUDs) { if (ic.CanBeSelected) { ic.DrawHUD(spriteBatch, character); } } if (GameMain.DebugDraw) { int i = 0; foreach (ItemComponent ic in activeHUDs) { if (i >= debugInitialHudPositions.Count) { break; } if (activeHUDs[i].GuiFrame == null) { continue; } if (ic.GuiFrame == null || ic.AllowUIOverlap || ic.GetLinkUIToComponent() != null) { continue; } GUI.DrawRectangle(spriteBatch, debugInitialHudPositions[i], Color.Orange); GUI.DrawRectangle(spriteBatch, ic.GuiFrame.Rect, Color.LightGreen); GUI.DrawLine(spriteBatch, debugInitialHudPositions[i].Location.ToVector2(), ic.GuiFrame.Rect.Location.ToVector2(), Color.Orange); i++; } } } readonly List texts = new(); public List GetHUDTexts(Character character, bool recreateHudTexts = true) { // Always create the texts if they have not yet been created if (texts.Any() && !recreateHudTexts) { return texts; } texts.Clear(); string nameText = RichString.Rich(Prefab.Name).SanitizedValue; if (Prefab.Tags.Contains("identitycard") || Tags.Contains("despawncontainer")) { string[] readTags = Tags.Split(','); string idName = null; foreach (string tag in readTags) { string[] s = tag.Split(':'); if (s[0] == "name") { idName = s[1]; break; } } if (idName != null) { nameText += $" ({idName})"; } } if (DroppedStack.Any()) { nameText += $" x{DroppedStack.Count()}"; } texts.Add(new ColoredText(nameText, GUIStyle.TextColorNormal, isCommand: false, isError: false)); if (CampaignMode.BlocksInteraction(CampaignInteractionType)) { texts.Add(new ColoredText(TextManager.GetWithVariable($"CampaignInteraction.{CampaignInteractionType}", "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(InputType.Use)).Value, Color.Cyan, isCommand: false, isError: false)); } else { foreach (ItemComponent itemComponent in components) { var interactionVisibility = GetComponentInteractionVisibility(character, itemComponent); if (interactionVisibility == InteractionVisibility.None) { continue; } if (itemComponent.DisplayMsg.IsNullOrEmpty()) { continue; } Color color = interactionVisibility == InteractionVisibility.MissingRequirement ? Color.Gray : Color.Cyan; texts.Add(new ColoredText(itemComponent.DisplayMsg.Value, color, isCommand: false, isError: false)); } } if (PlayerInput.KeyDown(InputType.ContextualCommand)) { texts.Add(new ColoredText(TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders")).Value, Color.Cyan, isCommand: false, isError: false)); } else { texts.Add(new ColoredText(TextManager.Get("itemmsg.morreoptionsavailable").Value, Color.LightGray * 0.7f, isCommand: false, isError: false)); } return texts; } private enum InteractionVisibility { None, MissingRequirement, Visible } /// /// Determine, for UI display purposes, the type of interaction visibility for an item component. /// /// Example: /// Visible -> Display cyan "click to interact" type text on item hover. /// MissingRequirement -> Display gray "need tool" type text on item hover. /// None -> Hide from item hover texts. /// /// Character, for tool requirement purposes. /// The item component to inspect. /// The interaction visibility state for this component. private static InteractionVisibility GetComponentInteractionVisibility(Character character, ItemComponent itemComponent) { if (!itemComponent.CanBePicked && !itemComponent.CanBeSelected) { return InteractionVisibility.None; } if (itemComponent is Holdable holdable && !holdable.CanBeDeattached()) { return InteractionVisibility.None; } if (itemComponent is ConnectionPanel connectionPanel && !connectionPanel.CanRewire()) { return InteractionVisibility.None; } InteractionVisibility interactionVisibility = InteractionVisibility.MissingRequirement; if (itemComponent.HasRequiredItems(character, addMessage: false)) { if (itemComponent is Repairable repairable) { if (repairable.IsBelowRepairThreshold) { interactionVisibility = InteractionVisibility.Visible; } } else { interactionVisibility = InteractionVisibility.Visible; } } return interactionVisibility; } public bool HasVisibleInteraction(Character character) { foreach (var component in components) { if (GetComponentInteractionVisibility(character, component) == InteractionVisibility.Visible) { return true; } } return false; } public void ForceHUDLayoutUpdate(bool ignoreLocking = false) { foreach (ItemComponent ic in activeHUDs) { if (ic.GuiFrame == null) { continue; } if (!ic.CanBeSelected && !ic.DrawHudWhenEquipped) { continue; } ic.GuiFrame.RectTransform.ScreenSpaceOffset = Point.Zero; if (ic.UseAlternativeLayout) { ic.AlternativeLayout?.ApplyTo(ic.GuiFrame.RectTransform); } else { ic.DefaultLayout?.ApplyTo(ic.GuiFrame.RectTransform); } } SetHUDLayout(ignoreLocking); } public override void AddToGUIUpdateList(int order = 0) { if (Screen.Selected is SubEditorScreen) { if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); } } else { if (HasInGameEditableProperties && Character.Controlled != null && (Character.Controlled.SelectedItem == this || EditableWhenEquipped)) { if (editingHUD != null && editingHUD.UserData == this) { editingHUD.AddToGUIUpdateList(); } } } var character = Character.Controlled; var selectedItem = Character.Controlled?.SelectedItem; if (character != null && selectedItem != this && GetComponent() == null) { bool insideCircuitBox = selectedItem?.GetComponent() != null && selectedItem.ContainedItems.Contains(this); if (!insideCircuitBox && selectedItem?.GetComponent()?.TargetItem != this && !character.HeldItems.Any(it => it.GetComponent()?.TargetItem == this)) { return; } } bool needsLayoutUpdate = false; foreach (ItemComponent ic in activeHUDs) { if (!ic.CanBeSelected) { continue; } bool useAlternativeLayout = activeHUDs.Count > 1; bool wasUsingAlternativeLayout = ic.UseAlternativeLayout; ic.UseAlternativeLayout = useAlternativeLayout; needsLayoutUpdate |= ic.UseAlternativeLayout != wasUsingAlternativeLayout; ic.AddToGUIUpdateList(order); } if (itemInUseWarning != null && itemInUseWarning.Visible) { itemInUseWarning.AddToGUIUpdateList(); } if (needsLayoutUpdate) { SetHUDLayout(); } } public void ClientEventRead(IReadMessage msg, float sendingTime) { EventType eventType = (EventType)msg.ReadRangedInteger((int)EventType.MinValue, (int)EventType.MaxValue); switch (eventType) { case EventType.ComponentState: { int componentIndex = msg.ReadRangedInteger(0, components.Count - 1); if (components[componentIndex] is IServerSerializable serverSerializable) { serverSerializable.ClientEventRead(msg, sendingTime); } else { throw new Exception($"Failed to read component state - {components[componentIndex].GetType()} in item \"{Prefab.Identifier}\" is not IServerSerializable."); } } break; case EventType.InventoryState: { int containerIndex = msg.ReadRangedInteger(0, components.Count - 1); if (components[containerIndex] is ItemContainer container) { container.Inventory.ClientEventRead(msg); } else { throw new Exception($"Failed to read inventory state - {components[containerIndex].GetType()} in item \"{Prefab.Identifier}\" is not an ItemContainer."); } } break; case EventType.Status: bool loadingRound = msg.ReadBoolean(); float newCondition = msg.ReadSingle(); SetCondition(newCondition, isNetworkEvent: true, executeEffects: !loadingRound); break; case EventType.AssignCampaignInteraction: bool isVisible = msg.ReadBoolean(); if (isVisible) { var interactionType = (CampaignMode.InteractionType)msg.ReadByte(); AssignCampaignInteractionType(interactionType); } break; case EventType.ApplyStatusEffect: { ActionType actionType = (ActionType)msg.ReadRangedInteger(0, Enum.GetValues(typeof(ActionType)).Length - 1); byte componentIndex = msg.ReadByte(); ushort targetCharacterID = msg.ReadUInt16(); byte targetLimbID = msg.ReadByte(); ushort useTargetID = msg.ReadUInt16(); Vector2? worldPosition = null; bool hasPosition = msg.ReadBoolean(); if (hasPosition) { worldPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); } ItemComponent targetComponent = componentIndex < components.Count ? components[componentIndex] : null; Character targetCharacter = FindEntityByID(targetCharacterID) as Character; Limb targetLimb = targetCharacter != null && targetLimbID < targetCharacter.AnimController.Limbs.Length ? targetCharacter.AnimController.Limbs[targetLimbID] : null; Entity useTarget = FindEntityByID(useTargetID); if (targetComponent == null) { ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, isNetworkEvent: true, worldPosition: worldPosition); } else { targetComponent.ApplyStatusEffects(actionType, 1.0f, targetCharacter, targetLimb, useTarget, worldPosition: worldPosition); } } break; case EventType.ChangeProperty: ReadPropertyChange(msg, false); break; case EventType.ItemStat: byte length = msg.ReadByte(); for (int i = 0; i < length; i++) { var statIdentifier = INetSerializableStruct.Read(msg); var statValue = msg.ReadSingle(); StatManager.ApplyStatDirect(statIdentifier, statValue); } break; case EventType.Upgrade: Identifier identifier = msg.ReadIdentifier(); byte level = msg.ReadByte(); if (UpgradePrefab.Find(identifier) is { } upgradePrefab) { Upgrade upgrade = new Upgrade(this, upgradePrefab, level); byte targetCount = msg.ReadByte(); for (int i = 0; i < targetCount; i++) { byte propertyCount = msg.ReadByte(); for (int j = 0; j < propertyCount; j++) { float value = msg.ReadSingle(); upgrade.TargetComponents.ElementAt(i).Value[j].SetOriginalValue(value); } } AddUpgrade(upgrade, false); } break; case EventType.DroppedStack: int itemCount = msg.ReadRangedInteger(0, Inventory.MaxPossibleStackSize); if (itemCount > 0) { List droppedStack = new List(); for (int i = 0; i < itemCount; i++) { var id = msg.ReadUInt16(); if (FindEntityByID(id) is not Item droppedItem) { DebugConsole.ThrowError($"Error while reading {EventType.DroppedStack} message: could not find an item with the ID {id}."); } else { droppedStack.Add(droppedItem); } } CreateDroppedStack(droppedStack, allowClientExecute: true); } else { RemoveFromDroppedStack(allowClientExecute: true); } break; case EventType.SetHighlight: bool isTargetedForThisClient = msg.ReadBoolean(); if (isTargetedForThisClient) { bool highlight = msg.ReadBoolean(); ExternalHighlight = highlight; if (highlight) { Color highlightColor = msg.ReadColorR8G8B8A8(); HighlightColor = highlightColor; } else { HighlightColor = null; } } break; case EventType.SwapItem: ushort newId = msg.ReadUInt16(); uint prefabUintId = msg.ReadUInt32(); ItemPrefab newPrefab = ItemPrefab.Prefabs.FirstOrDefault(p => p.UintIdentifier == prefabUintId); if (newPrefab is null) { DebugConsole.ThrowError($"Error while reading {EventType.SwapItem} message: could not find an item prefab with the hash {prefabUintId}."); break; } ReplaceFromNetwork(newPrefab, newId); break; default: throw new Exception($"Malformed incoming item event: unsupported event type {eventType}"); } } public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null) { Exception error(string reason) { string errorMsg = $"Failed to write a network event for the item \"{Name}\" - {reason}"; GameAnalyticsManager.AddErrorEventOnce($"Item.ClientWrite:{Name}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return new Exception(errorMsg); } if (extraData is null) { throw error("event data was null"); } if (!(extraData is IEventData eventData)) { throw error($"event data was of the wrong type (\"{extraData.GetType().Name}\")"); } EventType eventType = eventData.EventType; msg.WriteRangedInteger((int)eventType, (int)EventType.MinValue, (int)EventType.MaxValue); switch (eventData) { case ComponentStateEventData componentStateEventData: { var component = componentStateEventData.Component; if (component is null) { throw error("component was null"); } if (component is not IClientSerializable clientSerializable) { throw error($"component was not {nameof(IClientSerializable)}"); } int componentIndex = components.IndexOf(component); if (componentIndex < 0) { throw error("component did not belong to item"); } msg.WriteRangedInteger(componentIndex, 0, components.Count - 1); clientSerializable.ClientEventWrite(msg, extraData); } break; case InventoryStateEventData inventoryStateEventData: { var container = inventoryStateEventData.Component; if (container is null) { throw error("container was null"); } int containerIndex = components.IndexOf(container); if (containerIndex < 0) { throw error("container did not belong to item"); } msg.WriteRangedInteger(containerIndex, 0, components.Count - 1); container.Inventory.ClientEventWrite(msg, inventoryStateEventData); } break; case TreatmentEventData treatmentEventData: Character targetCharacter = treatmentEventData.TargetCharacter; msg.WriteUInt16(targetCharacter.ID); msg.WriteByte(treatmentEventData.LimbIndex); break; case ChangePropertyEventData changePropertyEventData: WritePropertyChange(msg, changePropertyEventData, inGameEditableOnly: true); editingHUDRefreshTimer = 1.0f; break; case CombineEventData combineEventData: Item combineTarget = combineEventData.CombineTarget; msg.WriteUInt16(combineTarget.ID); break; default: throw error($"Unsupported event type {eventData.GetType().Name}"); } } partial void UpdateNetPosition(float deltaTime) { if (GameMain.Client == null) { return; } if (parentInventory != null || body == null || !body.Enabled || Removed || (GetComponent() is { IsStuckToTarget: true })) { positionBuffer.Clear(); return; } isActive = true; if (positionBuffer.Count > 0) { transformDirty = true; } body.CorrectPosition(positionBuffer, out Vector2 newPosition, out Vector2 newVelocity, out float newRotation, out float newAngularVelocity); body.LinearVelocity = newVelocity; body.AngularVelocity = newAngularVelocity; float distSqr = Vector2.DistanceSquared(newPosition, body.SimPosition); if (distSqr > 0.0001f || Math.Abs(newRotation - body.Rotation) > 0.01f) { body.TargetPosition = newPosition; body.TargetRotation = newRotation; body.MoveToTargetPosition(lerp: true); if (distSqr > 10.0f * 10.0f) { //very large change in position, we need to recheck which submarine the item is in Submarine = null; UpdateTransform(); } } //if the item is outside the level, but not in a sub, it implies the item is inside a sub server-side but the client failed to properly move it // -> let's correct that by finding the correct sub if (Level.IsPositionAboveLevel(WorldPosition) && Submarine == null) { var newSub = Submarine.FindContainingInLocalCoordinates(ConvertUnits.ToDisplayUnits(body.SimPosition), inflate: 0.0f); if (newSub != null) { Submarine = newSub; FindHull(); } } Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition); rect.X = (int)(displayPos.X - rect.Width / 2.0f); rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); } public void ClientReadPosition(IReadMessage msg, float sendingTime) { if (body == null) { string errorMsg = "Received a position update for an item with no physics body (" + Name + ")"; #if DEBUG DebugConsole.ThrowError(errorMsg); #else if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); } #endif GameAnalyticsManager.AddErrorEventOnce("Item.ClientReadPosition:nophysicsbody", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); return; } var posInfo = body.ClientRead(msg, sendingTime, parentDebugName: Name); msg.ReadPadBits(); if (GetComponent() is { IsStuckToTarget: true }) { return; } if (posInfo != null) { int index = 0; while (index < positionBuffer.Count && sendingTime > positionBuffer[index].Timestamp) { index++; } positionBuffer.Insert(index, posInfo); } /*body.FarseerBody.Awake = awake; if (body.FarseerBody.Awake) { if ((newVelocity - body.LinearVelocity).LengthSquared() > 8.0f * 8.0f) body.LinearVelocity = newVelocity; } else { try { body.FarseerBody.Enabled = false; } catch (Exception e) { DebugConsole.ThrowError("Exception in PhysicsBody.Enabled = false (" + body.PhysEnabled + ")", e); if (body.UserData != null) DebugConsole.NewMessage("PhysicsBody UserData: " + body.UserData.GetType().ToString(), GUIStyle.Red); if (GameMain.World.ContactManager == null) DebugConsole.NewMessage("ContactManager is null!", GUIStyle.Red); else if (GameMain.World.ContactManager.BroadPhase == null) DebugConsole.NewMessage("Broadphase is null!", GUIStyle.Red); if (body.FarseerBody.FixtureList == null) DebugConsole.NewMessage("FixtureList is null!", GUIStyle.Red); } } if ((newPosition - SimPosition).Length() > body.LinearVelocity.Length() * 2.0f) { if (body.SetTransform(newPosition, newRotation)) { Vector2 displayPos = ConvertUnits.ToDisplayUnits(body.SimPosition); rect.X = (int)(displayPos.X - rect.Width / 2.0f); rect.Y = (int)(displayPos.Y + rect.Height / 2.0f); } }*/ } public void CreateClientEvent(T ic) where T : ItemComponent, IClientSerializable => CreateClientEvent(ic, null); public void CreateClientEvent(T ic, ItemComponent.IEventData extraData) where T : ItemComponent, IClientSerializable { if (GameMain.Client == null) { return; } #warning TODO: this should throw an exception if (!components.Contains(ic)) { return; } var eventData = new ComponentStateEventData(ic, extraData); if (!ic.ValidateEventData(eventData)) { string errorMsg = $"Client-side component event creation for the item \"{Prefab.Identifier}\" failed: {typeof(T).Name}.{nameof(ItemComponent.ValidateEventData)} returned false. " + $"Data: {extraData?.GetType().ToString() ?? "null"}"; GameAnalyticsManager.AddErrorEventOnce($"Item.CreateClientEvent:ValidateEventData:{Prefab.Identifier}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); throw new Exception(errorMsg); } GameMain.Client.CreateEntityEvent(this, eventData); } public static Item ReadSpawnData(IReadMessage msg, bool spawn = true) { string itemName = msg.ReadString(); string itemIdentifier = msg.ReadString(); bool descriptionChanged = msg.ReadBoolean(); string itemDesc = ""; if (descriptionChanged) { itemDesc = msg.ReadString(); } ushort itemId = msg.ReadUInt16(); ushort inventoryId = msg.ReadUInt16(); DebugConsole.Log($"Received entity spawn message for item \"{itemName}\" (identifier: {itemIdentifier}, id: {itemId})"); ItemPrefab itemPrefab = string.IsNullOrEmpty(itemIdentifier) ? ItemPrefab.Find(itemName, Identifier.Empty) : ItemPrefab.Find(itemName, itemIdentifier.ToIdentifier()); Vector2 pos = Vector2.Zero; Submarine sub = null; float rotation = 0.0f; int itemContainerIndex = -1; int inventorySlotIndex = -1; if (inventoryId > 0) { itemContainerIndex = msg.ReadByte(); inventorySlotIndex = msg.ReadByte(); } else { pos = new Vector2(msg.ReadSingle(), msg.ReadSingle()); rotation = msg.ReadRangedSingle(0, MathHelper.TwoPi, 8); ushort subID = msg.ReadUInt16(); if (subID > 0) { sub = Submarine.Loaded.Find(s => s.ID == subID); } } byte bodyType = msg.ReadByte(); bool spawnedInOutpost = msg.ReadBoolean(); bool allowStealing = msg.ReadBoolean(); int quality = msg.ReadRangedInteger(0, Items.Components.Quality.MaxQuality); byte teamID = msg.ReadByte(); bool hasIdCard = msg.ReadBoolean(); string ownerName = "", ownerTags = ""; int ownerBeardIndex = -1, ownerHairIndex = -1, ownerMoustacheIndex = -1, ownerFaceAttachmentIndex = -1; Color ownerHairColor = Color.White, ownerFacialHairColor = Color.White, ownerSkinColor = Color.White; Identifier ownerJobId = Identifier.Empty; Vector2 ownerSheetIndex = Vector2.Zero; int submarineSpecificId = 0; if (hasIdCard) { submarineSpecificId = msg.ReadInt32(); ownerName = msg.ReadString(); ownerTags = msg.ReadString(); ownerBeardIndex = msg.ReadByte() - 1; ownerHairIndex = msg.ReadByte() - 1; ownerMoustacheIndex = msg.ReadByte() - 1; ownerFaceAttachmentIndex = msg.ReadByte() - 1; ownerHairColor = msg.ReadColorR8G8B8(); ownerFacialHairColor = msg.ReadColorR8G8B8(); ownerSkinColor = msg.ReadColorR8G8B8(); ownerJobId = msg.ReadIdentifier(); int x = msg.ReadByte(); int y = msg.ReadByte(); ownerSheetIndex = (x, y); } bool tagsChanged = msg.ReadBoolean(); string tags = ""; if (tagsChanged) { HashSet addedTags = msg.ReadString().ToIdentifiers().ToHashSet(); HashSet removedTags = msg.ReadString().ToIdentifiers().ToHashSet(); if (itemPrefab != null) { tags = string.Join(',', itemPrefab.Tags.Where(t => !removedTags.Contains(t)).Union(addedTags)); } } bool isNameTag = msg.ReadBoolean(); string writtenName = ""; if (isNameTag) { writtenName = msg.ReadString(); } if (!spawn) { return null; } //---------------------------------------- if (itemPrefab == null) { string errorMsg = "Failed to spawn item, prefab not found (name: " + (itemName ?? "null") + ", identifier: " + (itemIdentifier ?? "null") + ")"; errorMsg += "\n" + string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(cp => cp.Name)); GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:PrefabNotFound" + (itemName ?? "null") + (itemIdentifier ?? "null"), GameAnalyticsManager.ErrorSeverity.Critical, errorMsg); DebugConsole.ThrowError(errorMsg); return null; } Inventory inventory = null; if (inventoryId > 0) { var inventoryOwner = FindEntityByID(inventoryId); if (inventoryOwner is Character character) { inventory = character.Inventory; } else if (inventoryOwner is Item parentItem) { if (itemContainerIndex < 0 || itemContainerIndex >= parentItem.components.Count) { string errorMsg = $"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{parentItem.Prefab.Identifier} ({parentItem.ID})\" (component index out of range). Index: {itemContainerIndex}, components: {parentItem.components.Count}."; GameAnalyticsManager.AddErrorEventOnce("Item.ReadSpawnData:ContainerIndexOutOfRange" + (itemName ?? "null") + (itemIdentifier ?? "null"), GameAnalyticsManager.ErrorSeverity.Error, errorMsg); DebugConsole.ThrowError(errorMsg); inventory = parentItem.GetComponent()?.Inventory; } else if (parentItem.components[itemContainerIndex] is ItemContainer container) { inventory = container.Inventory; } } else if (inventoryOwner == null) { DebugConsole.ThrowError($"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of an entity with the ID {inventoryId} (entity not found)"); } else { DebugConsole.ThrowError($"Failed to spawn item \"{(itemIdentifier ?? "null")}\" in the inventory of \"{inventoryOwner} ({inventoryOwner.ID})\" (invalid entity, should be an item or a character)"); } } Item item = null; try { item = new Item(itemPrefab, pos, sub, id: itemId) { SpawnedInCurrentOutpost = spawnedInOutpost, AllowStealing = allowStealing, Quality = quality }; } catch (Exception e) { DebugConsole.ThrowError($"Failed to spawn item {itemPrefab.Name}", e); throw; } if (item.body != null) { item.body.BodyType = (BodyType)bodyType; } foreach (WifiComponent wifiComponent in item.GetComponents()) { wifiComponent.TeamID = (CharacterTeamType)teamID; } foreach (IdCard idCard in item.GetComponents()) { idCard.SubmarineSpecificID = submarineSpecificId; idCard.TeamID = (CharacterTeamType)teamID; idCard.OwnerName = ownerName; idCard.OwnerTags = ownerTags; idCard.OwnerBeardIndex = ownerBeardIndex; idCard.OwnerHairIndex = ownerHairIndex; idCard.OwnerMoustacheIndex = ownerMoustacheIndex; idCard.OwnerFaceAttachmentIndex = ownerFaceAttachmentIndex; idCard.OwnerHairColor = ownerHairColor; idCard.OwnerFacialHairColor = ownerFacialHairColor; idCard.OwnerSkinColor = ownerSkinColor; idCard.OwnerJobId = ownerJobId; idCard.OwnerSheetIndex = ownerSheetIndex; } if (descriptionChanged) { item.Description = itemDesc; } if (tagsChanged) { item.Tags = tags; } var nameTag = item.GetComponent(); if (nameTag != null) { nameTag.WrittenName = writtenName; } if (sub != null) { item.CurrentHull = Hull.FindHull(pos + sub.Position, null, true); item.Submarine = item.CurrentHull?.Submarine; } if (inventory != null) { if (inventorySlotIndex is >= 0 and < 255 && !inventory.TryPutItem(item, inventorySlotIndex, allowSwapping: false, allowCombine: false, user: null, createNetworkEvent: false, ignoreCondition: true) && inventory.IsSlotEmpty(inventorySlotIndex)) { //If the item won't go nicely, force it to the slot. If the server says the item is in the slot, it should go in the slot. //May happen e.g. when a character is configured to spawn with an item that won't normally go in its inventory slots. inventory.ForceToSlot(item, index: inventorySlotIndex); } else { inventory.TryPutItem(item, user: null, allowedSlots: item.AllowedSlots, createNetworkEvent: false); } item.SetTransform(inventory.Owner.SimPosition, 0.0f); item.Submarine = inventory.Owner.Submarine; if (inventory.Owner is Character { Enabled: false } && item.body != null) { item.body.Enabled = false; } } return item; } partial void RemoveProjSpecific() { if (Inventory.DraggingItems.Contains(this)) { Inventory.DraggingItems.Clear(); Inventory.DraggingSlot = null; } } public void OnPlayerSkillsChanged() { foreach (ItemComponent ic in components) { ic.OnPlayerSkillsChanged(); } } } }