Client LuaCsForBarotrauma
GUI.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Diagnostics;
4 using Barotrauma.IO;
5 using System.Linq;
10 using Barotrauma.Sounds;
11 using EventInput;
12 using FarseerPhysics;
13 using Microsoft.Xna.Framework;
14 using Microsoft.Xna.Framework.Graphics;
15 using Microsoft.Xna.Framework.Input;
16 using System.Collections.Immutable;
17 
18 namespace Barotrauma
19 {
20  public enum GUISoundType
21  {
22  UIMessage,
26  Select,
27  PickItem,
29  DropItem,
30  PopupMenu,
31  Decrease,
32  Increase,
33  UISwitch,
34  TickBox,
36  Cart,
37  }
38 
39  public enum CursorState
40  {
41  Default = 0, // Cursor
42  Hand = 1, // Hand with a finger
43  Move = 2, // arrows pointing to all directions
44  IBeam = 3, // Text
45  Dragging = 4,// Closed hand
46  Waiting = 5, // Hourglass
47  WaitingBackground = 6, // Cursor + Hourglass
48  }
49 
50  static class GUI
51  {
52  // Controls where a line is drawn for given coords.
53  public enum OutlinePosition
54  {
55  Default = 0, // Thickness is inside of top left and outside of bottom right coord
56  Inside = 1, // Thickness is subtracted from the inside
57  Centered = 2, // Thickness is centered on given coords
58  Outside = 3, // Tickness is added to the outside
59  }
60  public static GUICanvas Canvas => GUICanvas.Instance;
61  public static CursorState MouseCursor = CursorState.Default;
62 
63  public static readonly SamplerState SamplerState = new SamplerState()
64  {
65  Filter = TextureFilter.Linear,
66  AddressU = TextureAddressMode.Wrap,
67  AddressV = TextureAddressMode.Wrap,
68  AddressW = TextureAddressMode.Wrap,
69  BorderColor = Color.White,
70  MaxAnisotropy = 4,
71  MaxMipLevel = 0,
72  MipMapLevelOfDetailBias = -0.8f,
73  ComparisonFunction = CompareFunction.Never,
74  FilterMode = TextureFilterMode.Default,
75  };
76 
77  public static readonly SamplerState SamplerStateClamp = new SamplerState()
78  {
79  Filter = TextureFilter.Linear,
80  AddressU = TextureAddressMode.Clamp,
81  AddressV = TextureAddressMode.Clamp,
82  AddressW = TextureAddressMode.Clamp,
83  BorderColor = Color.White,
84  MaxAnisotropy = 4,
85  MaxMipLevel = 0,
86  MipMapLevelOfDetailBias = -0.8f,
87  ComparisonFunction = CompareFunction.Never,
88  FilterMode = TextureFilterMode.Default,
89  };
90 
91  public static readonly string[] VectorComponentLabels = { "X", "Y", "Z", "W" };
92  public static readonly string[] RectComponentLabels = { "X", "Y", "W", "H" };
93  public static readonly string[] ColorComponentLabels = { "R", "G", "B", "A" };
94 
95  private static readonly object mutex = new object();
96 
97  public static readonly Vector2 ReferenceResolution = new Vector2(1920f, 1080f);
98  public static float Scale => (UIWidth / ReferenceResolution.X + GameMain.GraphicsHeight / ReferenceResolution.Y) / 2.0f * GameSettings.CurrentConfig.Graphics.HUDScale;
99  public static float xScale => UIWidth / ReferenceResolution.X * GameSettings.CurrentConfig.Graphics.HUDScale;
100  public static float yScale => GameMain.GraphicsHeight / ReferenceResolution.Y * GameSettings.CurrentConfig.Graphics.HUDScale;
101  public static int IntScale(float f) => (int)(f * Scale);
102  public static int IntScaleFloor(float f) => (int)Math.Floor(f * Scale);
103  public static int IntScaleCeiling(float f) => (int)Math.Ceiling(f * Scale);
104  public static float AdjustForTextScale(float f) => f * GameSettings.CurrentConfig.Graphics.TextScale;
105  public static float HorizontalAspectRatio => GameMain.GraphicsWidth / (float)GameMain.GraphicsHeight;
106  public static float VerticalAspectRatio => GameMain.GraphicsHeight / (float)GameMain.GraphicsWidth;
107  public static float RelativeHorizontalAspectRatio => HorizontalAspectRatio / (ReferenceResolution.X / ReferenceResolution.Y);
108  public static float RelativeVerticalAspectRatio => VerticalAspectRatio / (ReferenceResolution.Y / ReferenceResolution.X);
112  public static float AspectRatioAdjustment => HorizontalAspectRatio < 1.4f ? (1.0f - (1.4f - HorizontalAspectRatio)) : 1.0f;
113 
114  public static bool IsUltrawide => HorizontalAspectRatio > 2.3f;
115 
116  public static int UIWidth
117  {
118  get
119  {
120  if (IsUltrawide)
121  {
122  return (int)(GameMain.GraphicsHeight * ReferenceResolution.X / ReferenceResolution.Y);
123  }
124  else
125  {
126  return GameMain.GraphicsWidth;
127  }
128  }
129  }
130 
131  public static float SlicedSpriteScale
132  {
133  get
134  {
135  if (Math.Abs(1.0f - Scale) < 0.1f)
136  {
137  //don't scale if very close to the "reference resolution"
138  return 1.0f;
139  }
140  return Scale;
141  }
142  }
143 
144  private static Texture2D solidWhiteTexture;
145  public static Texture2D WhiteTexture => solidWhiteTexture;
146  private static GUICursor MouseCursorSprites => GUIStyle.CursorSprite;
147 
148  private static bool debugDrawSounds, debugDrawEvents;
149 
150  private static DebugDrawMetaData debugDrawMetaData;
151 
152  public struct DebugDrawMetaData
153  {
154  public bool Enabled;
156  public int Offset;
157  }
158 
159  public static GraphicsDevice GraphicsDevice => GameMain.Instance.GraphicsDevice;
160 
161  private static readonly List<GUIMessage> messages = new List<GUIMessage>();
162 
163  public static GUIFrame PauseMenu { get; private set; }
164  public static GUIFrame SettingsMenuContainer { get; private set; }
165  public static Sprite Arrow => GUIStyle.Arrow.Value.Sprite;
166 
167  public static bool HideCursor;
168 
169  public static KeyboardDispatcher KeyboardDispatcher { get; set; }
170 
174  public static bool ScreenChanged;
175 
176  private static bool settingsMenuOpen;
177  public static bool SettingsMenuOpen
178  {
179  get { return settingsMenuOpen; }
180  set
181  {
182  if (value == SettingsMenuOpen) { return; }
183 
184  if (value)
185  {
186  SettingsMenuContainer = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null);
187  new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, SettingsMenuContainer.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
188 
189  var settingsMenuInner = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), SettingsMenuContainer.RectTransform, Anchor.Center, scaleBasis: ScaleBasis.Smallest) { MinSize = new Point(640, 480) });
190  SettingsMenu.Create(settingsMenuInner.RectTransform);
191  }
192  else
193  {
194  SettingsMenu.Instance?.Close();
195  }
196  settingsMenuOpen = value;
197  }
198  }
199 
200  public static bool PauseMenuOpen { get; private set; }
201 
202  public static bool InputBlockingMenuOpen =>
203  PauseMenuOpen
204  || SettingsMenuOpen
205  || SocialOverlay.Instance is { IsOpen: true }
206  || DebugConsole.IsOpen
207  || GameSession.IsTabMenuOpen
208  || GameMain.GameSession?.GameMode is { Paused: true }
209  || CharacterHUD.IsCampaignInterfaceOpen
210  || GameMain.GameSession?.Campaign is { SlideshowPlayer: { Finished: false, Visible: true } };
211 
212  public static bool PreventPauseMenuToggle = false;
213 
214  public static Color ScreenOverlayColor
215  {
216  get;
217  set;
218  }
219 
220  public static bool DisableHUD, DisableUpperHUD, DisableItemHighlights, DisableCharacterNames;
221 
222  private static bool isSavingIndicatorEnabled;
223  private static Color savingIndicatorColor = Color.Transparent;
224  private static bool IsSavingIndicatorVisible => savingIndicatorColor.A > 0;
225  private static float savingIndicatorSpriteIndex;
226  private static float savingIndicatorColorLerpAmount;
227  private static SavingIndicatorState savingIndicatorState = SavingIndicatorState.None;
228  private static float? timeUntilSavingIndicatorDisabled;
229 
230  private static string loadedSpritesText;
231  private static DateTime loadedSpritesUpdateTime;
232 
233  private enum SavingIndicatorState
234  {
235  None,
236  FadingIn,
237  FadingOut
238  }
239 
240  public static void Init()
241  {
242  // create 1x1 texture for line drawing
243  CrossThread.RequestExecutionOnMainThread(() =>
244  {
245  solidWhiteTexture = new Texture2D(GraphicsDevice, 1, 1);
246  solidWhiteTexture.SetData(new Color[] { Color.White });// fill the texture with white
247  });
248  }
249 
253  public static void Draw(Camera cam, SpriteBatch spriteBatch)
254  {
255  lock (mutex)
256  {
257  usedIndicatorAngles.Clear();
258 
259  if (ScreenChanged)
260  {
261  updateList.Clear();
262  updateListSet.Clear();
263  Screen.Selected?.AddToGUIUpdateList();
264  ScreenChanged = false;
265  }
266 
267  foreach (GUIComponent c in updateList)
268  {
269  c.DrawAuto(spriteBatch);
270  }
271 
272  // always draw IME preview on top of everything else
273  foreach (GUIComponent c in updateList)
274  {
275  if (c is not GUITextBox box) { continue; }
276  box.DrawIMEPreview(spriteBatch);
277  }
278 
279  if (ScreenOverlayColor.A > 0.0f)
280  {
281  DrawRectangle(
282  spriteBatch,
283  new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight),
284  ScreenOverlayColor, true);
285  }
286 
287 #if UNSTABLE
288  string line1 = "Barotrauma Unstable v" + GameMain.Version;
289  string line2 = "(" + AssemblyInfo.BuildString + ", branch " + AssemblyInfo.GitBranch + ", revision " + AssemblyInfo.GitRevision + ")";
290 
291  Rectangle watermarkRect = new Rectangle(-50, GameMain.GraphicsHeight - 80, 50 + (int)(Math.Max(GUIStyle.LargeFont.MeasureString(line1).X, GUIStyle.Font.MeasureString(line2).X) * 1.2f), 100);
292  float alpha = 1.0f;
293 
294  int yOffset = 0;
295 
296  if (Screen.Selected == GameMain.GameScreen)
297  {
298  yOffset = (int)(-HUDLayoutSettings.ChatBoxArea.Height * 1.2f);
299  watermarkRect.Y += yOffset;
300  }
301 
302  if (Screen.Selected == GameMain.GameScreen || Screen.Selected == GameMain.SubEditorScreen)
303  {
304  alpha = 0.2f;
305  }
306 
307  GUIStyle.GetComponentStyle("OuterGlow").Sprites[GUIComponent.ComponentState.None][0].Draw(
308  spriteBatch, watermarkRect, Color.Black * 0.8f * alpha);
309  GUIStyle.LargeFont.DrawString(spriteBatch, line1,
310  new Vector2(10, GameMain.GraphicsHeight - 30 - GUIStyle.LargeFont.MeasureString(line1).Y + yOffset), Color.White * 0.6f * alpha);
311  GUIStyle.Font.DrawString(spriteBatch, line2,
312  new Vector2(10, GameMain.GraphicsHeight - 30 + yOffset), Color.White * 0.6f * alpha);
313 
314  if (Screen.Selected != GameMain.GameScreen)
315  {
316  var buttonRect =
317  new Rectangle(20 + (int)Math.Max(GUIStyle.LargeFont.MeasureString(line1).X, GUIStyle.Font.MeasureString(line2).X), GameMain.GraphicsHeight - (int)(45 * Scale) + yOffset, (int)(150 * Scale), (int)(40 * Scale));
318  if (DrawButton(spriteBatch, buttonRect, "Report Bug", GUIStyle.GetComponentStyle("GUIBugButton").Color * 0.8f))
319  {
320  GameMain.Instance.ShowBugReporter();
321  }
322  }
323 #endif
324 
325  if (DisableHUD)
326  {
327  DrawSavingIndicator(spriteBatch);
328  return;
329  }
330 
331  float startY = 10.0f;
332  float yStep = AdjustForTextScale(18) * yScale;
333  if (GameMain.ShowFPS || GameMain.DebugDraw || GameMain.ShowPerf)
334  {
335  float y = startY;
336  DrawString(spriteBatch, new Vector2(10, y),
337  "FPS: " + Math.Round(GameMain.PerformanceCounter.AverageFramesPerSecond),
338  Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
339  if (GameMain.GameSession != null && GameMain.GameSession.RoundDuration > 1.0)
340  {
341  y += yStep;
342  DrawString(spriteBatch, new Vector2(10, y),
343  $"Physics: {GameMain.CurrentUpdateRate}",
344  (GameMain.CurrentUpdateRate < Timing.FixedUpdateRate) ? Color.Red : Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
345  }
346  if (GameMain.DebugDraw || GameMain.ShowPerf)
347  {
348  y += yStep;
349  DrawString(spriteBatch, new Vector2(10, y),
350  "Active lights: " + Lights.LightManager.ActiveLightCount,
351  Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
352  y += yStep;
353  DrawString(spriteBatch, new Vector2(10, y),
354  "Physics: " + GameMain.World.UpdateTime.TotalMilliseconds + " ms",
355  Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
356  y += yStep;
357  try
358  {
359  DrawString(spriteBatch, new Vector2(10, y),
360  $"Bodies: {GameMain.World.BodyList.Count} ({GameMain.World.BodyList.Count(b => b != null && b.Awake && b.Enabled)} awake, {GameMain.World.BodyList.Count(b => b != null && b.Awake && b.BodyType == BodyType.Dynamic && b.Enabled)} dynamic)",
361  Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
362  }
363  catch (InvalidOperationException)
364  {
365  DebugConsole.AddWarning("Exception while rendering debug info. Physics bodies may have been created or removed while rendering.");
366  }
367  y += yStep;
368  DrawString(spriteBatch, new Vector2(10, y),
369  "Particle count: " + GameMain.ParticleManager.ParticleCount + "/" + GameMain.ParticleManager.MaxParticles,
370  Color.Lerp(GUIStyle.Green, GUIStyle.Red, (GameMain.ParticleManager.ParticleCount / (float)GameMain.ParticleManager.MaxParticles)), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
371 
372  }
373  }
374 
375  if (GameMain.ShowPerf)
376  {
377  float x = 400;
378  float y = startY;
379  DrawString(spriteBatch, new Vector2(x, y),
380  "Draw - Avg: " + GameMain.PerformanceCounter.DrawTimeGraph.Average().ToString("0.00") + " ms" +
381  " Max: " + GameMain.PerformanceCounter.DrawTimeGraph.LargestValue().ToString("0.00") + " ms",
382  GUIStyle.Green, Color.Black * 0.8f, font: GUIStyle.SmallFont);
383  y += yStep;
384  GameMain.PerformanceCounter.DrawTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: GUIStyle.Green);
385  y += yStep * 4;
386 
387  DrawString(spriteBatch, new Vector2(x, y),
388  "Update - Avg: " + GameMain.PerformanceCounter.UpdateTimeGraph.Average().ToString("0.00") + " ms" +
389  " Max: " + GameMain.PerformanceCounter.UpdateTimeGraph.LargestValue().ToString("0.00") + " ms",
390  Color.LightBlue, Color.Black * 0.8f, font: GUIStyle.SmallFont);
391  y += yStep;
392  GameMain.PerformanceCounter.UpdateTimeGraph.Draw(spriteBatch, new Rectangle((int)x, (int)y, 170, 50), color: Color.LightBlue);
393  y += yStep * 4;
394  foreach (string key in GameMain.PerformanceCounter.GetSavedIdentifiers.OrderBy(i => i))
395  {
396  float elapsedMillisecs = GameMain.PerformanceCounter.GetAverageElapsedMillisecs(key);
397 
398  int categoryDepth = key.Count(c => c == ':');
399  //color the more fine-grained counters red more easily (ok for the whole Update to take a longer time than specific part of the update)
400  float runningSlowThreshold = 10.0f / categoryDepth;
401  DrawString(spriteBatch, new Vector2(x + categoryDepth * 15, y),
402  key.Split(':').Last() + ": " + elapsedMillisecs.ToString("0.00"),
403  ToolBox.GradientLerp(elapsedMillisecs / runningSlowThreshold, Color.LightGreen, GUIStyle.Yellow, GUIStyle.Orange, GUIStyle.Red, Color.Magenta), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
404  y += yStep;
405  }
406  if (Powered.Grids != null)
407  {
408  DrawString(spriteBatch, new Vector2(x, y), "Grids: " + Powered.Grids.Count, Color.LightGreen, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
409  y += yStep;
410  }
411  if (Settings.EnableDiagnostics)
412  {
413  x += yStep * 2;
414  DrawString(spriteBatch, new Vector2(x, y), "ContinuousPhysicsTime: " + GameMain.World.ContinuousPhysicsTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContinuousPhysicsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
415  DrawString(spriteBatch, new Vector2(x, y + yStep), "ControllersUpdateTime: " + GameMain.World.ControllersUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ControllersUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
416  DrawString(spriteBatch, new Vector2(x, y + yStep * 2), "AddRemoveTime: " + GameMain.World.AddRemoveTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.AddRemoveTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
417  DrawString(spriteBatch, new Vector2(x, y + yStep * 3), "NewContactsTime: " + GameMain.World.NewContactsTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.NewContactsTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
418  DrawString(spriteBatch, new Vector2(x, y + yStep * 4), "ContactsUpdateTime: " + GameMain.World.ContactsUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.ContactsUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
419  DrawString(spriteBatch, new Vector2(x, y + yStep * 5), "SolveUpdateTime: " + GameMain.World.SolveUpdateTime.TotalMilliseconds.ToString("0.00"), Color.Lerp(Color.LightGreen, GUIStyle.Red, (float)GameMain.World.SolveUpdateTime.TotalMilliseconds / 10.0f), Color.Black * 0.5f, 0, GUIStyle.SmallFont);
420  }
421  }
422 
423  if (GameMain.DebugDraw && !Submarine.Unloading && !(Screen.Selected is RoundSummaryScreen))
424  {
425  float y = startY + yStep * 6;
426 
427  if (Screen.Selected.Cam != null)
428  {
429  y += yStep;
430  DrawString(spriteBatch, new Vector2(10, y),
431  "Camera pos: " + Screen.Selected.Cam.Position.ToPoint() + ", zoom: " + Screen.Selected.Cam.Zoom,
432  Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
433  }
434 
435  if (Submarine.MainSub != null)
436  {
437  y += yStep;
438  DrawString(spriteBatch, new Vector2(10, y),
439  "Sub pos: " + Submarine.MainSub.WorldPosition.ToPoint(),
440  Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
441  }
442 
443  if (loadedSpritesText == null || DateTime.Now > loadedSpritesUpdateTime)
444  {
445  loadedSpritesText = "Loaded sprites: " + Sprite.LoadedSprites.Count() + "\n(" + Sprite.LoadedSprites.Select(s => s.FilePath).Distinct().Count() + " unique textures)";
446  loadedSpritesUpdateTime = DateTime.Now + new TimeSpan(0, 0, seconds: 5);
447  }
448  y += yStep * 2;
449  DrawString(spriteBatch, new Vector2(10, y), loadedSpritesText, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
450 
451  if (debugDrawSounds)
452  {
453  float soundTextY = 0;
454  DrawString(spriteBatch, new Vector2(500, soundTextY),
455  "Sounds (Ctrl+S to hide): ", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
456  soundTextY += yStep;
457 
458  DrawString(spriteBatch, new Vector2(500, soundTextY),
459  "Current playback amplitude: " + GameMain.SoundManager.PlaybackAmplitude.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
460 
461  soundTextY += yStep;
462 
463  DrawString(spriteBatch, new Vector2(500, soundTextY),
464  "Compressed dynamic range gain: " + GameMain.SoundManager.CompressionDynamicRangeGain.ToString(), Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
465 
466  soundTextY += yStep;
467 
468  DrawString(spriteBatch, new Vector2(500, soundTextY),
469  "Loaded sounds: " + GameMain.SoundManager.LoadedSoundCount + " (" + GameMain.SoundManager.UniqueLoadedSoundCount + " unique)", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
470  soundTextY += yStep;
471 
472  for (int i = 0; i < SoundManager.SourceCount; i++)
473  {
474  Color clr = Color.White;
475  string soundStr = i + ": ";
476  SoundChannel playingSoundChannel = GameMain.SoundManager.GetSoundChannelFromIndex(SoundManager.SourcePoolIndex.Default, i);
477  if (playingSoundChannel == null)
478  {
479  soundStr += "none";
480  clr *= 0.5f;
481  }
482  else
483  {
484  soundStr += Path.GetFileNameWithoutExtension(playingSoundChannel.Sound.Filename);
485 
486 #if DEBUG
487  if (PlayerInput.GetKeyboardState.IsKeyDown(Keys.G))
488  {
489  if (PlayerInput.MousePosition.Y >= soundTextY && PlayerInput.MousePosition.Y <= soundTextY + 12)
490  {
491  GameMain.SoundManager.DebugSource(i);
492  }
493  }
494 #endif
495 
496  if (playingSoundChannel.Looping)
497  {
498  soundStr += " (looping)";
499  clr = Color.Yellow;
500  }
501  if (playingSoundChannel.IsStream)
502  {
503  soundStr += " (streaming)";
504  clr = Color.Lime;
505  }
506  if (!playingSoundChannel.IsPlaying)
507  {
508  soundStr += " (stopped)";
509  clr *= 0.5f;
510  }
511  else
512  {
513  if (playingSoundChannel.Muffled)
514  {
515  soundStr += " (muffled)";
516  clr = Color.Lerp(clr, Color.LightGray, 0.5f);
517  }
518  if (playingSoundChannel.FadingOutAndDisposing)
519  {
520  soundStr += ". Fading out...";
521  clr = Color.Lerp(clr, Color.Black, 0.15f);
522  }
523  }
524  }
525 
526  DrawString(spriteBatch, new Vector2(500, soundTextY), soundStr, clr, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
527  soundTextY += yStep;
528  }
529  }
530  else
531  {
532  DrawString(spriteBatch, new Vector2(500, 0),
533  "Ctrl+S to show sound debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
534  }
535 
536 
537  y += 185 * yScale;
538  if (debugDrawEvents)
539  {
540  DrawString(spriteBatch, new Vector2(10, y),
541  "Ctrl+E to hide EventManager debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
542  GameMain.GameSession?.EventManager?.DebugDrawHUD(spriteBatch, y + 15 * yScale);
543  }
544  else
545  {
546  DrawString(spriteBatch, new Vector2(10, y),
547  "Ctrl+E to show EventManager debug info", Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
548  }
549 
550  if (GameMain.GameSession?.GameMode is CampaignMode campaignMode)
551  {
552  // TODO: TEST THIS
553  if (debugDrawMetaData.Enabled)
554  {
555  string text = "Ctrl+M to hide campaign metadata debug info\n\n" +
556  $"Ctrl+1 to {(debugDrawMetaData.FactionMetadata ? "hide" : "show")} faction reputations, \n" +
557  $"Ctrl+2 to {(debugDrawMetaData.UpgradeLevels ? "hide" : "show")} upgrade levels, \n" +
558  $"Ctrl+3 to {(debugDrawMetaData.UpgradePrices ? "hide" : "show")} upgrade prices";
559  Vector2 textSize = GUIStyle.SmallFont.MeasureString(text);
560  Vector2 pos = new Vector2(GameMain.GraphicsWidth - (textSize.X + 10), 300);
561  DrawString(spriteBatch, pos, text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
562  pos.Y += textSize.Y + 8;
563  campaignMode.CampaignMetadata?.DebugDraw(spriteBatch, pos, campaignMode, debugDrawMetaData);
564  }
565  else
566  {
567  const string text = "Ctrl+M to show campaign metadata debug info";
568  DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (GUIStyle.SmallFont.MeasureString(text).X + 10), 300),
569  text, Color.White, Color.Black * 0.5f, 0, GUIStyle.SmallFont);
570  }
571  }
572 
573  IEnumerable<string> strings;
574  if (MouseOn != null)
575  {
576  RectTransform mouseOnRect = MouseOn.RectTransform;
577  bool isAbsoluteOffsetInUse = mouseOnRect.AbsoluteOffset != Point.Zero || mouseOnRect.RelativeOffset == Vector2.Zero;
578 
579  strings = new string[]
580  {
581  $"Selected UI Element: {MouseOn.GetType().Name} ({ MouseOn.Style?.Element.Name.LocalName ?? "no style" }, {MouseOn.Rect}",
582  $"Relative Offset: {mouseOnRect.RelativeOffset} | Absolute Offset: {(isAbsoluteOffsetInUse ? mouseOnRect.AbsoluteOffset : mouseOnRect.ParentRect.MultiplySize(mouseOnRect.RelativeOffset))}{(isAbsoluteOffsetInUse ? "" : " (Calculated from RelativeOffset)")}",
583  $"Anchor: {mouseOnRect.Anchor} | Pivot: {mouseOnRect.Pivot}"
584  };
585  }
586  else
587  {
588  strings = new string[]
589  {
590  $"GUI.Scale: {Scale}",
591  $"GUI.xScale: {xScale}",
592  $"GUI.yScale: {yScale}",
593  $"RelativeHorizontalAspectRatio: {RelativeHorizontalAspectRatio}",
594  $"RelativeVerticalAspectRatio: {RelativeVerticalAspectRatio}",
595  };
596  }
597 
598  strings = strings.Concat(new string[] { $"Cam.Zoom: {Screen.Selected.Cam?.Zoom ?? 0f}" });
599 
600  int padding = IntScale(10);
601  int yPos = padding;
602 
603  foreach (string str in strings)
604  {
605  Vector2 stringSize = GUIStyle.SmallFont.MeasureString(str);
606 
607  DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth - (int)stringSize.X - padding, yPos), str, Color.LightGreen, Color.Black, 0, GUIStyle.SmallFont);
608  yPos += (int)stringSize.Y + padding / 2;
609  }
610  }
611 
612  GameMain.GameSession?.EventManager?.DrawPinnedEvent(spriteBatch);
613 
614  if (HUDLayoutSettings.DebugDraw) { HUDLayoutSettings.Draw(spriteBatch); }
615 
616  GameMain.Client?.Draw(spriteBatch);
617 
618  if (Character.Controlled?.Inventory != null)
619  {
620  if (Character.Controlled.Stun < 0.1f && !Character.Controlled.IsDead)
621  {
622  Inventory.DrawFront(spriteBatch);
623  }
624  }
625 
626  DrawMessages(spriteBatch, cam);
627 
628  if (MouseOn != null && !MouseOn.ToolTip.IsNullOrWhiteSpace())
629  {
630  MouseOn.DrawToolTip(spriteBatch);
631  }
632 
633  if (SubEditorScreen.IsSubEditor())
634  {
635  // Draw our "infinite stack" on the cursor
636  switch (SubEditorScreen.DraggedItemPrefab)
637  {
638  case ItemPrefab itemPrefab:
639  {
640  var sprite = itemPrefab.InventoryIcon ?? itemPrefab.Sprite;
641  sprite?.Draw(spriteBatch, PlayerInput.MousePosition, scale: Math.Min(64 / sprite.size.X, 64 / sprite.size.Y) * Scale);
642  break;
643  }
644  case ItemAssemblyPrefab itemAssemblyPrefab:
645  {
646  itemAssemblyPrefab.Draw(spriteBatch, PlayerInput.MousePosition.FlipY());
647  break;
648  }
649  }
650  }
651 
652  DrawSavingIndicator(spriteBatch);
653  DrawCursor(spriteBatch);
654  HideCursor = false;
655  }
656  }
657 
658  public static void DrawMessageBoxesOnly(SpriteBatch spriteBatch)
659  {
660  bool anyDrawn = false;
661  foreach (var component in updateList)
662  {
663  component.DrawAuto(spriteBatch);
664  anyDrawn = true;
665  }
666  if (anyDrawn)
667  {
668  DrawCursor(spriteBatch);
669  }
670  }
671 
672  private static void DrawCursor(SpriteBatch spriteBatch)
673  {
674  if (GameMain.WindowActive && !HideCursor && MouseCursorSprites.Prefabs.Any())
675  {
676  spriteBatch.End();
677  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerStateClamp, rasterizerState: GameMain.ScissorTestEnable);
678 
679  if (GameMain.GameSession?.CrewManager is { DraggedOrderPrefab: { SymbolSprite: { } orderSprite, Color: var color }, DragOrder: true })
680  {
681  float spriteSize = Math.Max(orderSprite.size.X, orderSprite.size.Y);
682  orderSprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, color, orderSprite.size / 2f, scale: 32f / spriteSize * Scale);
683  }
684 
685  var sprite = MouseCursorSprites[MouseCursor] ?? MouseCursorSprites[CursorState.Default];
686  sprite.Draw(spriteBatch, PlayerInput.LatestMousePosition, Color.White, sprite.Origin, 0f, Scale / 1.5f);
687 
688  spriteBatch.End();
689  spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState, rasterizerState: GameMain.ScissorTestEnable);
690  }
691  }
692 
693  public static void DrawBackgroundSprite(SpriteBatch spriteBatch, Sprite backgroundSprite, Color color, Rectangle? drawArea = null, SpriteEffects spriteEffects = SpriteEffects.None)
694  {
695  Rectangle area = drawArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
696 
697  float scale = Math.Max(
698  (float)area.Width / backgroundSprite.SourceRect.Width,
699  (float)area.Height / backgroundSprite.SourceRect.Height) * 1.1f;
700  float paddingX = backgroundSprite.SourceRect.Width * scale - area.Width;
701  float paddingY = backgroundSprite.SourceRect.Height * scale - area.Height;
702 
703  double noiseT = Timing.TotalTime * 0.02f;
704  Vector2 pos = new Vector2((float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0) - 0.5f, (float)PerlinNoise.CalculatePerlin(noiseT, noiseT, 0.5f) - 0.5f);
705  pos = new Vector2(pos.X * paddingX, pos.Y * paddingY);
706 
707  spriteBatch.Draw(backgroundSprite.Texture,
708  area.Center.ToVector2() + pos,
709  backgroundSprite.SourceRect, color, 0.0f, backgroundSprite.size / 2,
710  scale, spriteEffects, 0.0f);
711  }
712 
713  #region Update list
714  private static readonly List<GUIComponent> updateList = new List<GUIComponent>();
715  //essentially a copy of the update list, used as an optimization to quickly check if the component is present in the update list
716  private static readonly HashSet<GUIComponent> updateListSet = new HashSet<GUIComponent>();
717  private static readonly Queue<GUIComponent> removals = new Queue<GUIComponent>();
718  private static readonly Queue<GUIComponent> additions = new Queue<GUIComponent>();
719  // A helpers list for all elements that have a draw order less than 0.
720  private static readonly List<GUIComponent> firstAdditions = new List<GUIComponent>();
721  // A helper list for all elements that have a draw order greater than 0.
722  private static readonly List<GUIComponent> lastAdditions = new List<GUIComponent>();
723 
728  public static void AddToUpdateList(GUIComponent component)
729  {
730  lock (mutex)
731  {
732  if (component == null)
733  {
734  DebugConsole.ThrowError("Trying to add a null component on the GUI update list!");
735  return;
736  }
737  if (!component.Visible) { return; }
738  if (component.UpdateOrder < 0)
739  {
740  firstAdditions.Add(component);
741  }
742  else if (component.UpdateOrder > 0)
743  {
744  lastAdditions.Add(component);
745  }
746  else
747  {
748  additions.Enqueue(component);
749  }
750  }
751  }
752 
757  public static void RemoveFromUpdateList(GUIComponent component, bool alsoChildren = true)
758  {
759  lock (mutex)
760  {
761  if (updateListSet.Contains(component))
762  {
763  removals.Enqueue(component);
764  }
765  if (alsoChildren)
766  {
767  if (component.RectTransform != null)
768  {
769  component.RectTransform.Children.ForEach(c => RemoveFromUpdateList(c.GUIComponent));
770  }
771  else
772  {
773  component.Children.ForEach(c => RemoveFromUpdateList(c));
774  }
775  }
776  }
777  }
778 
779  public static void ClearUpdateList()
780  {
781  lock (mutex)
782  {
783  if (KeyboardDispatcher.Subscriber is GUIComponent && !updateList.Contains(KeyboardDispatcher.Subscriber as GUIComponent))
784  {
786  }
787  updateList.Clear();
788  updateListSet.Clear();
789  }
790  }
791 
792  private static void RefreshUpdateList()
793  {
794  lock (mutex)
795  {
796  foreach (var component in updateList)
797  {
798  if (!component.Visible)
799  {
800  RemoveFromUpdateList(component);
801  }
802  }
803  ProcessHelperList(firstAdditions);
804  ProcessAdditions();
805  ProcessHelperList(lastAdditions);
806  ProcessRemovals();
807  }
808  }
809 
810  private static void ProcessAdditions()
811  {
812  lock (mutex)
813  {
814  while (additions.Count > 0)
815  {
816  var component = additions.Dequeue();
817  if (!updateListSet.Contains(component))
818  {
819  updateList.Add(component);
820  updateListSet.Add(component);
821  }
822  }
823  }
824  }
825 
826  private static void ProcessRemovals()
827  {
828  lock (mutex)
829  {
830  while (removals.Count > 0)
831  {
832  var component = removals.Dequeue();
833  updateList.Remove(component);
834  updateListSet.Remove(component);
836  {
838  }
839  }
840  }
841  }
842 
843  private static void ProcessHelperList(List<GUIComponent> list)
844  {
845  lock (mutex)
846  {
847  if (list.Count == 0) { return; }
848  foreach (var item in list)
849  {
850  int index = 0;
851  if (updateList.Count > 0)
852  {
853  index = updateList.Count;
854  while (index > 0 && updateList[index-1].UpdateOrder > item.UpdateOrder)
855  {
856  index--;
857  }
858  }
859  if (!updateListSet.Contains(item))
860  {
861  updateList.Insert(index, item);
862  updateListSet.Add(item);
863  }
864  }
865  list.Clear();
866  }
867  }
868 
869  private static void HandlePersistingElements(float deltaTime)
870  {
871  bool currentMessageBoxIsVerificationPrompt = GUIMessageBox.VisibleBox is GUIMessageBox { DrawOnTop: true };
872 
873  if (!currentMessageBoxIsVerificationPrompt)
874  {
875  GUIMessageBox.AddActiveToGUIUpdateList();
876  }
877 
878  if (SettingsMenuOpen)
879  {
880  SettingsMenuContainer.AddToGUIUpdateList();
881  }
882  else if (PauseMenuOpen)
883  {
884  PauseMenu.AddToGUIUpdateList();
885  }
886 
888 
889  GUIContextMenu.AddActiveToGUIUpdateList();
890 
891  //the "are you sure you want to quit" prompts are drawn on top of everything else
892  if (currentMessageBoxIsVerificationPrompt)
893  {
894  GUIMessageBox.VisibleBox.AddToGUIUpdateList();
895  }
896  }
897 
898  public static IEnumerable<GUIComponent> GetAdditions()
899  {
900  return additions.Union(firstAdditions).Union(lastAdditions);
901  }
902  #endregion
903 
904  public static GUIComponent MouseOn { get; private set; }
905 
906  public static bool IsMouseOn(GUIComponent target)
907  {
908  lock (mutex)
909  {
910  if (target == null) { return false; }
911  //if (MouseOn == null) { return true; }
912  return target == MouseOn || target.IsParentOf(MouseOn);
913  }
914  }
915 
916  public static void ForceMouseOn(GUIComponent c)
917  {
918  lock (mutex)
919  {
920  MouseOn = c;
921  }
922  }
923 
927  public static GUIComponent UpdateMouseOn()
928  {
929  lock (mutex)
930  {
931  GUIComponent prevMouseOn = MouseOn;
932  MouseOn = null;
933  int inventoryIndex = -1;
934 
935  Inventory.RefreshMouseOnInventory();
936  if (Inventory.IsMouseOnInventory)
937  {
938  inventoryIndex = updateList.IndexOf(CharacterHUD.HUDFrame);
939  }
940 
941  if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) ||
942  (prevMouseOn == null && !PlayerInput.SecondaryMouseButtonHeld() && !Inventory.DraggingItems.Any()))
943  {
944  for (var i = updateList.Count - 1; i > inventoryIndex; i--)
945  {
946  var c = updateList[i];
947  if (!c.CanBeFocused) { continue; }
948  if (c.MouseRect.Contains(PlayerInput.MousePosition))
949  {
950  if ((!PlayerInput.PrimaryMouseButtonHeld() && !PlayerInput.PrimaryMouseButtonClicked()) || c == prevMouseOn || prevMouseOn == null)
951  {
952  MouseOn = c;
953  }
954  break;
955  }
956  }
957  }
958  else
959  {
960  MouseOn = prevMouseOn;
961  }
962 
963  MouseCursor = UpdateMouseCursorState(MouseOn);
964  return MouseOn;
965  }
966  }
967 
968  private static CursorState UpdateMouseCursorState(GUIComponent c)
969  {
970  lock (mutex)
971  {
972  // Waiting and drag cursor override everything else
973  if (MouseCursor == CursorState.Waiting) { return CursorState.Waiting; }
974  if (GUIScrollBar.DraggingBar != null) { return GUIScrollBar.DraggingBar.Bar.HoverCursor; }
975 
976  if (SubEditorScreen.IsSubEditor() && SubEditorScreen.DraggedItemPrefab != null) { return CursorState.Hand; }
977 
978  // Wire cursors
979  if (Character.Controlled != null)
980  {
981  if (Character.Controlled.SelectedItem?.GetComponent<ConnectionPanel>() != null)
982  {
983  if (Connection.DraggingConnected != null)
984  {
985  return CursorState.Dragging;
986  }
987  else if (ConnectionPanel.HighlightedWire != null)
988  {
989  return CursorState.Hand;
990  }
991  }
992  if (Wire.DraggingWire != null) { return CursorState.Dragging; }
993  }
994 
995  if (c == null || c is GUICustomComponent)
996  {
997  switch (Screen.Selected)
998  {
999  // Character editor limbs
1000  case CharacterEditorScreen editor:
1001  return editor.GetMouseCursorState();
1002  // Portrait area during gameplay
1003  case GameScreen _ when !(Character.Controlled?.ShouldLockHud() ?? true):
1004  if (CharacterHUD.MouseOnCharacterPortrait() || CharacterHealth.IsMouseOnHealthBar())
1005  {
1006  return CursorState.Hand;
1007  }
1008  break;
1009  // Sub editor drag and highlight
1010  case SubEditorScreen editor:
1011  {
1012  if (MapEntity.StartMovingPos != Vector2.Zero || MapEntity.Resizing)
1013  {
1014  return CursorState.Dragging;
1015  }
1016  if (MapEntity.HighlightedEntities.Any(h => !h.IsSelected))
1017  {
1018  return CursorState.Hand;
1019  }
1020  break;
1021  }
1022  }
1023  }
1024 
1025  if (c != null && c.Visible)
1026  {
1027  if (c.AlwaysOverrideCursor) { return c.HoverCursor; }
1028 
1029  // When a button opens a submenu, it increases to the size of the entire screen.
1030  // And this is of course picked up as clickable area.
1031  // There has to be a better way of checking this but for now this works.
1032  var monitorRect = new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
1033 
1034  var parent = FindInteractParent(c);
1035 
1036  if (c.Enabled)
1037  {
1038  var dragHandle = c as GUIDragHandle ?? parent as GUIDragHandle;
1039  if (dragHandle != null)
1040  {
1041  return dragHandle.Dragging ? CursorState.Dragging : CursorState.Hand;
1042  }
1043  //do not show the hover cursor when the cursor is on a listbox (on the listbox itself, not any of elements inside it!)
1044  if (c is GUIListBox && (parent == null || parent == c))
1045  {
1046  return CursorState.Default;
1047  }
1048  // Some parent elements take priority
1049  // but not when the child is a GUIButton or GUITickBox
1050  if (parent is not GUIButton && parent is not GUIListBox ||
1051  (c is GUIButton) || (c is GUITickBox))
1052  {
1053  if (!c.Rect.Equals(monitorRect))
1054  {
1055  if (c is GUITickBox)
1056  {
1057  //tickboxes have some special logic: not all of the component is hoverable (just the box and the text area)
1058  if (c.State is GUIComponent.ComponentState.Hover or GUIComponent.ComponentState.HoverSelected)
1059  {
1060  return c.HoverCursor;
1061  }
1062  }
1063  else
1064  {
1065  return c.HoverCursor;
1066  }
1067  }
1068  }
1069  }
1070 
1071 
1072  // Children in list boxes can be interacted with despite not having
1073  // a GUIButton inside of them so instead of hard coding we check if
1074  // the children can be interacted with by checking their hover state
1075  if (parent is GUIListBox listBox && c.Parent == listBox.Content)
1076  {
1077  if (listBox.DraggedElement != null) { return CursorState.Dragging; }
1078  if (listBox.CurrentDragMode != GUIListBox.DragMode.NoDragging) { return CursorState.Move; }
1079 
1080  if (listBox.HoverCursor != CursorState.Default)
1081  {
1082  var hoverParent = c;
1083  while (true)
1084  {
1085  if (hoverParent == parent || hoverParent == null) { break; }
1086  if (hoverParent.State == GUIComponent.ComponentState.Hover) { return CursorState.Hand; }
1087  hoverParent = hoverParent.Parent;
1088  }
1089  }
1090  }
1091 
1092  if (parent != null && parent.CanBeFocused)
1093  {
1094  if (!parent.Rect.Equals(monitorRect)) { return parent.HoverCursor; }
1095  }
1096  }
1097 
1098  if (Inventory.IsMouseOnInventory) { return Inventory.GetInventoryMouseCursor(); }
1099 
1100  var character = Character.Controlled;
1101  // ReSharper disable once InvertIf
1102  if (character != null)
1103  {
1104  // Health menus
1105  if (character.CharacterHealth.MouseOnElement) { return CursorState.Hand; }
1106 
1107  if (character.SelectedCharacter != null)
1108  {
1109  if (character.SelectedCharacter.CharacterHealth.MouseOnElement)
1110  {
1111  return CursorState.Hand;
1112  }
1113  }
1114 
1115  // Character is hovering over an item placed in the world
1116  if (character.FocusedItem != null) { return CursorState.Hand; }
1117  }
1118 
1119  return CursorState.Default;
1120 
1121  static GUIComponent FindInteractParent(GUIComponent component)
1122  {
1123  while (true)
1124  {
1125  var parent = component.Parent;
1126  if (parent == null) { return null; }
1127 
1128  if (ContainsMouse(parent))
1129  {
1130  if (parent.Enabled)
1131  {
1132  switch (parent)
1133  {
1134  case GUIButton button:
1135  return button;
1136  case GUITextBox box:
1137  return box;
1138  case GUIListBox list:
1139  return list;
1140  case GUIScrollBar bar:
1141  return bar;
1142  case GUIDragHandle dragHandle:
1143  return dragHandle;
1144  }
1145  }
1146  component = parent;
1147  }
1148  else
1149  {
1150  return null;
1151  }
1152  }
1153  }
1154 
1155  static bool ContainsMouse(GUIComponent component)
1156  {
1157  // If component has a mouse rectangle then use that, if not use it's physical rect
1158  return !component.MouseRect.Equals(Rectangle.Empty) ?
1159  component.MouseRect.Contains(PlayerInput.MousePosition) :
1160  component.Rect.Contains(PlayerInput.MousePosition);
1161  }
1162  }
1163  }
1164 
1169  public static void SetCursorWaiting(int waitSeconds = 10, Func<bool> endCondition = null)
1170  {
1171  CoroutineManager.StartCoroutine(WaitCursorCoroutine(), "WaitCursorTimeout");
1172 
1173  IEnumerable<CoroutineStatus> WaitCursorCoroutine()
1174  {
1175  MouseCursor = CursorState.Waiting;
1176  var timeOut = DateTime.Now + new TimeSpan(0, 0, waitSeconds);
1177  while (DateTime.Now < timeOut)
1178  {
1179  if (endCondition != null)
1180  {
1181  try
1182  {
1183  if (endCondition.Invoke()) { break; }
1184  }
1185  catch { break; }
1186  }
1187  yield return CoroutineStatus.Running;
1188  }
1189  if (MouseCursor == CursorState.Waiting) { MouseCursor = CursorState.Default; }
1190  yield return CoroutineStatus.Success;
1191  }
1192  }
1193 
1194  public static void ClearCursorWait()
1195  {
1196  lock (mutex)
1197  {
1198  CoroutineManager.StopCoroutines("WaitCursorTimeout");
1199  MouseCursor = CursorState.Default;
1200  }
1201  }
1202 
1203  public static bool HasSizeChanged(Point referenceResolution, float referenceUIScale, float referenceHUDScale)
1204  {
1205  return GameMain.GraphicsWidth != referenceResolution.X || GameMain.GraphicsHeight != referenceResolution.Y ||
1206  referenceUIScale != Inventory.UIScale || referenceHUDScale != Scale;
1207  }
1208 
1209  public static void Update(float deltaTime)
1210  {
1211  lock (mutex)
1212  {
1213  if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.S))
1214  {
1215  debugDrawSounds = !debugDrawSounds;
1216  }
1217  if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.E))
1218  {
1219  debugDrawEvents = !debugDrawEvents;
1220  }
1221  if (PlayerInput.IsCtrlDown() && PlayerInput.KeyHit(Keys.M))
1222  {
1223  debugDrawMetaData.Enabled = !debugDrawMetaData.Enabled;
1224  }
1225 
1226  if (debugDrawMetaData.Enabled)
1227  {
1228  if (PlayerInput.KeyHit(Keys.Up))
1229  {
1230  debugDrawMetaData.Offset--;
1231  }
1232  if (PlayerInput.KeyHit(Keys.Down))
1233  {
1234  debugDrawMetaData.Offset++;
1235  }
1236  if (PlayerInput.IsCtrlDown())
1237  {
1238  if (PlayerInput.KeyHit(Keys.D1))
1239  {
1240  debugDrawMetaData.FactionMetadata = !debugDrawMetaData.FactionMetadata;
1241  debugDrawMetaData.Offset = 0;
1242  }
1243  if (PlayerInput.KeyHit(Keys.D2))
1244  {
1245  debugDrawMetaData.UpgradeLevels = !debugDrawMetaData.UpgradeLevels;
1246  debugDrawMetaData.Offset = 0;
1247  }
1248  if (PlayerInput.KeyHit(Keys.D3))
1249  {
1250  debugDrawMetaData.UpgradePrices = !debugDrawMetaData.UpgradePrices;
1251  debugDrawMetaData.Offset = 0;
1252  }
1253  }
1254  }
1255 
1256  HandlePersistingElements(deltaTime);
1257  RefreshUpdateList();
1258  UpdateMouseOn();
1259  Debug.Assert(updateList.Count == updateListSet.Count);
1260  foreach (var c in updateList)
1261  {
1262  c.UpdateAuto(deltaTime);
1263  }
1264  UpdateMessages(deltaTime);
1265  UpdateSavingIndicator(deltaTime);
1266  }
1267  }
1268 
1269  public static void UpdateGUIMessageBoxesOnly(float deltaTime)
1270  {
1271  GUIMessageBox.AddActiveToGUIUpdateList();
1272  RefreshUpdateList();
1273  UpdateMouseOn();
1274  foreach (var c in updateList)
1275  {
1276  c.UpdateAuto(deltaTime);
1277  }
1278  }
1279 
1280  private static void UpdateMessages(float deltaTime)
1281  {
1282  lock (mutex)
1283  {
1284  foreach (GUIMessage msg in messages)
1285  {
1286  if (msg.WorldSpace) { continue; }
1287  msg.Timer -= deltaTime;
1288 
1289  if (msg.Size.X > HUDLayoutSettings.MessageAreaTop.Width)
1290  {
1291  msg.Pos = Vector2.Lerp(Vector2.Zero, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width - msg.Size.X, 0), 1.0f - msg.Timer / msg.LifeTime);
1292  }
1293  else
1294  {
1295  //enough space to show the full message, position it at the center of the msg area
1296  if (msg.Timer > 1.0f)
1297  {
1298  msg.Pos = Vector2.Lerp(msg.Pos, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width / 2 - msg.Size.X / 2, 0), Math.Min(deltaTime * 10.0f, 1.0f));
1299  }
1300  else
1301  {
1302  msg.Pos = Vector2.Lerp(msg.Pos, new Vector2(-HUDLayoutSettings.MessageAreaTop.Width - msg.Size.X, 0), deltaTime * 10.0f);
1303  }
1304  }
1305  //only the first message (the currently visible one) is updated at a time
1306  break;
1307  }
1308 
1309  foreach (GUIMessage msg in messages)
1310  {
1311  if (!msg.WorldSpace) { continue; }
1312  msg.Timer -= deltaTime;
1313  msg.Pos += msg.Velocity * deltaTime;
1314  }
1315 
1316  messages.RemoveAll(m => m.Timer <= 0.0f);
1317  }
1318  }
1319 
1320  private static void UpdateSavingIndicator(float deltaTime)
1321  {
1322  if (GUIStyle.SavingIndicator == null) { return; }
1323  lock (mutex)
1324  {
1325  if (timeUntilSavingIndicatorDisabled.HasValue)
1326  {
1327  timeUntilSavingIndicatorDisabled -= deltaTime;
1328  if (timeUntilSavingIndicatorDisabled <= 0.0f)
1329  {
1330  isSavingIndicatorEnabled = false;
1331  timeUntilSavingIndicatorDisabled = null;
1332  }
1333  }
1334  if (isSavingIndicatorEnabled)
1335  {
1336  if (savingIndicatorColor == Color.Transparent)
1337  {
1338  savingIndicatorState = SavingIndicatorState.FadingIn;
1339  savingIndicatorColorLerpAmount = 0.0f;
1340  }
1341  else if (savingIndicatorColor == Color.White)
1342  {
1343  savingIndicatorState = SavingIndicatorState.None;
1344  }
1345  }
1346  else
1347  {
1348  if (savingIndicatorColor == Color.White)
1349  {
1350  savingIndicatorState = SavingIndicatorState.FadingOut;
1351  savingIndicatorColorLerpAmount = 0.0f;
1352  }
1353  else if (savingIndicatorColor == Color.Transparent)
1354  {
1355  savingIndicatorState = SavingIndicatorState.None;
1356  }
1357  }
1358  if (savingIndicatorState != SavingIndicatorState.None)
1359  {
1360  bool isFadingIn = savingIndicatorState == SavingIndicatorState.FadingIn;
1361  Color lerpStartColor = isFadingIn ? Color.Transparent : Color.White;
1362  Color lerpTargetColor = isFadingIn ? Color.White : Color.Transparent;
1363  savingIndicatorColorLerpAmount += (isFadingIn ? 2.0f : 0.5f) * deltaTime;
1364  savingIndicatorColor = Color.Lerp(lerpStartColor, lerpTargetColor, savingIndicatorColorLerpAmount);
1365  }
1366  if (IsSavingIndicatorVisible)
1367  {
1368  savingIndicatorSpriteIndex = (savingIndicatorSpriteIndex + 15.0f * deltaTime) % (GUIStyle.SavingIndicator.FrameCount + 1);
1369  }
1370  }
1371  }
1372 
1373 #region Element drawing
1374 
1375  private static readonly List<float> usedIndicatorAngles = new List<float>();
1376 
1379  public static void DrawIndicator(SpriteBatch spriteBatch, in Vector2 worldPosition, Camera cam, in Range<float> visibleRange, Sprite sprite, in Color color,
1380  bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null, LocalizedString label = null)
1381  {
1382  Vector2 diff = worldPosition - cam.WorldViewCenter;
1383  float dist = diff.Length();
1384 
1385  float symbolScale = Math.Min(64.0f / sprite.size.X, 1.0f) * scaleMultiplier * Scale;
1386 
1387  if (overrideAlpha.HasValue || visibleRange.Contains(dist))
1388  {
1389  float alpha = overrideAlpha ?? MathUtils.Min((dist - visibleRange.Start) / 100.0f, 1.0f - ((dist - visibleRange.End + 100f) / 100.0f), 1.0f);
1390  Vector2 targetScreenPos = cam.WorldToScreen(worldPosition);
1391 
1392  if (!createOffset)
1393  {
1394  sprite.Draw(spriteBatch, targetScreenPos, color * alpha, rotate: 0.0f, scale: symbolScale);
1395  return;
1396  }
1397 
1398  float screenDist = Vector2.Distance(cam.WorldToScreen(cam.WorldViewCenter), targetScreenPos);
1399  float angle = MathUtils.VectorToAngle(diff);
1400  float originalAngle = angle;
1401 
1402  const float minAngleDiff = 0.05f;
1403  bool overlapFound = true;
1404  int iterations = 0;
1405  while (overlapFound && iterations < 10)
1406  {
1407  overlapFound = false;
1408  foreach (float usedIndicatorAngle in usedIndicatorAngles)
1409  {
1410  float shortestAngle = MathUtils.GetShortestAngle(angle, usedIndicatorAngle);
1411  if (MathUtils.NearlyEqual(shortestAngle, 0.0f)) { shortestAngle = 0.01f; }
1412  if (Math.Abs(shortestAngle) < minAngleDiff)
1413  {
1414  angle -= Math.Sign(shortestAngle) * (minAngleDiff - Math.Abs(shortestAngle));
1415  overlapFound = true;
1416  break;
1417  }
1418  }
1419  iterations++;
1420  }
1421 
1422  usedIndicatorAngles.Add(angle);
1423 
1424  Vector2 iconDiff = new Vector2(
1425  (float)Math.Cos(angle) * Math.Min(GameMain.GraphicsWidth * 0.4f, screenDist + 10),
1426  (float)-Math.Sin(angle) * Math.Min(GameMain.GraphicsHeight * 0.4f, screenDist + 10));
1427 
1428  angle = MathHelper.Lerp(originalAngle, angle, MathHelper.Clamp(((screenDist + 10f) - iconDiff.Length()) / 10f, 0f, 1f));
1429 
1430  iconDiff = new Vector2(
1431  (float)Math.Cos(angle) * Math.Min(GameMain.GraphicsWidth * 0.4f, screenDist),
1432  (float)-Math.Sin(angle) * Math.Min(GameMain.GraphicsHeight * 0.4f, screenDist));
1433 
1434  Vector2 iconPos = cam.WorldToScreen(cam.WorldViewCenter) + iconDiff;
1435  sprite.Draw(spriteBatch, iconPos, color * alpha, rotate: 0.0f, scale: symbolScale);
1436 
1437  if (label != null)
1438  {
1439  float cursorDist = Vector2.Distance(PlayerInput.MousePosition, iconPos);
1440  if (cursorDist < sprite.size.X * symbolScale)
1441  {
1442  Vector2 textSize = GUIStyle.Font.MeasureString(label);
1443  Vector2 textPos = iconPos + new Vector2(sprite.size.X * symbolScale * 0.7f * Math.Sign(-iconDiff.X), -textSize.Y / 2);
1444  if (iconDiff.X > 0) { textPos.X -= textSize.X; }
1445  DrawString(spriteBatch, textPos + Vector2.One, label, Color.Black);
1446  DrawString(spriteBatch, textPos, label, color);
1447  }
1448  }
1449 
1450  if (screenDist - 10 > iconDiff.Length())
1451  {
1452  Vector2 normalizedDiff = Vector2.Normalize(targetScreenPos - iconPos);
1453  Vector2 arrowOffset = normalizedDiff * sprite.size.X * symbolScale * 0.7f;
1454  Arrow.Draw(spriteBatch, iconPos + arrowOffset, color * alpha, MathUtils.VectorToAngle(arrowOffset) + MathHelper.PiOver2, scale: 0.5f);
1455  }
1456  }
1457  }
1458 
1459  public static void DrawIndicator(SpriteBatch spriteBatch, Vector2 worldPosition, Camera cam, float hideDist, Sprite sprite, Color color,
1460  bool createOffset = true, float scaleMultiplier = 1.0f, float? overrideAlpha = null)
1461  {
1462  DrawIndicator(spriteBatch, worldPosition, cam, new Range<float>(hideDist, float.PositiveInfinity), sprite, color, createOffset, scaleMultiplier, overrideAlpha);
1463  }
1464 
1465  public static void DrawLine(SpriteBatch sb, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, float width = 1)
1466  {
1467  DrawLine(sb, solidWhiteTexture, start, end, clr, depth, (int)width);
1468  }
1469 
1470  public static void DrawLine(SpriteBatch sb, Sprite sprite, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, int width = 1)
1471  {
1472  Vector2 edge = end - start;
1473  // calculate angle to rotate line
1474  float angle = (float)Math.Atan2(edge.Y, edge.X);
1475 
1476  sb.Draw(sprite.Texture,
1477  new Rectangle(// rectangle defines shape of line and position of start of line
1478  (int)start.X,
1479  (int)start.Y,
1480  (int)edge.Length(), //sb will strech the texture to fill this rectangle
1481  width), //width of line, change this to make thicker line
1482  sprite.SourceRect,
1483  clr, //colour of line
1484  angle, //angle of line (calulated above)
1485  new Vector2(0, sprite.SourceRect.Height / 2), // point in line about which to rotate
1486  SpriteEffects.None,
1487  depth);
1488  }
1489 
1490  public static void DrawLine(SpriteBatch sb, Texture2D texture, Vector2 start, Vector2 end, Color clr, float depth = 0.0f, int width = 1)
1491  {
1492  Vector2 edge = end - start;
1493  // calculate angle to rotate line
1494  float angle = (float)Math.Atan2(edge.Y, edge.X);
1495 
1496  sb.Draw(texture,
1497  new Rectangle(// rectangle defines shape of line and position of start of line
1498  (int)start.X,
1499  (int)start.Y,
1500  (int)edge.Length(), //sb will strech the texture to fill this rectangle
1501  width), //width of line, change this to make thicker line
1502  null,
1503  clr, //colour of line
1504  angle, //angle of line (calulated above)
1505  new Vector2(0, texture.Height / 2.0f), // point in line about which to rotate
1506  SpriteEffects.None,
1507  depth);
1508  }
1509 
1510  public static void DrawString(SpriteBatch sb, Vector2 pos, LocalizedString text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, ForceUpperCase forceUpperCase = ForceUpperCase.Inherit)
1511  {
1512  DrawString(sb, pos, text.Value, color, backgroundColor, backgroundPadding, font, forceUpperCase);
1513  }
1514 
1515  public static void DrawString(SpriteBatch sb, Vector2 pos, string text, Color color, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, ForceUpperCase forceUpperCase = ForceUpperCase.Inherit)
1516  {
1517  if (color.A == 0) { return; }
1518  if (font == null) { font = GUIStyle.Font; }
1519  if (backgroundColor != null && backgroundColor.Value.A > 0)
1520  {
1521  Vector2 textSize = font.MeasureString(text);
1522  DrawRectangle(sb, pos - Vector2.One * backgroundPadding, textSize + Vector2.One * 2.0f * backgroundPadding, (Color)backgroundColor, true);
1523  }
1524 
1525  font.DrawString(sb, text, pos, color, forceUpperCase: forceUpperCase);
1526  }
1527 
1528  public static void DrawStringWithColors(SpriteBatch sb, Vector2 pos, string text, Color color, in ImmutableArray<RichTextData>? richTextData, Color? backgroundColor = null, int backgroundPadding = 0, GUIFont font = null, float depth = 0.0f)
1529  {
1530  if (font == null) font = GUIStyle.Font;
1531  if (backgroundColor != null)
1532  {
1533  Vector2 textSize = font.MeasureString(text);
1534  DrawRectangle(sb, pos - Vector2.One * backgroundPadding, textSize + Vector2.One * 2.0f * backgroundPadding, (Color)backgroundColor, true, depth, 5);
1535  }
1536 
1537  font.DrawStringWithColors(sb, text, pos, color, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, depth, richTextData);
1538  }
1539 
1540  private const int DonutSegments = 30;
1541  private static readonly ImmutableArray<Vector2> canonicalCircle
1542  = Enumerable.Range(0, DonutSegments)
1543  .Select(i => i * (2.0f * MathF.PI / DonutSegments))
1544  .Select(angle => new Vector2(MathF.Cos(angle), MathF.Sin(angle)))
1545  .ToImmutableArray();
1546  private static readonly VertexPositionColorTexture[] donutVerts = new VertexPositionColorTexture[DonutSegments * 4];
1547 
1548  public static void DrawDonutSection(
1549  SpriteBatch sb, Vector2 center, Range<float> radii, float sectionRad, Color clr, float depth = 0.0f, float rotationRad = 0.0f)
1550  {
1551  float getRadius(int vertexIndex)
1552  => (vertexIndex % 4) switch
1553  {
1554  0 => radii.End,
1555  1 => radii.End,
1556  2 => radii.Start,
1557  3 => radii.Start,
1558  _ => throw new InvalidOperationException()
1559  };
1560  static int getDirectionIndex(int vertexIndex)
1561  => (vertexIndex % 4) switch
1562  {
1563  0 => (vertexIndex / 4) + 0,
1564  1 => (vertexIndex / 4) + 1,
1565  2 => (vertexIndex / 4) + 0,
1566  3 => (vertexIndex / 4) + 1,
1567  _ => throw new InvalidOperationException()
1568  };
1569 
1570  float sectionProportion = sectionRad / (MathF.PI * 2.0f);
1571  int maxDirectionIndex = Math.Min(DonutSegments, (int)MathF.Ceiling(sectionProportion * DonutSegments));
1572 
1573  Vector2 getDirection(int vertexIndex)
1574  {
1575  int directionIndex = getDirectionIndex(vertexIndex);
1576  Vector2 dir = canonicalCircle[directionIndex % DonutSegments];
1577  if (maxDirectionIndex > 0 && directionIndex >= maxDirectionIndex)
1578  {
1579  float maxSectionProportion = (float)maxDirectionIndex / DonutSegments;
1580  dir = Vector2.Lerp(
1581  canonicalCircle[maxDirectionIndex - 1],
1582  canonicalCircle[maxDirectionIndex % DonutSegments],
1583  1.0f - (maxSectionProportion - sectionProportion) * DonutSegments);
1584  }
1585 
1586  return new Vector2(dir.Y, -dir.X);
1587  }
1588 
1589  for (int vertexIndex = 0; vertexIndex < maxDirectionIndex * 4; vertexIndex++)
1590  {
1591  donutVerts[vertexIndex].Color = clr;
1592  donutVerts[vertexIndex].Position = new Vector3(center + Vector2.Transform(getDirection(vertexIndex) * getRadius(vertexIndex), Matrix.CreateRotationZ(rotationRad)), 0.0f);
1593  }
1594  sb.Draw(solidWhiteTexture, donutVerts, depth, count: maxDirectionIndex);
1595  }
1596 
1597  public static void DrawRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, bool isFilled = false, float depth = 0.0f, float thickness = 1)
1598  {
1599  if (size.X < 0)
1600  {
1601  start.X += size.X;
1602  size.X = -size.X;
1603  }
1604  if (size.Y < 0)
1605  {
1606  start.Y += size.Y;
1607  size.Y = -size.Y;
1608  }
1609  DrawRectangle(sb, new Rectangle((int)start.X, (int)start.Y, (int)size.X, (int)size.Y), clr, isFilled, depth, thickness);
1610  }
1611 
1612  public static void DrawRectangle(SpriteBatch sb, Rectangle rect, Color clr, bool isFilled = false, float depth = 0.0f, float thickness = 1)
1613  {
1614  if (isFilled)
1615  {
1616  sb.Draw(solidWhiteTexture, rect, null, clr, 0.0f, Vector2.Zero, SpriteEffects.None, depth);
1617  }
1618  else
1619  {
1620  Rectangle srcRect = new Rectangle(0, 0, 1, 1);
1621  sb.Draw(solidWhiteTexture, new Vector2(rect.X, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height), SpriteEffects.None, depth);
1622  sb.Draw(solidWhiteTexture, new Vector2(rect.X + thickness, rect.Y), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth);
1623  sb.Draw(solidWhiteTexture, new Vector2(rect.X + thickness, rect.Bottom - thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(rect.Width - thickness, thickness), SpriteEffects.None, depth);
1624  sb.Draw(solidWhiteTexture, new Vector2(rect.Right - thickness, rect.Y + thickness), srcRect, clr, 0.0f, Vector2.Zero, new Vector2(thickness, rect.Height - thickness * 2f), SpriteEffects.None, depth);
1625  }
1626  }
1627 
1628  public static void DrawRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 origin, float rotation, Color clr, float depth = 0.0f, float thickness = 1, OutlinePosition outlinePos = OutlinePosition.Centered)
1629  {
1630  Vector2 topLeft = new Vector2(-origin.X, -origin.Y);
1631  Vector2 topRight = new Vector2(-origin.X + size.X, -origin.Y);
1632  Vector2 bottomLeft = new Vector2(-origin.X, -origin.Y + size.Y);
1633  Vector2 actualSize = size;
1634 
1635  switch(outlinePos)
1636  {
1637  case OutlinePosition.Default:
1638  actualSize += new Vector2(thickness);
1639  break;
1640  case OutlinePosition.Centered:
1641  topLeft -= new Vector2(thickness * 0.5f);
1642  topRight -= new Vector2(thickness * 0.5f);
1643  bottomLeft -= new Vector2(thickness * 0.5f);
1644  actualSize += new Vector2(thickness);
1645  break;
1646  case OutlinePosition.Inside:
1647  topRight -= new Vector2(thickness, 0.0f);
1648  bottomLeft -= new Vector2(0.0f, thickness);
1649  break;
1650  case OutlinePosition.Outside:
1651  topLeft -= new Vector2(thickness);
1652  topRight -= new Vector2(0.0f, thickness);
1653  bottomLeft -= new Vector2(thickness, 0.0f);
1654  actualSize += new Vector2(thickness * 2.0f);
1655  break;
1656  }
1657 
1658  Matrix rotate = Matrix.CreateRotationZ(rotation);
1659  topLeft = Vector2.Transform(topLeft, rotate) + position;
1660  topRight = Vector2.Transform(topRight, rotate) + position;
1661  bottomLeft = Vector2.Transform(bottomLeft, rotate) + position;
1662 
1663  Rectangle srcRect = new Rectangle(0, 0, 1, 1);
1664  sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
1665  sb.Draw(solidWhiteTexture, topLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
1666  sb.Draw(solidWhiteTexture, topRight, srcRect, clr, rotation, Vector2.Zero, new Vector2(thickness, actualSize.Y), SpriteEffects.None, depth);
1667  sb.Draw(solidWhiteTexture, bottomLeft, srcRect, clr, rotation, Vector2.Zero, new Vector2(actualSize.X, thickness), SpriteEffects.None, depth);
1668  }
1669 
1670  public static void DrawFilledRectangle(SpriteBatch sb, Vector2 position, Vector2 size, Vector2 pivot, float rotation, Color clr, float depth = 0.0f)
1671  {
1672  Rectangle srcRect = new Rectangle(0, 0, 1, 1);
1673  sb.Draw(solidWhiteTexture, position, srcRect, clr, rotation, (pivot/size), size, SpriteEffects.None, depth);
1674  }
1675 
1676  public static void DrawFilledRectangle(SpriteBatch sb, RectangleF rect, Color clr, float depth = 0.0f)
1677  {
1678  DrawFilledRectangle(sb, rect.Location, rect.Size, clr, depth);
1679  }
1680 
1681  public static void DrawFilledRectangle(SpriteBatch sb, Vector2 start, Vector2 size, Color clr, float depth = 0.0f)
1682  {
1683  if (size.X < 0)
1684  {
1685  start.X += size.X;
1686  size.X = -size.X;
1687  }
1688  if (size.Y < 0)
1689  {
1690  start.Y += size.Y;
1691  size.Y = -size.Y;
1692  }
1693 
1694  sb.Draw(solidWhiteTexture, start, null, clr, 0f, Vector2.Zero, size, SpriteEffects.None, depth);
1695  }
1696 
1697  public static void DrawRectangle(SpriteBatch sb, Vector2 center, float width, float height, float rotation, Color clr, float depth = 0.0f, float thickness = 1)
1698  {
1699  Matrix rotate = Matrix.CreateRotationZ(rotation);
1700 
1701  width *= 0.5f;
1702  height *= 0.5f;
1703  Vector2 topLeft = center + Vector2.Transform(new Vector2(-width, -height), rotate);
1704  Vector2 topRight = center + Vector2.Transform(new Vector2(width, -height), rotate);
1705  Vector2 bottomLeft = center + Vector2.Transform(new Vector2(-width, height), rotate);
1706  Vector2 bottomRight = center + Vector2.Transform(new Vector2(width, height), rotate);
1707 
1708  DrawLine(sb, topLeft, topRight, clr, depth, thickness);
1709  DrawLine(sb, topRight, bottomRight, clr, depth, thickness);
1710  DrawLine(sb, bottomRight, bottomLeft, clr, depth, thickness);
1711  DrawLine(sb, bottomLeft, topLeft, clr, depth, thickness);
1712  }
1713 
1714  public static void DrawRectangle(SpriteBatch sb, Vector2[] corners, Color clr, float depth = 0.0f, float thickness = 1)
1715  {
1716  if (corners.Length != 4)
1717  {
1718  throw new Exception("Invalid length of the corners array! Must be 4");
1719  }
1720  DrawLine(sb, corners[0], corners[1], clr, depth, thickness);
1721  DrawLine(sb, corners[1], corners[2], clr, depth, thickness);
1722  DrawLine(sb, corners[2], corners[3], clr, depth, thickness);
1723  DrawLine(sb, corners[3], corners[0], clr, depth, thickness);
1724  }
1725 
1726  public static void DrawProgressBar(SpriteBatch sb, Vector2 start, Vector2 size, float progress, Color clr, float depth = 0.0f)
1727  {
1728  DrawProgressBar(sb, start, size, progress, clr, new Color(0.5f, 0.57f, 0.6f, 1.0f), depth);
1729  }
1730 
1731  public static void DrawProgressBar(SpriteBatch sb, Vector2 start, Vector2 size, float progress, Color clr, Color outlineColor, float depth = 0.0f)
1732  {
1733  DrawRectangle(sb, new Vector2(start.X, -start.Y), size, outlineColor, false, depth);
1734 
1735  int padding = 2;
1736  DrawRectangle(sb, new Rectangle((int)start.X + padding, -(int)(start.Y - padding), (int)((size.X - padding * 2) * progress), (int)size.Y - padding * 2),
1737  clr, true, depth);
1738  }
1739 
1740  public static bool DrawButton(SpriteBatch sb, Rectangle rect, string text, Color color, bool isHoldable = false)
1741  {
1742  bool clicked = false;
1743 
1744  if (rect.Contains(PlayerInput.MousePosition))
1745  {
1746  clicked = PlayerInput.PrimaryMouseButtonHeld();
1747 
1748  color = clicked ?
1749  new Color((int)(color.R * 0.8f), (int)(color.G * 0.8f), (int)(color.B * 0.8f), color.A) :
1750  new Color((int)(color.R * 1.2f), (int)(color.G * 1.2f), (int)(color.B * 1.2f), color.A);
1751 
1752  if (!isHoldable) clicked = PlayerInput.PrimaryMouseButtonClicked();
1753  }
1754 
1755  DrawRectangle(sb, rect, color, true);
1756 
1757  Vector2 origin;
1758  try
1759  {
1760  origin = GUIStyle.Font.MeasureString(text) / 2;
1761  }
1762  catch
1763  {
1764  origin = Vector2.Zero;
1765  }
1766 
1767  GUIStyle.Font.DrawString(sb, text, new Vector2(rect.Center.X, rect.Center.Y), Color.White, 0.0f, origin, 1.0f, SpriteEffects.None, 0.0f);
1768 
1769  return clicked;
1770  }
1771 
1772  private static void DrawMessages(SpriteBatch spriteBatch, Camera cam)
1773  {
1774  if (messages.Count == 0) { return; }
1775 
1776  bool useScissorRect = messages.Any(m => !m.WorldSpace);
1777  Rectangle prevScissorRect = spriteBatch.GraphicsDevice.ScissorRectangle;
1778  if (useScissorRect)
1779  {
1780  spriteBatch.End();
1781  spriteBatch.GraphicsDevice.ScissorRectangle = HUDLayoutSettings.MessageAreaTop;
1782  spriteBatch.Begin(SpriteSortMode.Deferred, rasterizerState: GameMain.ScissorTestEnable);
1783  }
1784 
1785  foreach (GUIMessage msg in messages)
1786  {
1787  if (msg.WorldSpace) { continue; }
1788 
1789  Vector2 drawPos = new Vector2(HUDLayoutSettings.MessageAreaTop.Right, HUDLayoutSettings.MessageAreaTop.Center.Y);
1790 
1791  msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos + Vector2.One, Color.Black, 0, msg.Origin, 1.0f, SpriteEffects.None, 0);
1792  msg.Font.DrawString(spriteBatch, msg.Text, drawPos + msg.DrawPos, msg.Color, 0, msg.Origin, 1.0f, SpriteEffects.None, 0);
1793  break;
1794  }
1795 
1796  if (useScissorRect)
1797  {
1798  spriteBatch.End();
1799  spriteBatch.GraphicsDevice.ScissorRectangle = prevScissorRect;
1800  spriteBatch.Begin(SpriteSortMode.Deferred);
1801  }
1802 
1803  foreach (GUIMessage msg in messages)
1804  {
1805  if (!msg.WorldSpace) { continue; }
1806 
1807  if (cam != null)
1808  {
1809  float alpha = 1.0f;
1810  if (msg.Timer < 1.0f) { alpha -= 1.0f - msg.Timer; }
1811 
1812  Vector2 drawPos = cam.WorldToScreen(msg.DrawPos);
1813  msg.Font.DrawString(spriteBatch, msg.Text, drawPos + Vector2.One, Color.Black * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0);
1814  msg.Font.DrawString(spriteBatch, msg.Text, drawPos, msg.Color * alpha, 0, msg.Origin, 1.0f, SpriteEffects.None, 0);
1815  }
1816  }
1817 
1818  messages.RemoveAll(m => m.Timer <= 0.0f);
1819  }
1820 
1824  public static void DrawBezierWithDots(SpriteBatch spriteBatch, Vector2 start, Vector2 end, Vector2 control, int pointCount, Color color, int dotSize = 2)
1825  {
1826  for (int i = 0; i < pointCount; i++)
1827  {
1828  float t = (float)i / (pointCount - 1);
1829  Vector2 pos = MathUtils.Bezier(start, control, end, t);
1830  ShapeExtensions.DrawPoint(spriteBatch, pos, color, dotSize);
1831  }
1832  }
1833 
1834  public static void DrawSineWithDots(SpriteBatch spriteBatch, Vector2 from, Vector2 dir, float amplitude, float length, float scale, int pointCount, Color color, int dotSize = 2)
1835  {
1836  Vector2 up = dir.Right();
1837  //DrawLine(spriteBatch, from, from + dir, GUIStyle.Red);
1838  //DrawLine(spriteBatch, from, from + up * dir.Length(), Color.Blue);
1839  for (int i = 0; i < pointCount; i++)
1840  {
1841  Vector2 pos = from;
1842  if (i > 0)
1843  {
1844  float t = (float)i / (pointCount - 1);
1845  float sin = (float)Math.Sin(t / length * scale) * amplitude;
1846  pos += (up * sin) + (dir * t);
1847  }
1848  ShapeExtensions.DrawPoint(spriteBatch, pos, color, dotSize);
1849  }
1850  }
1851 
1852  private static void DrawSavingIndicator(SpriteBatch spriteBatch)
1853  {
1854  if (!IsSavingIndicatorVisible || GUIStyle.SavingIndicator == null) { return; }
1855  var sheet = GUIStyle.SavingIndicator;
1856  Vector2 pos = new Vector2(GameMain.GraphicsWidth, GameMain.GraphicsHeight) - new Vector2(HUDLayoutSettings.Padding) - 2 * Scale * sheet.FrameSize.ToVector2();
1857  sheet.Draw(spriteBatch, (int)Math.Floor(savingIndicatorSpriteIndex), pos, savingIndicatorColor, origin: Vector2.Zero, rotate: 0.0f, scale: new Vector2(Scale));
1858  }
1859 
1860  public static void DrawCapsule(SpriteBatch sb, Vector2 origin, float length, float radius, float rotation, Color clr, float depth = 0, float thickness = 1)
1861  {
1862  DrawDonutSection(sb, origin + Vector2.Transform(-new Vector2(length / 2, 0), Matrix.CreateRotationZ(rotation)), new Range<float>(radius - thickness / 2, radius + thickness / 2), MathHelper.Pi, clr, depth, rotation - MathHelper.Pi);
1863  DrawRectangle(sb, origin, new Vector2(length, radius * 2), new Vector2(length / 2, radius), rotation, clr, depth, thickness);
1864  DrawDonutSection(sb, origin + Vector2.Transform(new Vector2(length / 2, 0), Matrix.CreateRotationZ(rotation)), new Range<float>(radius - thickness / 2, radius + thickness / 2), MathHelper.Pi, clr, depth, rotation);
1865  }
1866  #endregion
1867 
1868  #region Element creation
1869 
1870  public static Texture2D CreateCircle(int radius, bool filled = false)
1871  {
1872  int outerRadius = radius * 2 + 2; // So circle doesn't go out of bounds
1873 
1874  Color[] data = new Color[outerRadius * outerRadius];
1875 
1876  // Colour the entire texture transparent first.
1877  for (int i = 0; i < data.Length; i++)
1878  data[i] = Color.Transparent;
1879 
1880  if (filled)
1881  {
1882  float diameterSqr = radius * radius;
1883  for (int x = 0; x < outerRadius; x++)
1884  {
1885  for (int y = 0; y < outerRadius; y++)
1886  {
1887  Vector2 pos = new Vector2(radius - x, radius - y);
1888  if (pos.LengthSquared() <= diameterSqr)
1889  {
1890  TrySetArray(data, y * outerRadius + x + 1, Color.White);
1891  }
1892  }
1893  }
1894  }
1895  else
1896  {
1897  // Work out the minimum step necessary using trigonometry + sine approximation.
1898  double angleStep = 1f / radius;
1899 
1900  for (double angle = 0; angle < Math.PI * 2; angle += angleStep)
1901  {
1902  // Use the parametric definition of a circle: http://en.wikipedia.org/wiki/Circle#Cartesian_coordinates
1903  int x = (int)Math.Round(radius + radius * Math.Cos(angle));
1904  int y = (int)Math.Round(radius + radius * Math.Sin(angle));
1905 
1906  TrySetArray(data, y * outerRadius + x + 1, Color.White);
1907  }
1908  }
1909 
1910  Texture2D texture = null;
1911  CrossThread.RequestExecutionOnMainThread(() =>
1912  {
1913  texture = new Texture2D(GraphicsDevice, outerRadius, outerRadius);
1914  texture.SetData(data);
1915  });
1916  return texture;
1917  }
1918 
1919  public static Texture2D CreateCapsule(int radius, int height)
1920  {
1921  int textureWidth = Math.Max(radius * 2, 1);
1922  int textureHeight = Math.Max(height + radius * 2, 1);
1923 
1924  Color[] data = new Color[textureWidth * textureHeight];
1925 
1926  // Colour the entire texture transparent first.
1927  for (int i = 0; i < data.Length; i++)
1928  data[i] = Color.Transparent;
1929 
1930  // Work out the minimum step necessary using trigonometry + sine approximation.
1931  double angleStep = 1f / radius;
1932 
1933  for (int i = 0; i < 2; i++)
1934  {
1935  for (double angle = 0; angle < Math.PI * 2; angle += angleStep)
1936  {
1937  // Use the parametric definition of a circle: http://en.wikipedia.org/wiki/Circle#Cartesian_coordinates
1938  int x = (int)Math.Round(radius + radius * Math.Cos(angle));
1939  int y = (height - 1) * i + (int)Math.Round(radius + radius * Math.Sin(angle));
1940 
1941  TrySetArray(data, y * textureWidth + x, Color.White);
1942  }
1943  }
1944 
1945  for (int y = radius; y < textureHeight - radius; y++)
1946  {
1947  TrySetArray(data, y * textureWidth, Color.White);
1948  TrySetArray(data, y * textureWidth + (textureWidth - 1), Color.White);
1949  }
1950 
1951  Texture2D texture = null;
1952  CrossThread.RequestExecutionOnMainThread(() =>
1953  {
1954  texture = new Texture2D(GraphicsDevice, textureWidth, textureHeight);
1955  texture.SetData(data);
1956  });
1957  return texture;
1958  }
1959 
1960  public static Texture2D CreateRectangle(int width, int height)
1961  {
1962  width = Math.Max(width, 1);
1963  height = Math.Max(height, 1);
1964  Color[] data = new Color[width * height];
1965 
1966  for (int i = 0; i < data.Length; i++)
1967  data[i] = Color.Transparent;
1968 
1969  for (int y = 0; y < height; y++)
1970  {
1971  TrySetArray(data, y * width, Color.White);
1972  TrySetArray(data, y * width + (width - 1), Color.White);
1973  }
1974 
1975  for (int x = 0; x < width; x++)
1976  {
1977  TrySetArray(data, x, Color.White);
1978  TrySetArray(data, (height - 1) * width + x, Color.White);
1979  }
1980 
1981  Texture2D texture = null;
1982  CrossThread.RequestExecutionOnMainThread(() =>
1983  {
1984  texture = new Texture2D(GraphicsDevice, width, height);
1985  texture.SetData(data);
1986  });
1987  return texture;
1988  }
1989 
1990  private static bool TrySetArray(Color[] data, int index, Color value)
1991  {
1992  if (index >= 0 && index < data.Length)
1993  {
1994  data[index] = value;
1995  return true;
1996  }
1997  else
1998  {
1999  return false;
2000  }
2001  }
2002 
2006  public static List<GUIButton> CreateButtons(int count, Vector2 relativeSize, RectTransform parent,
2007  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
2008  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2009  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false,
2010  Alignment textAlignment = Alignment.Center, string style = "")
2011  {
2012  Func<RectTransform, GUIButton> constructor = rectT => new GUIButton(rectT, string.Empty, textAlignment, style);
2013  return CreateElements(count, relativeSize, parent, constructor, anchor, pivot, minSize, maxSize, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
2014  }
2015 
2019  public static List<GUIButton> CreateButtons(int count, Point absoluteSize, RectTransform parent,
2020  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null,
2021  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2022  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false,
2023  Alignment textAlignment = Alignment.Center, string style = "")
2024  {
2025  Func<RectTransform, GUIButton> constructor = rectT => new GUIButton(rectT, string.Empty, textAlignment, style);
2026  return CreateElements(count, absoluteSize, parent, constructor, anchor, pivot, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
2027  }
2028 
2032  public static List<T> CreateElements<T>(int count, Vector2 relativeSize, RectTransform parent, Func<RectTransform, T> constructor,
2033  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
2034  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2035  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false)
2036  where T : GUIComponent
2037  {
2038  return CreateElements(count, parent, constructor, relativeSize, null, anchor, pivot, minSize, maxSize, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
2039  }
2040 
2044  public static List<T> CreateElements<T>(int count, Point absoluteSize, RectTransform parent, Func<RectTransform, T> constructor,
2045  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null,
2046  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2047  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false)
2048  where T : GUIComponent
2049  {
2050  return CreateElements(count, parent, constructor, null, absoluteSize, anchor, pivot, null, null, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
2051  }
2052 
2053  public static GUIComponent CreateEnumField(Enum value, int elementHeight, LocalizedString name, RectTransform parent, string toolTip = null, GUIFont font = null)
2054  {
2055  font = font ?? GUIStyle.SmallFont;
2056  var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementHeight), parent), color: Color.Transparent);
2057  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform), name, font: font)
2058  {
2059  ToolTip = toolTip
2060  };
2061  GUIDropDown enumDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform, Anchor.TopRight),
2062  elementCount: Enum.GetValues(value.GetType()).Length)
2063  {
2064  ToolTip = toolTip
2065  };
2066  foreach (object enumValue in Enum.GetValues(value.GetType()))
2067  {
2068  enumDropDown.AddItem(enumValue.ToString(), enumValue);
2069  }
2070  enumDropDown.SelectItem(value);
2071  return frame;
2072  }
2073 
2074  public static GUIComponent CreateRectangleField(Rectangle value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null)
2075  {
2076  var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent);
2077  font = font ?? GUIStyle.SmallFont;
2078  new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), frame.RectTransform), name, font: font)
2079  {
2080  ToolTip = toolTip
2081  };
2082  var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight)
2083  {
2084  Stretch = true,
2085  RelativeSpacing = 0.01f
2086  };
2087  for (int i = 3; i >= 0; i--)
2088  {
2089  var element = new GUIFrame(new RectTransform(new Vector2(0.22f, 1), inputArea.RectTransform) { MinSize = new Point(50, 0), MaxSize = new Point(150, 50) }, style: null);
2090  new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), RectComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft);
2091  GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight),
2092  NumberType.Int)
2093  {
2094  Font = font
2095  };
2096  // Not sure if the min value could in any case be negative.
2097  numberInput.MinValueInt = 0;
2098  // Just something reasonable to keep the value in the input rect.
2099  numberInput.MaxValueInt = 9999;
2100  switch (i)
2101  {
2102  case 0:
2103  numberInput.IntValue = value.X;
2104  break;
2105  case 1:
2106  numberInput.IntValue = value.Y;
2107  break;
2108  case 2:
2109  numberInput.IntValue = value.Width;
2110  break;
2111  case 3:
2112  numberInput.IntValue = value.Height;
2113  break;
2114  }
2115  }
2116  return frame;
2117  }
2118 
2119  public static GUIComponent CreatePointField(Point value, int elementHeight, LocalizedString displayName, RectTransform parent, LocalizedString toolTip = null)
2120  {
2121  var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent);
2122  new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont)
2123  {
2124  ToolTip = toolTip
2125  };
2126  var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight)
2127  {
2128  Stretch = true,
2129  RelativeSpacing = 0.05f
2130  };
2131  for (int i = 1; i >= 0; i--)
2132  {
2133  var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null);
2134  new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft);
2135  GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight),
2136  NumberType.Int)
2137  {
2138  Font = GUIStyle.SmallFont
2139  };
2140 
2141  if (i == 0)
2142  numberInput.IntValue = value.X;
2143  else
2144  numberInput.IntValue = value.Y;
2145  }
2146  return frame;
2147  }
2148 
2149  public static GUIComponent CreateVector2Field(Vector2 value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null, int decimalsToDisplay = 1)
2150  {
2151  font = font ?? GUIStyle.SmallFont;
2152  var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent);
2153  new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), name, font: font)
2154  {
2155  ToolTip = toolTip
2156  };
2157  var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight)
2158  {
2159  Stretch = true,
2160  RelativeSpacing = 0.05f
2161  };
2162  for (int i = 1; i >= 0; i--)
2163  {
2164  var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null);
2165  new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft);
2166  GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Float) { Font = font };
2167  switch (i)
2168  {
2169  case 0:
2170  numberInput.FloatValue = value.X;
2171  break;
2172  case 1:
2173  numberInput.FloatValue = value.Y;
2174  break;
2175  }
2176  numberInput.DecimalsToDisplay = decimalsToDisplay;
2177  }
2178  return frame;
2179  }
2180 
2181  public static GUITextBox CreateTextBoxWithPlaceholder(RectTransform rectT, string text, LocalizedString placeholder)
2182  {
2183  var holder = new GUIFrame(rectT, style: null);
2184  var textBox = new GUITextBox(new RectTransform(Vector2.One, holder.RectTransform, Anchor.CenterLeft), text, createClearButton: false);
2185  var placeholderElement = new GUITextBlock(new RectTransform(Vector2.One, holder.RectTransform, Anchor.CenterLeft),
2186  textColor: Color.DarkGray * 0.6f,
2187  text: placeholder,
2188  textAlignment: Alignment.CenterLeft)
2189  {
2190  CanBeFocused = false
2191  };
2192 
2193  new GUICustomComponent(new RectTransform(Vector2.Zero, holder.RectTransform),
2194  onUpdate: delegate { placeholderElement.RectTransform.NonScaledSize = textBox.Frame.RectTransform.NonScaledSize; });
2195 
2196  textBox.OnSelected += delegate { placeholderElement.Visible = false; };
2197  textBox.OnDeselected += delegate { placeholderElement.Visible = textBox.Text.IsNullOrWhiteSpace(); };
2198 
2199  placeholderElement.Visible = string.IsNullOrWhiteSpace(text);
2200  return textBox;
2201  }
2202 
2203  public static void NotifyPrompt(LocalizedString header, LocalizedString body)
2204  {
2205  GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));
2206  msgBox.Buttons[0].OnClicked = delegate
2207  {
2208  msgBox.Close();
2209  return true;
2210  };
2211  }
2212 
2213  public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null, Vector2? relativeSize = null, Point? minSize = null)
2214  {
2215  LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") };
2216  GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, relativeSize: relativeSize ?? new Vector2(0.2f, 0.175f), minSize: minSize ?? new Point(300, 175));
2217 
2218  // Cancel button
2219  msgBox.Buttons[1].OnClicked = delegate
2220  {
2221  onDeny?.Invoke();
2222  msgBox.Close();
2223  return true;
2224  };
2225 
2226  // Ok button
2227  msgBox.Buttons[0].OnClicked = delegate
2228  {
2229  onConfirm.Invoke();
2230  msgBox.Close();
2231  return true;
2232  };
2233  return msgBox;
2234  }
2235 
2236  public static GUIMessageBox PromptTextInput(LocalizedString header, string body, Action<string> onConfirm)
2237  {
2238  LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") };
2239  GUIMessageBox msgBox = new GUIMessageBox(header, string.Empty, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));
2240  GUITextBox textBox = new GUITextBox(new RectTransform(Vector2.One, msgBox.Content.RectTransform), text: body)
2241  {
2242  OverflowClip = true
2243  };
2244 
2245  // Cancel button
2246  msgBox.Buttons[1].OnClicked = delegate
2247  {
2248  msgBox.Close();
2249  return true;
2250  };
2251 
2252  // Ok button
2253  msgBox.Buttons[0].OnClicked = delegate
2254  {
2255  onConfirm.Invoke(textBox.Text);
2256  msgBox.Close();
2257  return true;
2258  };
2259  return msgBox;
2260  }
2261 
2262 #endregion
2263 
2264 #region Element positioning
2265  private static List<T> CreateElements<T>(int count, RectTransform parent, Func<RectTransform, T> constructor,
2266  Vector2? relativeSize = null, Point? absoluteSize = null,
2267  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
2268  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2269  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false)
2270  where T : GUIComponent
2271  {
2272  var elements = new List<T>();
2273  int extraTotal = 0;
2274  for (int i = 0; i < count; i++)
2275  {
2276  if (extraSpacing != null)
2277  {
2278  extraTotal += extraSpacing(i);
2279  }
2280  if (relativeSize.HasValue)
2281  {
2282  var size = relativeSize.Value;
2283  var offsets = CalculateOffsets(size, startOffsetRelative, startOffsetAbsolute, relativeSpacing, absoluteSpacing, i, extraTotal, isHorizontal);
2284  elements.Add(constructor(new RectTransform(size, parent, anchor, pivot, minSize, maxSize)
2285  {
2286  RelativeOffset = offsets.Item1,
2287  AbsoluteOffset = offsets.Item2
2288  }));
2289  }
2290  else
2291  {
2292  var size = absoluteSize.Value;
2293  var offsets = CalculateOffsets(size, startOffsetRelative, startOffsetAbsolute, relativeSpacing, absoluteSpacing, i, extraTotal, isHorizontal);
2294  elements.Add(constructor(new RectTransform(size, parent, anchor, pivot)
2295  {
2296  RelativeOffset = offsets.Item1,
2297  AbsoluteOffset = offsets.Item2
2298  }));
2299  }
2300  }
2301  return elements;
2302  }
2303 
2304  private static Tuple<Vector2, Point> CalculateOffsets(Vector2 relativeSize, float startOffsetRelative, int startOffsetAbsolute, float relativeSpacing, int absoluteSpacing, int counter, int extra, bool isHorizontal)
2305  {
2306  float relX = 0, relY = 0;
2307  int absX = 0, absY = 0;
2308  if (isHorizontal)
2309  {
2310  relX = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, relativeSize.X, counter);
2311  absX = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, counter, extra);
2312  }
2313  else
2314  {
2315  relY = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, relativeSize.Y, counter);
2316  absY = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, counter, extra);
2317  }
2318  return Tuple.Create(new Vector2(relX, relY), new Point(absX, absY));
2319  }
2320 
2321  private static Tuple<Vector2, Point> CalculateOffsets(Point absoluteSize, float startOffsetRelative, int startOffsetAbsolute, float relativeSpacing, int absoluteSpacing, int counter, int extra, bool isHorizontal)
2322  {
2323  float relX = 0, relY = 0;
2324  int absX = 0, absY = 0;
2325  if (isHorizontal)
2326  {
2327  relX = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, counter);
2328  absX = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, absoluteSize.X, counter, extra);
2329  }
2330  else
2331  {
2332  relY = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, counter);
2333  absY = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, absoluteSize.Y, counter, extra);
2334  }
2335  return Tuple.Create(new Vector2(relX, relY), new Point(absX, absY));
2336  }
2337 
2338  private static float CalculateRelativeOffset(float startOffset, float spacing, float size, int counter)
2339  {
2340  return startOffset + (spacing + size) * counter;
2341  }
2342 
2343  private static float CalculateRelativeOffset(float startOffset, float spacing, int counter)
2344  {
2345  return startOffset + spacing * counter;
2346  }
2347 
2348  private static int CalculateAbsoluteOffset(int startOffset, int spacing, int counter, int extra)
2349  {
2350  return startOffset + spacing * counter + extra;
2351  }
2352 
2353  private static int CalculateAbsoluteOffset(int startOffset, int spacing, int size, int counter, int extra)
2354  {
2355  return startOffset + (spacing + size) * counter + extra;
2356  }
2357 
2364  public static void PreventElementOverlap(IList<GUIComponent> elements, IList<Rectangle> disallowedAreas = null, Rectangle? clampArea = null)
2365  {
2366  List<GUIComponent> sortedElements = elements.OrderByDescending(e => e.Rect.Width + e.Rect.Height).ToList();
2367 
2368  Rectangle area = clampArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
2369  for (int i = 0; i < sortedElements.Count; i++)
2370  {
2371  Point moveAmount = Point.Zero;
2372  Rectangle rect1 = sortedElements[i].Rect;
2373  moveAmount.X += Math.Max(area.X - rect1.X, 0);
2374  moveAmount.X -= Math.Max(rect1.Right - area.Right, 0);
2375  moveAmount.Y += Math.Max(area.Y - rect1.Y, 0);
2376  moveAmount.Y -= Math.Max(rect1.Bottom - area.Bottom, 0);
2377  sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount;
2378  }
2379 
2380  bool intersections = true;
2381  int iterations = 0;
2382  while (intersections && iterations < 100)
2383  {
2384  intersections = false;
2385  for (int i = 0; i < sortedElements.Count; i++)
2386  {
2387  Rectangle rect1 = sortedElements[i].Rect;
2388  for (int j = i + 1; j < sortedElements.Count; j++)
2389  {
2390  Rectangle rect2 = sortedElements[j].Rect;
2391  if (!rect1.Intersects(rect2)) { continue; }
2392 
2393  intersections = true;
2394  Point centerDiff = rect1.Center - rect2.Center;
2395  //move the interfaces away from each other, in a random direction if they're at the same position
2396  Vector2 moveAmount = centerDiff == Point.Zero ? Vector2.UnitX + Rand.Vector(0.1f) : Vector2.Normalize(centerDiff.ToVector2());
2397 
2398  //if the horizontal move amount is much larger than vertical, only move horizontally
2399  //(= attempt to place the elements side-by-side if they're more apart horizontally than vertically)
2400  if (Math.Abs(moveAmount.X) > Math.Abs(moveAmount.Y) * 8.0f)
2401  {
2402  moveAmount.Y = 0.0f;
2403  }
2404  //same for the y-axis
2405  else if (Math.Abs(moveAmount.Y) > Math.Abs(moveAmount.X) * 8.0f)
2406  {
2407  moveAmount.X = 0.0f;
2408  }
2409 
2410  //make sure we don't move the interfaces out of the screen
2411  Vector2 moveAmount1 = ClampMoveAmount(rect1, area, moveAmount * 10.0f);
2412  Vector2 moveAmount2 = ClampMoveAmount(rect2, area, -moveAmount * 10.0f);
2413 
2414  //move by 10 units in the desired direction and repeat until nothing overlaps
2415  //(or after 100 iterations, in which case we'll just give up and let them overlap)
2416  sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount1.ToPoint();
2417  sortedElements[j].RectTransform.ScreenSpaceOffset += moveAmount2.ToPoint();
2418  }
2419 
2420  if (disallowedAreas == null) { continue; }
2421  foreach (Rectangle rect2 in disallowedAreas)
2422  {
2423  if (!rect1.Intersects(rect2)) { continue; }
2424  intersections = true;
2425 
2426  Point centerDiff = rect1.Center - rect2.Center;
2427  //move the interface away from the disallowed area
2428  Vector2 moveAmount = centerDiff == Point.Zero ? Rand.Vector(1.0f) : Vector2.Normalize(centerDiff.ToVector2());
2429 
2430  //make sure we don't move the interface out of the screen
2431  Vector2 moveAmount1 = ClampMoveAmount(rect1, area, moveAmount * 10.0f);
2432 
2433  //move by 10 units in the desired direction and repeat until nothing overlaps
2434  //(or after 100 iterations, in which case we'll just give up and let them overlap)
2435  sortedElements[i].RectTransform.ScreenSpaceOffset += (moveAmount1).ToPoint();
2436  }
2437  }
2438  iterations++;
2439  }
2440  }
2441 
2442  private static Vector2 ClampMoveAmount(Rectangle rect, Rectangle clampTo, Vector2 moveAmount)
2443  {
2444  if (rect.Y < clampTo.Y)
2445  {
2446  moveAmount.Y = Math.Max(moveAmount.Y, 0.0f);
2447  }
2448  else if (rect.Bottom > clampTo.Bottom)
2449  {
2450  moveAmount.Y = Math.Min(moveAmount.Y, 0.0f);
2451  }
2452  if (rect.X < clampTo.X)
2453  {
2454  moveAmount.X = Math.Max(moveAmount.X, 0.0f);
2455  }
2456  else if (rect.Right > clampTo.Right)
2457  {
2458  moveAmount.X = Math.Min(moveAmount.X, 0.0f);
2459  }
2460  return moveAmount;
2461  }
2462 
2463 
2464  #endregion
2465 
2466 #region Misc
2467  public static void TogglePauseMenu()
2468  {
2469  if (Screen.Selected == GameMain.MainMenuScreen) { return; }
2470  if (PreventPauseMenuToggle) { return; }
2471 
2472  SettingsMenuOpen = false;
2473 
2474  TogglePauseMenu(null, null);
2475 
2476  if (PauseMenuOpen)
2477  {
2478  Inventory.DraggingItems.Clear();
2479  Inventory.DraggingInventory = null;
2480 
2481  PauseMenu = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null);
2482  new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PauseMenu.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
2483 
2484  var pauseMenuInner = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.3f), PauseMenu.RectTransform, Anchor.Center) { MinSize = new Point(250, 300) });
2485 
2486  float padding = 0.06f;
2487 
2488  var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.8f), pauseMenuInner.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, padding) })
2489  {
2490  AbsoluteSpacing = IntScale(15)
2491  };
2492 
2493  new GUIButton(new RectTransform(new Vector2(0.1f, 0.07f), pauseMenuInner.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(padding) },
2494  "", style: "GUIBugButton")
2495  {
2496  IgnoreLayoutGroups = true,
2497  ToolTip = TextManager.Get("bugreportbutton") + $" (v{GameMain.Version})",
2498  OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; }
2499  };
2500 
2501  CreateButton("PauseMenuResume", buttonContainer, null);
2502  CreateButton("PauseMenuSettings", buttonContainer, () => SettingsMenuOpen = true);
2503 
2504  bool IsFriendlyOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedFriendlyOutpost;
2505  if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null)
2506  {
2507  if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode)
2508  {
2509  CreateButton("PauseMenuRetry", buttonContainer, verificationTextTag: "PauseMenuRetryVerification", action: () =>
2510  {
2511  if (GameMain.GameSession.RoundSummary?.Frame != null)
2512  {
2513  GUIMessageBox.MessageBoxes.Remove(GameMain.GameSession.RoundSummary.Frame);
2514  }
2515  GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction");
2516  GameMain.GameSession.LoadPreviousSave();
2517  });
2518 
2519  if (IsFriendlyOutpostLevel() && !spMode.CrewDead)
2520  {
2521  CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToMainMenuVerification", action: () =>
2522  {
2523  if (IsFriendlyOutpostLevel()) { GameMain.QuitToMainMenu(save: true); }
2524  });
2525  }
2526  }
2527  else if (GameMain.GameSession.GameMode is TestGameMode)
2528  {
2529  CreateButton("PauseMenuReturnToEditor", buttonContainer, action: () =>
2530  {
2531  GameMain.GameSession?.EndRound("");
2532  });
2533  }
2534  else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound))
2535  {
2536  bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsFriendlyOutpostLevel();
2537  if (canSave)
2538  {
2539  CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToServerLobbyVerification", action: () =>
2540  {
2541  GameMain.Client?.RequestRoundEnd(save: true);
2542  });
2543  }
2544 
2545  CreateButton(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby" : "EndRound", buttonContainer,
2546  verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd",
2547  action: () =>
2548  {
2549  GameMain.Client?.RequestRoundEnd(save: false);
2550  });
2551  }
2552  }
2553 
2554  if (GameMain.GameSession != null || Screen.Selected is CharacterEditorScreen || Screen.Selected is SubEditorScreen)
2555  {
2556  CreateButton("PauseMenuQuit", buttonContainer,
2557  verificationTextTag: GameMain.GameSession == null ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification",
2558  action: () =>
2559  {
2560  GameMain.QuitToMainMenu(save: false);
2561  });
2562  }
2563  else
2564  {
2565  CreateButton("PauseMenuQuit", buttonContainer, action: () => { GameMain.QuitToMainMenu(save: false); });
2566  }
2567 
2568  GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock));
2569  //scale to ensure there's enough room for all the buttons
2570  pauseMenuInner.RectTransform.MinSize = new Point(
2571  pauseMenuInner.RectTransform.MinSize.X,
2572  Math.Max(
2573  (int)(buttonContainer.Children.Sum(c => c.Rect.Height + buttonContainer.AbsoluteSpacing) / buttonContainer.RectTransform.RelativeSize.Y),
2574  pauseMenuInner.RectTransform.MinSize.X));
2575 
2576  }
2577 
2578  void CreateButton(string textTag, GUIComponent parent, Action action, string verificationTextTag = null)
2579  {
2580  new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), TextManager.Get(textTag))
2581  {
2582  OnClicked = (btn, userData) =>
2583  {
2584  if (string.IsNullOrEmpty(verificationTextTag))
2585  {
2586  PauseMenuOpen = false;
2587  action?.Invoke();
2588  }
2589  else
2590  {
2591  CreateVerificationPrompt(verificationTextTag, action);
2592  }
2593  return true;
2594  }
2595  };
2596  }
2597 
2598  void CreateVerificationPrompt(string textTag, Action confirmAction)
2599  {
2600  var msgBox = new GUIMessageBox("", TextManager.Get(textTag),
2601  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") })
2602  {
2603  UserData = "verificationprompt",
2604  DrawOnTop = true
2605  };
2606  msgBox.Buttons[0].OnClicked = (_, __) =>
2607  {
2608  PauseMenuOpen = false;
2609  confirmAction?.Invoke();
2610  return true;
2611  };
2612  msgBox.Buttons[0].OnClicked += msgBox.Close;
2613  msgBox.Buttons[1].OnClicked += msgBox.Close;
2614  }
2615  }
2616 
2617  private static bool TogglePauseMenu(GUIButton button, object obj)
2618  {
2619  PauseMenuOpen = !PauseMenuOpen;
2620  if (!PauseMenuOpen && PauseMenu != null)
2621  {
2622  PauseMenu.RectTransform.Parent = null;
2623  PauseMenu = null;
2624  }
2625  return true;
2626  }
2627 
2632  public static void AddMessage(LocalizedString message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null)
2633  {
2634  AddMessage(message.Value, color, lifeTime, playSound, font);
2635  }
2636 
2637  public static void AddMessage(LocalizedString message, Color color, Vector2 pos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage, int subId = -1)
2638  {
2639  AddMessage(message.Value, color, pos, velocity, lifeTime, playSound, soundType, subId);
2640  }
2641 
2642  public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null)
2643  {
2644  var guiMessage = new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont);
2645  lock (mutex)
2646  {
2647  if (messages.Any(msg => msg.Text == message)) { return; }
2648  messages.Add(guiMessage);
2649  }
2650  if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); }
2651  }
2652 
2653  public static void AddMessage(string message, Color color, Vector2 pos, Vector2 velocity, float lifeTime = 3.0f, bool playSound = true, GUISoundType soundType = GUISoundType.UIMessage, int subId = -1)
2654  {
2655  Submarine sub = Submarine.Loaded.FirstOrDefault(s => s.ID == subId);
2656 
2657  var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, GUIStyle.Font, sub: sub);
2658  if (playSound) { SoundPlayer.PlayUISound(soundType); }
2659 
2660  lock (mutex)
2661  {
2662  bool overlapFound = true;
2663  int tries = 0;
2664  while (overlapFound)
2665  {
2666  overlapFound = false;
2667  foreach (var otherMessage in messages)
2668  {
2669  float xDiff = otherMessage.Pos.X - newMessage.Pos.X;
2670  if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; }
2671  float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y;
2672  if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; }
2673  Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f));
2674  if (moveDir.LengthSquared() > 0.0001f)
2675  {
2676  moveDir = Vector2.Normalize(moveDir);
2677  }
2678  else
2679  {
2680  moveDir = Rand.Vector(1.0f);
2681  }
2682  moveDir.Y = -Math.Abs(moveDir.Y);
2683  newMessage.Pos -= Vector2.UnitY * 10;
2684  }
2685  tries++;
2686  if (tries > 20) { break; }
2687  }
2688  messages.Add(newMessage);
2689  }
2690  }
2691 
2692  public static void ClearMessages()
2693  {
2694  lock (mutex)
2695  {
2696  messages.Clear();
2697  }
2698  }
2699 
2700  public static bool IsFourByThree()
2701  {
2702  float aspectRatio = HorizontalAspectRatio;
2703  return aspectRatio > 1.3f && aspectRatio < 1.4f;
2704  }
2705 
2706  public static void SetSavingIndicatorState(bool enabled)
2707  {
2708  if (enabled)
2709  {
2710  timeUntilSavingIndicatorDisabled = null;
2711  }
2712  isSavingIndicatorEnabled = enabled;
2713  }
2714 
2715  public static void DisableSavingIndicatorDelayed(float delay = 3.0f)
2716  {
2717  if (!isSavingIndicatorEnabled) { return; }
2718  timeUntilSavingIndicatorDisabled = delay;
2719  }
2720 #endregion
2721  }
2722 }
static GameMain Instance
Definition: GameMain.cs:144
readonly string Filename
Definition: Sound.cs:19
static ? SocialOverlay Instance
void AddToGuiUpdateList()
NumberType
Definition: Enums.cs:741
GUISoundType
Definition: GUI.cs:21
CursorState
Definition: GUI.cs:40
@ Character
Characters only