using Barotrauma.Extensions; using Barotrauma.IO; using Barotrauma.Items.Components; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; namespace Barotrauma { sealed class SubmarinePreview : IDisposable { private readonly SubmarineInfo submarineInfo; private SpriteRecorder spriteRecorder; private Camera camera; private Task loadTask; private (Vector2 Min, Vector2 Max) bounds; private volatile bool isDisposed; private GUIFrame previewFrame; private sealed class LoadedHull { public UInt16 ID; public readonly ImmutableList LinkedHulls; public readonly Rectangle Rect; public readonly Identifier NameIdentifier; public LoadedHull(XElement element) { ID = (ushort)element.GetAttributeInt("id", Entity.NullEntityID); NameIdentifier = element.GetAttributeIdentifier("roomname", ""); Rect = element.GetAttributeRect("rect", Rectangle.Empty); Rect.Y = -Rect.Y; LinkedHulls = element.GetAttributeUshortArray("linked", Array.Empty()).ToImmutableList(); } } private sealed class HullCollection { public readonly List Hulls = new List(); public readonly List Rects = new List(); public readonly LocalizedString Name; public HullCollection(LoadedHull hull) { Name = TextManager.Get(hull.NameIdentifier).Fallback(hull.NameIdentifier.ToString()); AddHull(hull); } public void AddHull(LoadedHull hull) { Hulls.Add(hull); Rects.Add(hull.Rect); } private bool Contains(UInt16 hullId) { return Hulls.Any(h => h.ID == hullId); } public bool IsLinkedTo(HullCollection other) { return Hulls.Any(h => h.LinkedHulls.Any(id => other.Contains(id))) || other.Hulls.Any(h => h.LinkedHulls.Any(id => Contains(id))); } } private readonly struct Door { public readonly Rectangle Rect; public Door(Rectangle rect) { rect.Y = -rect.Y; Rect = rect; } } private readonly List hullCollections = new List(); private readonly List doors; private static SubmarinePreview instance = null; public static void Create(SubmarineInfo submarineInfo) { Close(); instance = new SubmarinePreview(submarineInfo); } public static void Close() { instance?.Dispose(); instance = null; } private SubmarinePreview(SubmarineInfo subInfo) { camera = new Camera(); submarineInfo = subInfo; spriteRecorder = new SpriteRecorder(); isDisposed = false; loadTask = null; hullCollections = new List(); doors = new List(); previewFrame = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null); new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, previewFrame.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker"); new GUIButton(new RectTransform(Vector2.One, previewFrame.RectTransform), "", style: null) { OnClicked = (btn, obj) => { Dispose(); return false; } }; var innerFrame = new GUIFrame(new RectTransform(Vector2.One * 0.9f, previewFrame.RectTransform, Anchor.Center)); int innerPadding = GUI.IntScale(100f); var innerPadded = new GUIFrame(new RectTransform(new Point(innerFrame.Rect.Width - innerPadding, innerFrame.Rect.Height - innerPadding), previewFrame.RectTransform, Anchor.Center), style: null) { OutlineColor = Color.Black, OutlineThickness = 2 }; GUITextBlock titleText = null; GUIListBox specsContainer = null; new GUICustomComponent(new RectTransform(Vector2.One, innerPadded.RectTransform, Anchor.Center), (spriteBatch, component) => { if (isDisposed) { return; } camera.UpdateTransform(interpolate: true, updateListener: false); Rectangle drawRect = new Rectangle(component.Rect.X + 1, component.Rect.Y + 1, component.Rect.Width - 2, component.Rect.Height - 2); RenderSubmarine(spriteBatch, drawRect, component); }, (deltaTime, component) => { if (isDisposed) { return; } bool isMouseOnComponent = GUI.MouseOn == component; camera.MoveCamera(deltaTime, allowZoom: isMouseOnComponent, followSub: false); if (isMouseOnComponent && (PlayerInput.MidButtonHeld() || PlayerInput.PrimaryMouseButtonHeld())) { Vector2 moveSpeed = PlayerInput.MouseSpeed * (float)deltaTime * 60.0f / camera.Zoom; moveSpeed.X = -moveSpeed.X; camera.Position += moveSpeed; } if (titleText != null && specsContainer != null) { specsContainer.Visible = GUI.IsMouseOn(titleText); } if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.Escape)) { Dispose(); } }); var topContainer = new GUIFrame(new RectTransform(new Vector2(1f, 0.07f), innerPadded.RectTransform, Anchor.TopLeft), style: null) { Color = Color.Black * 0.65f }; var topLayout = new GUILayoutGroup(new RectTransform(new Vector2(0.97f, 5f / 7f), topContainer.RectTransform, Anchor.Center), isHorizontal: true, childAnchor: Anchor.CenterLeft); titleText = new GUITextBlock(new RectTransform(new Vector2(0.95f, 1f), topLayout.RectTransform), subInfo.DisplayName, font: GUIStyle.LargeFont); new GUIButton(new RectTransform(new Vector2(0.05f, 1f), topLayout.RectTransform), TextManager.Get("Close")) { OnClicked = (btn, obj) => { Dispose(); return false; } }; specsContainer = new GUIListBox(new RectTransform(new Vector2(0.4f, 1f), innerPadded.RectTransform, Anchor.TopLeft) { RelativeOffset = new Vector2(0.015f, 0.07f) }) { CurrentSelectMode = GUIListBox.SelectMode.None, Color = Color.Black * 0.65f, ScrollBarEnabled = false, ScrollBarVisible = false, Spacing = GUI.IntScale(5) }; subInfo.CreateSpecsWindow(specsContainer, GUIStyle.Font, includeTitle: false, includeDescription: true); int width = specsContainer.Rect.Width; void recalculateSpecsContainerHeight() { int totalSize = 0; var children = specsContainer.Content.Children.Where(c => c.Visible); foreach (GUIComponent child in children) { totalSize += child.Rect.Height; } totalSize += specsContainer.Content.CountChildren * specsContainer.Spacing; if (specsContainer.PadBottom) { GUIComponent last = specsContainer.Content.Children.LastOrDefault(); if (last != null) { totalSize += specsContainer.Rect.Height - last.Rect.Height; } } specsContainer.RectTransform.Resize(new Point(width, totalSize), true); specsContainer.RecalculateChildren(); } //hell recalculateSpecsContainerHeight(); specsContainer.Content.GetAllChildren().ForEach(c => { var firstChild = c.Children.FirstOrDefault() as GUITextBlock; if (firstChild != null) { firstChild.CalculateHeightFromText(); firstChild.SetTextPos(); c.RectTransform.MinSize = new Point(0, firstChild.Rect.Height); } c.CalculateHeightFromText(); c.SetTextPos(); }); recalculateSpecsContainerHeight(); TaskPool.Add(nameof(GeneratePreviewMeshes), GeneratePreviewMeshes(), _ => { if (isDisposed) { return; } // Reset the camera's position on the main thread, // because the Camera class is not thread-safe and // it's possible for its state to not get updated // properly if done within a task camera.Position = (bounds.Min + bounds.Max) * (0.5f, -0.5f); Vector2 span2d = bounds.Max - bounds.Min; Vector2 scaledSpan2d = span2d / camera.Resolution.ToVector2(); float scaledSpan = Math.Max(scaledSpan2d.X, scaledSpan2d.Y); camera.MinZoom = Math.Min(0.1f, 0.4f / scaledSpan); camera.Zoom = 0.7f / scaledSpan; camera.StopMovement(); camera.UpdateTransform(interpolate: false, updateListener: false); }); } public static void AddToGUIUpdateList() { instance?.previewFrame?.AddToGUIUpdateList(); } public Task GeneratePreviewMeshes() { if (loadTask != null) { throw new InvalidOperationException("Tried to start SubmarinePreview loadTask more than once!"); } loadTask = Task.Run(GeneratePreviewMeshesInternal); return loadTask; } private async Task GeneratePreviewMeshesInternal() { await Task.Yield(); spriteRecorder.Begin(SpriteSortMode.BackToFront); HashSet toIgnore = new HashSet(); HashSet wires = new HashSet(); foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) { case "item": foreach (var component in subElement.Elements()) { switch (component.Name.LocalName.ToLowerInvariant()) { case "itemcontainer": ExtractItemContainerIds(component, toIgnore); break; case "connectionpanel": ExtractConnectionPanelLinks(component, wires); break; } } break; } if (isDisposed) { return; } await Task.Yield(); } var wireNodes = new List(); List loadedHulls = new List(); foreach (var subElement in submarineInfo.SubmarineElement.Elements()) { if (subElement.GetAttributeBool("hiddeningame", false)) { continue; } switch (subElement.Name.LocalName.ToLowerInvariant()) { case "structure": case "item": var id = subElement.GetAttributeInt("ID", 0); if (wires.Contains(id)) { wireNodes.Add(subElement); } else if (!toIgnore.Contains(id)) { BakeMapEntity(subElement); } break; case "hull": Identifier identifier = subElement.GetAttributeIdentifier("roomname", ""); if (!identifier.IsEmpty) { loadedHulls.Add(new LoadedHull(subElement)); } break; } if (isDisposed) { return; } await Task.Yield(); } //List tempHullCollections = new List(); foreach (LoadedHull hull in loadedHulls) { hullCollections.Add(new HullCollection(hull)); } bool intersectionFound; do { intersectionFound = false; for (int i = 0; i < hullCollections.Count; i++) { for (int j = i + 1; j < hullCollections.Count; j++) { var collection1 = hullCollections[i]; var collection2 = hullCollections[j]; if (collection1.IsLinkedTo(collection2)) { collection2.Hulls.ForEach(h => collection1.AddHull(h)); hullCollections.Remove(collection2); intersectionFound = true; break; } } if (intersectionFound) { break; } } } while (intersectionFound); bounds = (spriteRecorder.Min, spriteRecorder.Max); wireNodes.ForEach(BakeWireNodes); spriteRecorder.End(); } private static void ExtractItemContainerIds(XElement component, HashSet ids) { string containedString = component.GetAttributeString("contained", ""); string[] itemIdStrings = containedString.Split(','); for (int i = 0; i < itemIdStrings.Length; i++) { foreach (string idStr in itemIdStrings[i].Split(';')) { if (!int.TryParse(idStr, NumberStyles.Any, CultureInfo.InvariantCulture, out int id)) { continue; } if (id != 0 && !ids.Contains(id)) { ids.Add(id); } } } } private static void ExtractConnectionPanelLinks(XElement component, HashSet ids) { var pins = component.Elements("input").Concat(component.Elements("output")); foreach (var pin in pins) { var links = pin.Elements("link"); foreach (var link in links) { int id = link.GetAttributeInt("w", 0); if (id != 0 && !ids.Contains(id)) { ids.Add(id); } } } } private void BakeWireNodes(XElement element) { var prefabIdentifier = element.GetAttributeIdentifier("identifier", ""); if (prefabIdentifier.IsEmpty) { return; } if (!ItemPrefab.Prefabs.TryGet(prefabIdentifier, out var prefab)) { return; } var prefabWireComponentElement = prefab.ConfigElement.GetChildElement("wire"); if (prefabWireComponentElement is null) { return; } var wireComponent = element.GetChildElement("wire"); if (wireComponent is null) { return; } var color = element.GetAttributeColor("spritecolor") ?? Color.White; var nodes = Wire.ExtractNodes(wireComponent).ToImmutableArray(); var wireSprite = Wire.ExtractWireSprite(prefab.ConfigElement); var useSpriteDepth = element.GetAttributeBool("usespritedepth", false); var depth = useSpriteDepth ? element.GetAttributeFloat("spritedepth", 1.0f) : wireSprite.Depth; var width = prefabWireComponentElement.GetAttributeFloat("width", 0.3f); for (int i = 0; i < nodes.Length - 1; i++) { var line = (Start: nodes[i], End: nodes[i + 1]); var wireSegment = new Wire.WireSection(line.Start, line.End); wireSegment.Draw(spriteRecorder, wireSprite, color, Vector2.Zero, depth, width); } } private void BakeMapEntity(XElement element) { Identifier identifier = element.GetAttributeIdentifier("identifier", Identifier.Empty); if (identifier.IsEmpty) { return; } Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty); if (rect.Equals(Rectangle.Empty)) { return; } float depth = element.GetAttributeFloat("spritedepth", 1f); bool flippedX = element.GetAttributeBool("flippedx", false); bool flippedY = element.GetAttributeBool("flippedy", false); float scale = element.GetAttributeFloat("scale", 1f); Color color = element.GetAttributeColor("spritecolor", Color.White); float rotationRad = MathHelper.ToRadians(element.GetAttributeFloat("rotation", 0f)); MapEntityPrefab prefab; if (element.NameAsIdentifier() == "item" && ItemPrefab.Prefabs.TryGet(identifier, out ItemPrefab ip)) { prefab = ip; } else { prefab = MapEntityPrefab.FindByIdentifier(identifier); } if (prefab == null) { return; } flippedX &= prefab.CanSpriteFlipX; flippedY &= prefab.CanSpriteFlipY; SpriteEffects spriteEffects = SpriteEffects.None; if (flippedX) { spriteEffects |= SpriteEffects.FlipHorizontally; } if (flippedY) { spriteEffects |= SpriteEffects.FlipVertically; } var prevEffects = prefab.Sprite.effects; prefab.Sprite.effects ^= spriteEffects; bool overrideSprite = false; ItemPrefab itemPrefab = prefab as ItemPrefab; if (itemPrefab != null) { BakeItemComponents(itemPrefab, rect, color, scale, rotationRad, depth, out overrideSprite); } if (!overrideSprite) { if (prefab is StructurePrefab structurePrefab) { ParseUpgrades(structurePrefab.ConfigElement, ref scale); if (!prefab.ResizeVertical) { rect.Height = (int)(rect.Height * scale / prefab.Scale); } if (!prefab.ResizeHorizontal) { rect.Width = (int)(rect.Width * scale / prefab.Scale); } var textureScale = element.GetAttributeVector2("texturescale", Vector2.One); Vector2 backGroundOffset = Vector2.Zero; Vector2 textureOffset = element.GetAttributeVector2("textureoffset", Vector2.Zero); textureOffset = Structure.UpgradeTextureOffset( targetSize: rect.Size.ToVector2(), originalTextureOffset: textureOffset, submarineInfo: submarineInfo, sourceRect: prefab.Sprite.SourceRect, scale: textureScale * scale, flippedX: flippedX, flippedY: flippedY); backGroundOffset = new Vector2( MathUtils.PositiveModulo(-textureOffset.X, prefab.Sprite.SourceRect.Width * textureScale.X * scale), MathUtils.PositiveModulo(-textureOffset.Y, prefab.Sprite.SourceRect.Height * textureScale.Y * scale)); prefab.Sprite.DrawTiled( spriteBatch: spriteRecorder, position: new Vector2(rect.X + rect.Width / 2, -(rect.Y - rect.Height / 2)), targetSize: rect.Size.ToVector2(), origin: rect.Size.ToVector2() * new Vector2(0.5f, 0.5f), color: color, startOffset: backGroundOffset, textureScale: textureScale * scale, depth: depth, rotation: rotationRad); } else if (itemPrefab != null) { bool usePrefabValues = element.GetAttributeBool("isoverride", false) != itemPrefab.IsOverride; if (usePrefabValues) { scale = itemPrefab.ConfigElement.GetAttributeFloat(scale, "scale", "Scale"); } ParseUpgrades(itemPrefab.ConfigElement, ref scale); if (prefab.ResizeVertical || prefab.ResizeHorizontal) { if (!prefab.ResizeHorizontal) { rect.Width = (int)(prefab.Sprite.size.X * scale); } if (!prefab.ResizeVertical) { rect.Height = (int)(prefab.Sprite.size.Y * scale); } var spritePos = rect.Center.ToVector2(); //spritePos.Y = rect.Height - spritePos.Y; prefab.Sprite.DrawTiled( spriteRecorder, rect.Location.ToVector2() * new Vector2(1f, -1f), rect.Size.ToVector2(), color: color, textureScale: Vector2.One * scale, depth: depth); foreach (var decorativeSprite in itemPrefab.DecorativeSprites) { float offsetState = 0f; Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; if (flippedX) { offset.X = -offset.X; } if (flippedY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.DrawTiled(spriteRecorder, new Vector2(spritePos.X + offset.X - rect.Width / 2, -(spritePos.Y + offset.Y + rect.Height / 2)), rect.Size.ToVector2(), color: color, textureScale: Vector2.One * scale, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); } } else { rect.Width = (int)(rect.Width * scale / prefab.Scale); rect.Height = (int)(rect.Height * scale / prefab.Scale); var spritePos = rect.Center.ToVector2(); spritePos.Y -= rect.Height; //spritePos.Y = rect.Height - spritePos.Y; prefab.Sprite.Draw( spriteRecorder, spritePos * new Vector2(1f, -1f), color, prefab.Sprite.Origin, rotationRad, scale, prefab.Sprite.effects, depth); foreach (var decorativeSprite in itemPrefab.DecorativeSprites) { float rotationState = 0f; float offsetState = 0f; float rot = decorativeSprite.GetRotation(ref rotationState, 0f); Vector2 offset = decorativeSprite.GetOffset(ref offsetState, Vector2.Zero) * scale; if (flippedX) { offset.X = -offset.X; } if (flippedY) { offset.Y = -offset.Y; } decorativeSprite.Sprite.Draw(spriteRecorder, new Vector2(spritePos.X + offset.X, -(spritePos.Y + offset.Y)), color, rotationRad + rot, decorativeSprite.GetScale(0f) * scale, prefab.Sprite.effects, depth: Math.Min(depth + (decorativeSprite.Sprite.Depth - prefab.Sprite.Depth), 0.999f)); } } } } prefab.Sprite.effects = prevEffects; } private void BakeItemComponents( ItemPrefab prefab, Rectangle rect, Color color, float scale, float rotationRad, float depth, out bool overrideSprite) { overrideSprite = false; float relativeScale = scale / prefab.Scale; foreach (var subElement in prefab.ConfigElement.Elements()) { switch (subElement.Name.LocalName.ToLowerInvariant()) { case "turret": Sprite barrelSprite = null; Sprite railSprite = null; foreach (var turretSubElem in subElement.Elements()) { switch (turretSubElem.Name.ToString().ToLowerInvariant()) { case "barrelsprite": barrelSprite = new Sprite(turretSubElem); break; case "railsprite": railSprite = new Sprite(turretSubElem); break; } } Vector2 barrelPos = subElement.GetAttributeVector2("barrelpos", Vector2.Zero); Vector2 relativeBarrelPos = barrelPos * prefab.Scale - new Vector2(rect.Width / 2, rect.Height / 2); var transformedBarrelPos = MathUtils.RotatePoint( relativeBarrelPos, rotationRad); Vector2 drawPos = new Vector2(rect.X + rect.Width * relativeScale / 2 + transformedBarrelPos.X * relativeScale, rect.Y - rect.Height * relativeScale / 2 - transformedBarrelPos.Y * relativeScale); drawPos.Y = -drawPos.Y; railSprite?.Draw(spriteRecorder, drawPos, color, rotationRad, scale, SpriteEffects.None, depth + (railSprite.Depth - prefab.Sprite.Depth)); barrelSprite?.Draw(spriteRecorder, drawPos, color, rotationRad, scale, SpriteEffects.None, depth + (barrelSprite.Depth - prefab.Sprite.Depth)); break; case "door": var scaledRect = rect with { Size = (rect.Size.ToVector2() * relativeScale).ToPoint() }; doors.Add(new Door(scaledRect)); var doorSpriteElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("sprite", StringComparison.OrdinalIgnoreCase)); if (doorSpriteElem != null) { string texturePath = doorSpriteElem.GetAttributeStringUnrestricted("texture", ""); Vector2 pos = scaledRect.Location.ToVector2() * new Vector2(1f, -1f); if (subElement.GetAttributeBool("horizontal", false)) { pos.Y += (float)scaledRect.Height * 0.5f; } else { pos.X += (float)scaledRect.Width * 0.5f; } Sprite doorSprite = new Sprite(doorSpriteElem, texturePath.Contains("/") ? "" : Path.GetDirectoryName(prefab.FilePath)); spriteRecorder.Draw(doorSprite.Texture, pos, new Rectangle((int)doorSprite.SourceRect.X, (int)doorSprite.SourceRect.Y, (int)doorSprite.size.X, (int)doorSprite.size.Y), color, 0.0f, doorSprite.Origin, new Vector2(scale), SpriteEffects.None, doorSprite.Depth); } break; case "ladder": var backgroundSprElem = subElement.Elements().FirstOrDefault(e => e.Name.LocalName.Equals("backgroundsprite", StringComparison.OrdinalIgnoreCase)); if (backgroundSprElem != null) { Sprite backgroundSprite = new Sprite(backgroundSprElem); backgroundSprite.DrawTiled(spriteRecorder, new Vector2(rect.Left, -rect.Top) - backgroundSprite.Origin * scale, new Vector2(backgroundSprite.size.X * scale, rect.Height), color: color, textureScale: Vector2.One * scale, depth: depth + 0.1f); } break; } } } private void ParseUpgrades(XElement prefabConfigElement, ref float scale) { foreach (var upgrade in prefabConfigElement.Elements("Upgrade")) { var upgradeVersion = new Version(upgrade.GetAttributeString("gameversion", "0.0.0.0")); if (upgradeVersion >= submarineInfo.GameVersion) { string scaleModifier = upgrade.GetAttributeString("scale", "*1"); if (scaleModifier.StartsWith("*")) { if (float.TryParse(scaleModifier.Substring(1), NumberStyles.Any, CultureInfo.InvariantCulture, out float parsedScale)) { scale *= parsedScale; } } else { if (float.TryParse(scaleModifier, NumberStyles.Any, CultureInfo.InvariantCulture, out float parsedScale)) { scale = parsedScale; } } } } } private void RenderSubmarine(SpriteBatch spriteBatch, Rectangle scissorRectangle, GUIComponent component) { if (spriteRecorder == null) { return; } GUI.DrawRectangle(spriteBatch, scissorRectangle, new Color(0.051f, 0.149f, 0.271f, 1.0f), isFilled: true); if (!spriteRecorder.ReadyToRender) { LocalizedString waitText = !loadTask.IsCompleted ? TextManager.Get("generatingsubmarinepreview", "loading") : (loadTask.Exception?.ToString() ?? "Task completed without marking as ready to render"); Vector2 origin = (GUIStyle.Font.MeasureString(waitText) * 0.5f); origin.X = MathF.Round(origin.X); origin.Y = MathF.Round(origin.Y); GUIStyle.Font.DrawString( spriteBatch, waitText, scissorRectangle.Center.ToVector2(), Color.White, 0f, origin, 1f, SpriteEffects.None, 0f); return; } spriteBatch.End(); var prevScissorRect = GameMain.Instance.GraphicsDevice.ScissorRectangle; GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; var prevRasterizerState = GameMain.Instance.GraphicsDevice.RasterizerState; GameMain.Instance.GraphicsDevice.RasterizerState = GameMain.ScissorTestEnable; spriteRecorder.Render(camera); var mousePos = camera.ScreenToWorld(PlayerInput.MousePosition); mousePos.Y = -mousePos.Y; spriteBatch.Begin(SpriteSortMode.BackToFront, rasterizerState: GameMain.ScissorTestEnable, transformMatrix: camera.Transform); GameMain.Instance.GraphicsDevice.ScissorRectangle = scissorRectangle; foreach (var hullCollection in hullCollections) { bool mouseOver = false; if (GUI.MouseOn == null || GUI.MouseOn == component) { foreach (var rect in hullCollection.Rects) { mouseOver = rect.Contains(mousePos); if (mouseOver) { break; } } } foreach (var rect in hullCollection.Rects) { GUI.DrawRectangle(spriteBatch, rect, mouseOver ? Color.Red : Color.Blue, depth: mouseOver ? 0.45f : 0.5f, thickness: (mouseOver ? 4f : 2f) / camera.Zoom); } if (mouseOver) { LocalizedString str = hullCollection.Name; Vector2 strSize = GUIStyle.Font.MeasureString(str) / camera.Zoom; Vector2 padding = new Vector2(30, 30) / camera.Zoom; Vector2 shift = new Vector2(10, 0) / camera.Zoom; GUI.DrawRectangle(spriteBatch, mousePos + shift, strSize + padding, Color.Black, isFilled: true, depth: 0.25f); GUIStyle.Font.DrawString(spriteBatch, str, mousePos + shift + (strSize + padding) * 0.5f, Color.White, 0f, strSize * camera.Zoom * 0.5f, 1f / camera.Zoom, SpriteEffects.None, 0f); } } foreach (var door in doors) { GUI.DrawRectangle(spriteBatch, door.Rect, GUIStyle.Green * 0.5f, isFilled: true, depth: 0.4f); } spriteBatch.End(); GameMain.Instance.GraphicsDevice.ScissorRectangle = prevScissorRect; GameMain.Instance.GraphicsDevice.RasterizerState = prevRasterizerState; spriteBatch.Begin(SpriteSortMode.Deferred); } public void Dispose() { if (previewFrame != null) { previewFrame.RectTransform.Parent = null; previewFrame = null; } spriteRecorder?.Dispose(); spriteRecorder = null; camera = null; isDisposed = true; } } }