#nullable enable using System; using System.Collections.Generic; using Barotrauma.IO; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; namespace Barotrauma { class EditorImageManager { private struct EditorImageContainer { public float Rotation; public float Scale; public Vector2 Position; public string Path; public float Opacity; public EditorImage.DrawTargetType DrawTarget; public EditorImage CreateImage() { return new EditorImage(Path, Position) { Position = Position, Scale = Scale, Opacity = Opacity, Rotation = Rotation, DrawTarget = DrawTarget }; } public static EditorImageContainer? Load(XElement element) { string path = element.GetAttributeString("path", ""); if (string.IsNullOrWhiteSpace(path)) { return null; } Vector2 pos = element.GetAttributeVector2("pos", Vector2.Zero); float scale = element.GetAttributeFloat("scale", 1f); float rotation = element.GetAttributeFloat("rotation", 0f); float opacity = element.GetAttributeFloat("opacity", 1f); string drawTargetString = element.GetAttributeString("drawtarget", ""); if (!Enum.TryParse(drawTargetString, out var drawTarget)) { drawTarget = EditorImage.DrawTargetType.World; } return new EditorImageContainer { Path = path, Rotation = rotation, Opacity = opacity, Position = pos, Scale = scale, DrawTarget = drawTarget }; } public static EditorImageContainer ImageToContainer(EditorImage img) { return new EditorImageContainer { Path = img.ImagePath, Rotation = img.Rotation, Position = img.Position, Opacity = img.Opacity, Scale = img.Scale, DrawTarget = img.DrawTarget }; } public static XElement SerializeImage(EditorImageContainer image) { return new XElement("image", new XAttribute("pos", XMLExtensions.Vector2ToString(image.Position)), new XAttribute("rotation", image.Rotation), new XAttribute("opacity", image.Opacity), new XAttribute("path", image.Path), new XAttribute("scale", image.Scale), new XAttribute("drawtarget", image.DrawTarget.ToString())); } } private readonly List PendingImages = new List(); public readonly List Images = new List(); private readonly List screenImages = new List(), worldImages = new List(); public bool EditorMode; private LocalizedString editModeText = ""; private Vector2 textSize = Vector2.Zero; public void Save(XElement element) { XElement saveElement = new XElement("editorimages"); foreach (EditorImage image in Images) { EditorImageContainer container = EditorImageContainer.ImageToContainer(image); saveElement.Add(EditorImageContainer.SerializeImage(container)); } foreach (EditorImageContainer container in PendingImages) { saveElement.Add(EditorImageContainer.SerializeImage(container)); } element.Add(saveElement); } public void Load(XElement element) { Clear(alsoPending: true); foreach (var subElement in element.Elements()) { EditorImageContainer? tempImage = EditorImageContainer.Load(subElement); if (tempImage != null) { PendingImages.Add(tempImage.Value); } } } public void OnEditorSelected() { editModeText = TextManager.Get("SubEditor.ImageEditingMode"); textSize = GUIStyle.LargeFont.MeasureString(editModeText); TryLoadPendingImages(); } private void TryLoadPendingImages() { if (PendingImages.Count == 0) { return; } Clear(alsoPending: false); foreach (EditorImageContainer pendingImage in PendingImages) { EditorImage img = pendingImage.CreateImage(); if (img.Image == null) { continue; } Images.Add(img); img.UpdateRectangle(); } UpdateImageCategories(); PendingImages.Clear(); } public void Clear(bool alsoPending = false) { foreach (EditorImage img in Images) { img.Image?.Dispose(); } Images.Clear(); screenImages.Clear(); worldImages.Clear(); if (alsoPending) { PendingImages.Clear(); } } public void Update(float deltaTime) { if (!EditorMode) { return; } foreach (EditorImage image in Images) { image.Update(deltaTime); } if (PlayerInput.PrimaryMouseButtonDown()) { EditorImage? hover = Images.FirstOrDefault(img => img.IsMouseOn()); if (hover != null) { foreach (EditorImage image in Images) { image.Selected = false; } hover.Selected = true; } } if (PlayerInput.KeyHit(Keys.Delete) || (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.D))) { Images.RemoveAll(img => img.Selected); UpdateImageCategories(); } if (PlayerInput.KeyHit(Keys.Space)) { foreach (EditorImage image in Images) { if (image.Selected) { if (image.DrawTarget == EditorImage.DrawTargetType.World) { Vector2 pos = image.Position; pos.Y = -pos.Y; pos = Screen.Selected.Cam.WorldToScreen(pos); if (PlayerInput.IsShiftDown()) { pos = new Vector2(GameMain.GraphicsWidth / 2f, GameMain.GraphicsHeight / 2f); } image.Position = pos; image.DrawTarget = EditorImage.DrawTargetType.Camera; image.Scale *= Screen.Selected.Cam.Zoom; image.UpdateRectangle(); } else { Vector2 pos = Screen.Selected.Cam.ScreenToWorld(image.Position); pos.Y = -pos.Y; image.Position = pos; image.DrawTarget = EditorImage.DrawTargetType.World; image.Scale /= Screen.Selected.Cam.Zoom; image.UpdateRectangle(); } } } UpdateImageCategories(); } MapEntity.DisableSelect = true; } private void UpdateImageCategories() { screenImages.Clear(); worldImages.Clear(); foreach (EditorImage image in Images) { switch (image.DrawTarget) { case EditorImage.DrawTargetType.World: worldImages.Add(image); break; default: screenImages.Add(image); break; } } } public void CreateImageWizard() { string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (!Directory.Exists(home)) { return; } FileSelection.OnFileSelected = file => { Vector2 pos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); pos.Y = -pos.Y; Images.Add(new EditorImage(file, pos) { DrawTarget = EditorImage.DrawTargetType.World }); UpdateImageCategories(); GameSettings.SaveCurrentConfig(); }; FileSelection.ClearFileTypeFilters(); FileSelection.AddFileTypeFilter("PNG", "*.png"); FileSelection.AddFileTypeFilter("JPEG", "*.jpg, *.jpeg"); FileSelection.AddFileTypeFilter("All files", "*.*"); FileSelection.SelectFileTypeFilter("*.png"); FileSelection.CurrentDirectory = home; FileSelection.Open = true; } public void DrawEditing(SpriteBatch spriteBatch, Camera cam) { if (!EditorMode) { return; } DrawImages(spriteBatch, cam); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); Vector2 textPos = new Vector2(GameMain.GraphicsWidth / 2f - (textSize.X / 2f), GameMain.GraphicsHeight / 10f - (textSize.Y / 2f)); GUI.DrawString(spriteBatch, textPos, editModeText, GUIStyle.Yellow, Color.Black * 0.4f, 8, GUIStyle.LargeFont); spriteBatch.End(); } public void Draw(SpriteBatch spriteBatch, Camera cam) { if (EditorMode) { return; } DrawImages(spriteBatch, cam); } private void DrawImages(SpriteBatch spriteBatch, Camera cam) { if (screenImages.Count > 0) { spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); foreach (EditorImage image in screenImages) { image.Draw(spriteBatch); if (EditorMode) { image.DrawEditing(spriteBatch, cam); } } spriteBatch.End(); } if (worldImages.Count > 0) { spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, transformMatrix: cam.Transform); foreach (EditorImage image in worldImages) { image.Draw(spriteBatch); if (EditorMode) { image.DrawEditing(spriteBatch, cam); } } spriteBatch.End(); } } } class EditorImage { public enum DrawTargetType { Camera, World } public Texture2D? Image; public string ImagePath; public Vector2 Position; public float Rotation; public float Opacity = 1f; public float Scale = 1f; public DrawTargetType DrawTarget; public bool Selected; public Rectangle Bounds; private float prevAngle; private bool disableMove; private bool isDragging; private readonly Dictionary widgets = new Dictionary(); public EditorImage(string path, Vector2 pos) { Image = Sprite.LoadTexture(path, compress: false); ImagePath = path; Position = pos; UpdateRectangle(); } public bool IsMouseOn() => Bounds.Contains(GetMousePos()); public Vector2 GetMousePos() { switch (DrawTarget) { case DrawTargetType.Camera: return PlayerInput.MousePosition; case DrawTargetType.World: Vector2 pos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition); pos.Y = -pos.Y; return pos; default: return PlayerInput.MousePosition; } } public void Update(float deltaTime) { if (!Selected) { return; } if (widgets.Values.Any(w => w.IsSelected)) { return; } if (PlayerInput.PrimaryMouseButtonDown() && !disableMove && IsMouseOn()) { isDragging = true; } if (isDragging) { Camera cam = Screen.Selected.Cam; if (PlayerInput.MouseSpeed != Vector2.Zero) { Vector2 mouseSpeed = PlayerInput.MouseSpeed; if (DrawTarget == DrawTargetType.World) { mouseSpeed /= cam.Zoom; } Position += mouseSpeed; UpdateRectangle(); } } if (PlayerInput.KeyDown(Keys.OemPlus) || PlayerInput.KeyDown(Keys.Up)) { Opacity += 0.01f; } if (PlayerInput.KeyDown(Keys.OemMinus) || PlayerInput.KeyDown(Keys.Down)) { Opacity -= 0.01f; } if (PlayerInput.KeyHit(Keys.D0)) { Opacity = 1f; } Opacity = Math.Clamp(Opacity, 0, 1f); if (!PlayerInput.PrimaryMouseButtonHeld()) { isDragging = false; } } private void DrawWidgets(SpriteBatch spriteBatch) { float widgetSize = Image == null ? 100f : Math.Max(Image.Width, Image.Height) / 2f; int width = 3; int size = 32; if (DrawTarget == DrawTargetType.World) { width = Math.Max(width, (int) (width / Screen.Selected.Cam.Zoom)); } Widget currentWidget = GetWidget("transform", size, width, widget => { widget.MouseDown += () => { widget.Color = GUIStyle.Green; prevAngle = Rotation; disableMove = true; }; widget.Deselected += () => { widget.Color = Color.Yellow; disableMove = false; }; widget.MouseHeld += (deltaTime) => { Rotation = GetRotationAngle(Position) + (float) Math.PI / 2f; float distance = Vector2.Distance(Position, GetMousePos()); Scale = Math.Abs(distance) / widgetSize; if (PlayerInput.IsShiftDown()) { const float rotationStep = (float) (Math.PI / 4f); Rotation = (float) Math.Round(Rotation / rotationStep) * rotationStep; } if (PlayerInput.IsCtrlDown()) { const float scaleStep = 0.1f; Scale = (float) Math.Round(Scale / scaleStep) * scaleStep; } UpdateRectangle(); }; widget.PreUpdate += (deltaTime) => { if (DrawTarget != DrawTargetType.World) { return; } widget.DrawPos = new Vector2(widget.DrawPos.X, -widget.DrawPos.Y); widget.DrawPos = Screen.Selected.Cam.WorldToScreen(widget.DrawPos); }; widget.PostUpdate += (deltaTime) => { if (DrawTarget != DrawTargetType.World) { return; } widget.DrawPos = Screen.Selected.Cam.ScreenToWorld(widget.DrawPos); widget.DrawPos = new Vector2(widget.DrawPos.X, -widget.DrawPos.Y); }; widget.PreDraw += (sprtBtch, deltaTime) => { widget.Tooltip = $"Scale: {Math.Round(Scale, 2)}\n" + $"Rotation: {(int) MathHelper.ToDegrees(Rotation)}"; float rotation = Rotation - (float) Math.PI / 2f; widget.DrawPos = Position + new Vector2((float) Math.Cos(rotation), (float) Math.Sin(rotation)) * (Scale * widgetSize); widget.Update(deltaTime); }; }); currentWidget.Draw(spriteBatch, (float) Timing.Step); GUI.DrawLine(spriteBatch, Position, currentWidget.DrawPos, GUIStyle.Green, width: width); } private float GetRotationAngle(Vector2 drawPosition) { Vector2 rotationVector = GetMousePos() - drawPosition; rotationVector.Normalize(); double angle = Math.Atan2(MathHelper.ToRadians(rotationVector.Y), MathHelper.ToRadians(rotationVector.X)); if (angle < 0) { angle = Math.Abs(angle - prevAngle) < Math.Abs((angle + Math.PI * 2) - prevAngle) ? angle : angle + Math.PI * 2; } else if (angle > 0) { angle = Math.Abs(angle - prevAngle) < Math.Abs((angle - Math.PI * 2) - prevAngle) ? angle : angle - Math.PI * 2; } angle = MathHelper.Clamp((float) angle, -((float) Math.PI * 2), (float) Math.PI * 2); prevAngle = (float) angle; return (float) angle; } private Widget GetWidget(string id, int size, float thickness = 1f, Action? initMethod = null) { if (!widgets.TryGetValue(id, out Widget? widget)) { widget = new Widget(id, size, WidgetShape.Rectangle) { Color = Color.Yellow, RequireMouseOn = false }; widgets.Add(id, widget); initMethod?.Invoke(widget); } widget.Size = size; widget.Thickness = thickness; return widget; } public void UpdateRectangle() { if (Image == null) { Bounds = new Rectangle((int) Position.X, (int) Position.Y, 512, 512); return; } Vector2 size = new Vector2(Image.Width * Scale, Image.Height * Scale); Bounds = new Rectangle((Position - size / 2f).ToPoint(), size.ToPoint()); } public void Draw(SpriteBatch spriteBatch) { if (Image == null) { return; } spriteBatch.Draw(Image, Position, null, Color.White * Opacity, Rotation, new Vector2(Image.Width / 2f, Image.Height / 2f), scale: Scale, SpriteEffects.None, 0f); } public void DrawEditing(SpriteBatch spriteBatch, Camera cam) { Rectangle bounds = Bounds; int width = 4; if (DrawTarget == DrawTargetType.World) { width = (int) (width / cam.Zoom); } GUI.DrawRectangle(spriteBatch, bounds, Selected ? GUIStyle.Red : GUIStyle.Green, thickness: width); if (Selected) { DrawWidgets(spriteBatch); } } } }