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.SOURCE_COUNT; 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)
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 + getDirection(vertexIndex) * getRadius(vertexIndex), 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 #endregion
1860 
1861 #region Element creation
1862 
1863  public static Texture2D CreateCircle(int radius, bool filled = false)
1864  {
1865  int outerRadius = radius * 2 + 2; // So circle doesn't go out of bounds
1866 
1867  Color[] data = new Color[outerRadius * outerRadius];
1868 
1869  // Colour the entire texture transparent first.
1870  for (int i = 0; i < data.Length; i++)
1871  data[i] = Color.Transparent;
1872 
1873  if (filled)
1874  {
1875  float diameterSqr = radius * radius;
1876  for (int x = 0; x < outerRadius; x++)
1877  {
1878  for (int y = 0; y < outerRadius; y++)
1879  {
1880  Vector2 pos = new Vector2(radius - x, radius - y);
1881  if (pos.LengthSquared() <= diameterSqr)
1882  {
1883  TrySetArray(data, y * outerRadius + x + 1, Color.White);
1884  }
1885  }
1886  }
1887  }
1888  else
1889  {
1890  // Work out the minimum step necessary using trigonometry + sine approximation.
1891  double angleStep = 1f / radius;
1892 
1893  for (double angle = 0; angle < Math.PI * 2; angle += angleStep)
1894  {
1895  // Use the parametric definition of a circle: http://en.wikipedia.org/wiki/Circle#Cartesian_coordinates
1896  int x = (int)Math.Round(radius + radius * Math.Cos(angle));
1897  int y = (int)Math.Round(radius + radius * Math.Sin(angle));
1898 
1899  TrySetArray(data, y * outerRadius + x + 1, Color.White);
1900  }
1901  }
1902 
1903  Texture2D texture = null;
1904  CrossThread.RequestExecutionOnMainThread(() =>
1905  {
1906  texture = new Texture2D(GraphicsDevice, outerRadius, outerRadius);
1907  texture.SetData(data);
1908  });
1909  return texture;
1910  }
1911 
1912  public static Texture2D CreateCapsule(int radius, int height)
1913  {
1914  int textureWidth = Math.Max(radius * 2, 1);
1915  int textureHeight = Math.Max(height + radius * 2, 1);
1916 
1917  Color[] data = new Color[textureWidth * textureHeight];
1918 
1919  // Colour the entire texture transparent first.
1920  for (int i = 0; i < data.Length; i++)
1921  data[i] = Color.Transparent;
1922 
1923  // Work out the minimum step necessary using trigonometry + sine approximation.
1924  double angleStep = 1f / radius;
1925 
1926  for (int i = 0; i < 2; i++)
1927  {
1928  for (double angle = 0; angle < Math.PI * 2; angle += angleStep)
1929  {
1930  // Use the parametric definition of a circle: http://en.wikipedia.org/wiki/Circle#Cartesian_coordinates
1931  int x = (int)Math.Round(radius + radius * Math.Cos(angle));
1932  int y = (height - 1) * i + (int)Math.Round(radius + radius * Math.Sin(angle));
1933 
1934  TrySetArray(data, y * textureWidth + x, Color.White);
1935  }
1936  }
1937 
1938  for (int y = radius; y < textureHeight - radius; y++)
1939  {
1940  TrySetArray(data, y * textureWidth, Color.White);
1941  TrySetArray(data, y * textureWidth + (textureWidth - 1), Color.White);
1942  }
1943 
1944  Texture2D texture = null;
1945  CrossThread.RequestExecutionOnMainThread(() =>
1946  {
1947  texture = new Texture2D(GraphicsDevice, textureWidth, textureHeight);
1948  texture.SetData(data);
1949  });
1950  return texture;
1951  }
1952 
1953  public static Texture2D CreateRectangle(int width, int height)
1954  {
1955  width = Math.Max(width, 1);
1956  height = Math.Max(height, 1);
1957  Color[] data = new Color[width * height];
1958 
1959  for (int i = 0; i < data.Length; i++)
1960  data[i] = Color.Transparent;
1961 
1962  for (int y = 0; y < height; y++)
1963  {
1964  TrySetArray(data, y * width, Color.White);
1965  TrySetArray(data, y * width + (width - 1), Color.White);
1966  }
1967 
1968  for (int x = 0; x < width; x++)
1969  {
1970  TrySetArray(data, x, Color.White);
1971  TrySetArray(data, (height - 1) * width + x, Color.White);
1972  }
1973 
1974  Texture2D texture = null;
1975  CrossThread.RequestExecutionOnMainThread(() =>
1976  {
1977  texture = new Texture2D(GraphicsDevice, width, height);
1978  texture.SetData(data);
1979  });
1980  return texture;
1981  }
1982 
1983  private static bool TrySetArray(Color[] data, int index, Color value)
1984  {
1985  if (index >= 0 && index < data.Length)
1986  {
1987  data[index] = value;
1988  return true;
1989  }
1990  else
1991  {
1992  return false;
1993  }
1994  }
1995 
1999  public static List<GUIButton> CreateButtons(int count, Vector2 relativeSize, RectTransform parent,
2000  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
2001  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2002  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false,
2003  Alignment textAlignment = Alignment.Center, string style = "")
2004  {
2005  Func<RectTransform, GUIButton> constructor = rectT => new GUIButton(rectT, string.Empty, textAlignment, style);
2006  return CreateElements(count, relativeSize, parent, constructor, anchor, pivot, minSize, maxSize, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
2007  }
2008 
2012  public static List<GUIButton> CreateButtons(int count, Point absoluteSize, RectTransform parent,
2013  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null,
2014  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2015  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false,
2016  Alignment textAlignment = Alignment.Center, string style = "")
2017  {
2018  Func<RectTransform, GUIButton> constructor = rectT => new GUIButton(rectT, string.Empty, textAlignment, style);
2019  return CreateElements(count, absoluteSize, parent, constructor, anchor, pivot, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
2020  }
2021 
2025  public static List<T> CreateElements<T>(int count, Vector2 relativeSize, RectTransform parent, Func<RectTransform, T> constructor,
2026  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
2027  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2028  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false)
2029  where T : GUIComponent
2030  {
2031  return CreateElements(count, parent, constructor, relativeSize, null, anchor, pivot, minSize, maxSize, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
2032  }
2033 
2037  public static List<T> CreateElements<T>(int count, Point absoluteSize, RectTransform parent, Func<RectTransform, T> constructor,
2038  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null,
2039  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2040  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false)
2041  where T : GUIComponent
2042  {
2043  return CreateElements(count, parent, constructor, null, absoluteSize, anchor, pivot, null, null, absoluteSpacing, relativeSpacing, extraSpacing, startOffsetAbsolute, startOffsetRelative, isHorizontal);
2044  }
2045 
2046  public static GUIComponent CreateEnumField(Enum value, int elementHeight, LocalizedString name, RectTransform parent, string toolTip = null, GUIFont font = null)
2047  {
2048  font = font ?? GUIStyle.SmallFont;
2049  var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, elementHeight), parent), color: Color.Transparent);
2050  new GUITextBlock(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform), name, font: font)
2051  {
2052  ToolTip = toolTip
2053  };
2054  GUIDropDown enumDropDown = new GUIDropDown(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform, Anchor.TopRight),
2055  elementCount: Enum.GetValues(value.GetType()).Length)
2056  {
2057  ToolTip = toolTip
2058  };
2059  foreach (object enumValue in Enum.GetValues(value.GetType()))
2060  {
2061  enumDropDown.AddItem(enumValue.ToString(), enumValue);
2062  }
2063  enumDropDown.SelectItem(value);
2064  return frame;
2065  }
2066 
2067  public static GUIComponent CreateRectangleField(Rectangle value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null)
2068  {
2069  var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent);
2070  font = font ?? GUIStyle.SmallFont;
2071  new GUITextBlock(new RectTransform(new Vector2(0.2f, 1), frame.RectTransform), name, font: font)
2072  {
2073  ToolTip = toolTip
2074  };
2075  var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.8f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight)
2076  {
2077  Stretch = true,
2078  RelativeSpacing = 0.01f
2079  };
2080  for (int i = 3; i >= 0; i--)
2081  {
2082  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);
2083  new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), RectComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft);
2084  GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight),
2085  NumberType.Int)
2086  {
2087  Font = font
2088  };
2089  // Not sure if the min value could in any case be negative.
2090  numberInput.MinValueInt = 0;
2091  // Just something reasonable to keep the value in the input rect.
2092  numberInput.MaxValueInt = 9999;
2093  switch (i)
2094  {
2095  case 0:
2096  numberInput.IntValue = value.X;
2097  break;
2098  case 1:
2099  numberInput.IntValue = value.Y;
2100  break;
2101  case 2:
2102  numberInput.IntValue = value.Width;
2103  break;
2104  case 3:
2105  numberInput.IntValue = value.Height;
2106  break;
2107  }
2108  }
2109  return frame;
2110  }
2111 
2112  public static GUIComponent CreatePointField(Point value, int elementHeight, LocalizedString displayName, RectTransform parent, LocalizedString toolTip = null)
2113  {
2114  var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent);
2115  new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), displayName, font: GUIStyle.SmallFont)
2116  {
2117  ToolTip = toolTip
2118  };
2119  var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight)
2120  {
2121  Stretch = true,
2122  RelativeSpacing = 0.05f
2123  };
2124  for (int i = 1; i >= 0; i--)
2125  {
2126  var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null);
2127  new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: GUIStyle.SmallFont, textAlignment: Alignment.CenterLeft);
2128  GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight),
2129  NumberType.Int)
2130  {
2131  Font = GUIStyle.SmallFont
2132  };
2133 
2134  if (i == 0)
2135  numberInput.IntValue = value.X;
2136  else
2137  numberInput.IntValue = value.Y;
2138  }
2139  return frame;
2140  }
2141 
2142  public static GUIComponent CreateVector2Field(Vector2 value, int elementHeight, LocalizedString name, RectTransform parent, LocalizedString toolTip = null, GUIFont font = null, int decimalsToDisplay = 1)
2143  {
2144  font = font ?? GUIStyle.SmallFont;
2145  var frame = new GUIFrame(new RectTransform(new Point(parent.Rect.Width, Math.Max(elementHeight, 26)), parent), color: Color.Transparent);
2146  new GUITextBlock(new RectTransform(new Vector2(0.4f, 1), frame.RectTransform), name, font: font)
2147  {
2148  ToolTip = toolTip
2149  };
2150  var inputArea = new GUILayoutGroup(new RectTransform(new Vector2(0.6f, 1), frame.RectTransform, Anchor.TopRight), isHorizontal: true, childAnchor: Anchor.CenterRight)
2151  {
2152  Stretch = true,
2153  RelativeSpacing = 0.05f
2154  };
2155  for (int i = 1; i >= 0; i--)
2156  {
2157  var element = new GUIFrame(new RectTransform(new Vector2(0.45f, 1), inputArea.RectTransform), style: null);
2158  new GUITextBlock(new RectTransform(new Vector2(0.3f, 1), element.RectTransform, Anchor.CenterLeft), VectorComponentLabels[i], font: font, textAlignment: Alignment.CenterLeft);
2159  GUINumberInput numberInput = new GUINumberInput(new RectTransform(new Vector2(0.7f, 1), element.RectTransform, Anchor.CenterRight), NumberType.Float) { Font = font };
2160  switch (i)
2161  {
2162  case 0:
2163  numberInput.FloatValue = value.X;
2164  break;
2165  case 1:
2166  numberInput.FloatValue = value.Y;
2167  break;
2168  }
2169  numberInput.DecimalsToDisplay = decimalsToDisplay;
2170  }
2171  return frame;
2172  }
2173 
2174  public static GUITextBox CreateTextBoxWithPlaceholder(RectTransform rectT, string text, LocalizedString placeholder)
2175  {
2176  var holder = new GUIFrame(rectT, style: null);
2177  var textBox = new GUITextBox(new RectTransform(Vector2.One, holder.RectTransform, Anchor.CenterLeft), text, createClearButton: false);
2178  var placeholderElement = new GUITextBlock(new RectTransform(Vector2.One, holder.RectTransform, Anchor.CenterLeft),
2179  textColor: Color.DarkGray * 0.6f,
2180  text: placeholder,
2181  textAlignment: Alignment.CenterLeft)
2182  {
2183  CanBeFocused = false
2184  };
2185 
2186  new GUICustomComponent(new RectTransform(Vector2.Zero, holder.RectTransform),
2187  onUpdate: delegate { placeholderElement.RectTransform.NonScaledSize = textBox.Frame.RectTransform.NonScaledSize; });
2188 
2189  textBox.OnSelected += delegate { placeholderElement.Visible = false; };
2190  textBox.OnDeselected += delegate { placeholderElement.Visible = textBox.Text.IsNullOrWhiteSpace(); };
2191 
2192  placeholderElement.Visible = string.IsNullOrWhiteSpace(text);
2193  return textBox;
2194  }
2195 
2196  public static void NotifyPrompt(LocalizedString header, LocalizedString body)
2197  {
2198  GUIMessageBox msgBox = new GUIMessageBox(header, body, new[] { TextManager.Get("Ok") }, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));
2199  msgBox.Buttons[0].OnClicked = delegate
2200  {
2201  msgBox.Close();
2202  return true;
2203  };
2204  }
2205 
2206  public static GUIMessageBox AskForConfirmation(LocalizedString header, LocalizedString body, Action onConfirm, Action onDeny = null, Vector2? relativeSize = null, Point? minSize = null)
2207  {
2208  LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") };
2209  GUIMessageBox msgBox = new GUIMessageBox(header, body, buttons, relativeSize: relativeSize ?? new Vector2(0.2f, 0.175f), minSize: minSize ?? new Point(300, 175));
2210 
2211  // Cancel button
2212  msgBox.Buttons[1].OnClicked = delegate
2213  {
2214  onDeny?.Invoke();
2215  msgBox.Close();
2216  return true;
2217  };
2218 
2219  // Ok button
2220  msgBox.Buttons[0].OnClicked = delegate
2221  {
2222  onConfirm.Invoke();
2223  msgBox.Close();
2224  return true;
2225  };
2226  return msgBox;
2227  }
2228 
2229  public static GUIMessageBox PromptTextInput(LocalizedString header, string body, Action<string> onConfirm)
2230  {
2231  LocalizedString[] buttons = { TextManager.Get("Ok"), TextManager.Get("Cancel") };
2232  GUIMessageBox msgBox = new GUIMessageBox(header, string.Empty, buttons, new Vector2(0.2f, 0.175f), minSize: new Point(300, 175));
2233  GUITextBox textBox = new GUITextBox(new RectTransform(Vector2.One, msgBox.Content.RectTransform), text: body)
2234  {
2235  OverflowClip = true
2236  };
2237 
2238  // Cancel button
2239  msgBox.Buttons[1].OnClicked = delegate
2240  {
2241  msgBox.Close();
2242  return true;
2243  };
2244 
2245  // Ok button
2246  msgBox.Buttons[0].OnClicked = delegate
2247  {
2248  onConfirm.Invoke(textBox.Text);
2249  msgBox.Close();
2250  return true;
2251  };
2252  return msgBox;
2253  }
2254 
2255 #endregion
2256 
2257 #region Element positioning
2258  private static List<T> CreateElements<T>(int count, RectTransform parent, Func<RectTransform, T> constructor,
2259  Vector2? relativeSize = null, Point? absoluteSize = null,
2260  Anchor anchor = Anchor.TopLeft, Pivot? pivot = null, Point? minSize = null, Point? maxSize = null,
2261  int absoluteSpacing = 0, float relativeSpacing = 0, Func<int, int> extraSpacing = null,
2262  int startOffsetAbsolute = 0, float startOffsetRelative = 0, bool isHorizontal = false)
2263  where T : GUIComponent
2264  {
2265  var elements = new List<T>();
2266  int extraTotal = 0;
2267  for (int i = 0; i < count; i++)
2268  {
2269  if (extraSpacing != null)
2270  {
2271  extraTotal += extraSpacing(i);
2272  }
2273  if (relativeSize.HasValue)
2274  {
2275  var size = relativeSize.Value;
2276  var offsets = CalculateOffsets(size, startOffsetRelative, startOffsetAbsolute, relativeSpacing, absoluteSpacing, i, extraTotal, isHorizontal);
2277  elements.Add(constructor(new RectTransform(size, parent, anchor, pivot, minSize, maxSize)
2278  {
2279  RelativeOffset = offsets.Item1,
2280  AbsoluteOffset = offsets.Item2
2281  }));
2282  }
2283  else
2284  {
2285  var size = absoluteSize.Value;
2286  var offsets = CalculateOffsets(size, startOffsetRelative, startOffsetAbsolute, relativeSpacing, absoluteSpacing, i, extraTotal, isHorizontal);
2287  elements.Add(constructor(new RectTransform(size, parent, anchor, pivot)
2288  {
2289  RelativeOffset = offsets.Item1,
2290  AbsoluteOffset = offsets.Item2
2291  }));
2292  }
2293  }
2294  return elements;
2295  }
2296 
2297  private static Tuple<Vector2, Point> CalculateOffsets(Vector2 relativeSize, float startOffsetRelative, int startOffsetAbsolute, float relativeSpacing, int absoluteSpacing, int counter, int extra, bool isHorizontal)
2298  {
2299  float relX = 0, relY = 0;
2300  int absX = 0, absY = 0;
2301  if (isHorizontal)
2302  {
2303  relX = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, relativeSize.X, counter);
2304  absX = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, counter, extra);
2305  }
2306  else
2307  {
2308  relY = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, relativeSize.Y, counter);
2309  absY = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, counter, extra);
2310  }
2311  return Tuple.Create(new Vector2(relX, relY), new Point(absX, absY));
2312  }
2313 
2314  private static Tuple<Vector2, Point> CalculateOffsets(Point absoluteSize, float startOffsetRelative, int startOffsetAbsolute, float relativeSpacing, int absoluteSpacing, int counter, int extra, bool isHorizontal)
2315  {
2316  float relX = 0, relY = 0;
2317  int absX = 0, absY = 0;
2318  if (isHorizontal)
2319  {
2320  relX = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, counter);
2321  absX = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, absoluteSize.X, counter, extra);
2322  }
2323  else
2324  {
2325  relY = CalculateRelativeOffset(startOffsetRelative, relativeSpacing, counter);
2326  absY = CalculateAbsoluteOffset(startOffsetAbsolute, absoluteSpacing, absoluteSize.Y, counter, extra);
2327  }
2328  return Tuple.Create(new Vector2(relX, relY), new Point(absX, absY));
2329  }
2330 
2331  private static float CalculateRelativeOffset(float startOffset, float spacing, float size, int counter)
2332  {
2333  return startOffset + (spacing + size) * counter;
2334  }
2335 
2336  private static float CalculateRelativeOffset(float startOffset, float spacing, int counter)
2337  {
2338  return startOffset + spacing * counter;
2339  }
2340 
2341  private static int CalculateAbsoluteOffset(int startOffset, int spacing, int counter, int extra)
2342  {
2343  return startOffset + spacing * counter + extra;
2344  }
2345 
2346  private static int CalculateAbsoluteOffset(int startOffset, int spacing, int size, int counter, int extra)
2347  {
2348  return startOffset + (spacing + size) * counter + extra;
2349  }
2350 
2357  public static void PreventElementOverlap(IList<GUIComponent> elements, IList<Rectangle> disallowedAreas = null, Rectangle? clampArea = null)
2358  {
2359  List<GUIComponent> sortedElements = elements.OrderByDescending(e => e.Rect.Width + e.Rect.Height).ToList();
2360 
2361  Rectangle area = clampArea ?? new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight);
2362  for (int i = 0; i < sortedElements.Count; i++)
2363  {
2364  Point moveAmount = Point.Zero;
2365  Rectangle rect1 = sortedElements[i].Rect;
2366  moveAmount.X += Math.Max(area.X - rect1.X, 0);
2367  moveAmount.X -= Math.Max(rect1.Right - area.Right, 0);
2368  moveAmount.Y += Math.Max(area.Y - rect1.Y, 0);
2369  moveAmount.Y -= Math.Max(rect1.Bottom - area.Bottom, 0);
2370  sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount;
2371  }
2372 
2373  bool intersections = true;
2374  int iterations = 0;
2375  while (intersections && iterations < 100)
2376  {
2377  intersections = false;
2378  for (int i = 0; i < sortedElements.Count; i++)
2379  {
2380  Rectangle rect1 = sortedElements[i].Rect;
2381  for (int j = i + 1; j < sortedElements.Count; j++)
2382  {
2383  Rectangle rect2 = sortedElements[j].Rect;
2384  if (!rect1.Intersects(rect2)) { continue; }
2385 
2386  intersections = true;
2387  Point centerDiff = rect1.Center - rect2.Center;
2388  //move the interfaces away from each other, in a random direction if they're at the same position
2389  Vector2 moveAmount = centerDiff == Point.Zero ? Vector2.UnitX + Rand.Vector(0.1f) : Vector2.Normalize(centerDiff.ToVector2());
2390 
2391  //if the horizontal move amount is much larger than vertical, only move horizontally
2392  //(= attempt to place the elements side-by-side if they're more apart horizontally than vertically)
2393  if (Math.Abs(moveAmount.X) > Math.Abs(moveAmount.Y) * 8.0f)
2394  {
2395  moveAmount.Y = 0.0f;
2396  }
2397  //same for the y-axis
2398  else if (Math.Abs(moveAmount.Y) > Math.Abs(moveAmount.X) * 8.0f)
2399  {
2400  moveAmount.X = 0.0f;
2401  }
2402 
2403  //make sure we don't move the interfaces out of the screen
2404  Vector2 moveAmount1 = ClampMoveAmount(rect1, area, moveAmount * 10.0f);
2405  Vector2 moveAmount2 = ClampMoveAmount(rect2, area, -moveAmount * 10.0f);
2406 
2407  //move by 10 units in the desired direction and repeat until nothing overlaps
2408  //(or after 100 iterations, in which case we'll just give up and let them overlap)
2409  sortedElements[i].RectTransform.ScreenSpaceOffset += moveAmount1.ToPoint();
2410  sortedElements[j].RectTransform.ScreenSpaceOffset += moveAmount2.ToPoint();
2411  }
2412 
2413  if (disallowedAreas == null) { continue; }
2414  foreach (Rectangle rect2 in disallowedAreas)
2415  {
2416  if (!rect1.Intersects(rect2)) { continue; }
2417  intersections = true;
2418 
2419  Point centerDiff = rect1.Center - rect2.Center;
2420  //move the interface away from the disallowed area
2421  Vector2 moveAmount = centerDiff == Point.Zero ? Rand.Vector(1.0f) : Vector2.Normalize(centerDiff.ToVector2());
2422 
2423  //make sure we don't move the interface out of the screen
2424  Vector2 moveAmount1 = ClampMoveAmount(rect1, area, moveAmount * 10.0f);
2425 
2426  //move by 10 units in the desired direction and repeat until nothing overlaps
2427  //(or after 100 iterations, in which case we'll just give up and let them overlap)
2428  sortedElements[i].RectTransform.ScreenSpaceOffset += (moveAmount1).ToPoint();
2429  }
2430  }
2431  iterations++;
2432  }
2433  }
2434 
2435  private static Vector2 ClampMoveAmount(Rectangle rect, Rectangle clampTo, Vector2 moveAmount)
2436  {
2437  if (rect.Y < clampTo.Y)
2438  {
2439  moveAmount.Y = Math.Max(moveAmount.Y, 0.0f);
2440  }
2441  else if (rect.Bottom > clampTo.Bottom)
2442  {
2443  moveAmount.Y = Math.Min(moveAmount.Y, 0.0f);
2444  }
2445  if (rect.X < clampTo.X)
2446  {
2447  moveAmount.X = Math.Max(moveAmount.X, 0.0f);
2448  }
2449  else if (rect.Right > clampTo.Right)
2450  {
2451  moveAmount.X = Math.Min(moveAmount.X, 0.0f);
2452  }
2453  return moveAmount;
2454  }
2455 
2456 
2457  #endregion
2458 
2459 #region Misc
2460  public static void TogglePauseMenu()
2461  {
2462  if (Screen.Selected == GameMain.MainMenuScreen) { return; }
2463  if (PreventPauseMenuToggle) { return; }
2464 
2465  SettingsMenuOpen = false;
2466 
2467  TogglePauseMenu(null, null);
2468 
2469  if (PauseMenuOpen)
2470  {
2471  Inventory.DraggingItems.Clear();
2472  Inventory.DraggingInventory = null;
2473 
2474  PauseMenu = new GUIFrame(new RectTransform(Vector2.One, Canvas, Anchor.Center), style: null);
2475  new GUIFrame(new RectTransform(GUI.Canvas.RelativeSize, PauseMenu.RectTransform, Anchor.Center), style: "GUIBackgroundBlocker");
2476 
2477  var pauseMenuInner = new GUIFrame(new RectTransform(new Vector2(0.13f, 0.3f), PauseMenu.RectTransform, Anchor.Center) { MinSize = new Point(250, 300) });
2478 
2479  float padding = 0.06f;
2480 
2481  var buttonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.7f, 0.8f), pauseMenuInner.RectTransform, Anchor.BottomCenter) { RelativeOffset = new Vector2(0.0f, padding) })
2482  {
2483  AbsoluteSpacing = IntScale(15)
2484  };
2485 
2486  new GUIButton(new RectTransform(new Vector2(0.1f, 0.07f), pauseMenuInner.RectTransform, Anchor.TopRight) { RelativeOffset = new Vector2(padding) },
2487  "", style: "GUIBugButton")
2488  {
2489  IgnoreLayoutGroups = true,
2490  ToolTip = TextManager.Get("bugreportbutton") + $" (v{GameMain.Version})",
2491  OnClicked = (btn, userdata) => { GameMain.Instance.ShowBugReporter(); return true; }
2492  };
2493 
2494  CreateButton("PauseMenuResume", buttonContainer, null);
2495  CreateButton("PauseMenuSettings", buttonContainer, () => SettingsMenuOpen = true);
2496 
2497  bool IsFriendlyOutpostLevel() => GameMain.GameSession != null && Level.IsLoadedFriendlyOutpost;
2498  if (Screen.Selected == GameMain.GameScreen && GameMain.GameSession != null)
2499  {
2500  if (GameMain.GameSession.GameMode is SinglePlayerCampaign spMode)
2501  {
2502  CreateButton("PauseMenuRetry", buttonContainer, verificationTextTag: "PauseMenuRetryVerification", action: () =>
2503  {
2504  if (GameMain.GameSession.RoundSummary?.Frame != null)
2505  {
2506  GUIMessageBox.MessageBoxes.Remove(GameMain.GameSession.RoundSummary.Frame);
2507  }
2508  GUIMessageBox.MessageBoxes.RemoveAll(mb => mb.UserData as string == "ConversationAction");
2509  GameMain.GameSession.LoadPreviousSave();
2510  });
2511 
2512  if (IsFriendlyOutpostLevel() && !spMode.CrewDead)
2513  {
2514  CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToMainMenuVerification", action: () =>
2515  {
2516  if (IsFriendlyOutpostLevel()) { GameMain.QuitToMainMenu(save: true); }
2517  });
2518  }
2519  }
2520  else if (GameMain.GameSession.GameMode is TestGameMode)
2521  {
2522  CreateButton("PauseMenuReturnToEditor", buttonContainer, action: () =>
2523  {
2524  GameMain.GameSession?.EndRound("");
2525  });
2526  }
2527  else if (!GameMain.GameSession.GameMode.IsSinglePlayer && GameMain.Client != null && GameMain.Client.HasPermission(ClientPermissions.ManageRound))
2528  {
2529  bool canSave = GameMain.GameSession.GameMode is CampaignMode && IsFriendlyOutpostLevel();
2530  if (canSave)
2531  {
2532  CreateButton("PauseMenuSaveQuit", buttonContainer, verificationTextTag: "PauseMenuSaveAndReturnToServerLobbyVerification", action: () =>
2533  {
2534  GameMain.Client?.RequestRoundEnd(save: true);
2535  });
2536  }
2537 
2538  CreateButton(GameMain.GameSession.GameMode is CampaignMode ? "ReturnToServerlobby" : "EndRound", buttonContainer,
2539  verificationTextTag: GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd",
2540  action: () =>
2541  {
2542  GameMain.Client?.RequestRoundEnd(save: false);
2543  });
2544  }
2545  }
2546 
2547  if (GameMain.GameSession != null || Screen.Selected is CharacterEditorScreen || Screen.Selected is SubEditorScreen)
2548  {
2549  CreateButton("PauseMenuQuit", buttonContainer,
2550  verificationTextTag: GameMain.GameSession == null ? "PauseMenuQuitVerificationEditor" : "PauseMenuQuitVerification",
2551  action: () =>
2552  {
2553  GameMain.QuitToMainMenu(save: false);
2554  });
2555  }
2556  else
2557  {
2558  CreateButton("PauseMenuQuit", buttonContainer, action: () => { GameMain.QuitToMainMenu(save: false); });
2559  }
2560 
2561  GUITextBlock.AutoScaleAndNormalize(buttonContainer.Children.Where(c => c is GUIButton).Select(c => ((GUIButton)c).TextBlock));
2562  //scale to ensure there's enough room for all the buttons
2563  pauseMenuInner.RectTransform.MinSize = new Point(
2564  pauseMenuInner.RectTransform.MinSize.X,
2565  Math.Max(
2566  (int)(buttonContainer.Children.Sum(c => c.Rect.Height + buttonContainer.AbsoluteSpacing) / buttonContainer.RectTransform.RelativeSize.Y),
2567  pauseMenuInner.RectTransform.MinSize.X));
2568 
2569  }
2570 
2571  void CreateButton(string textTag, GUIComponent parent, Action action, string verificationTextTag = null)
2572  {
2573  new GUIButton(new RectTransform(new Vector2(1.0f, 0.1f), parent.RectTransform), TextManager.Get(textTag))
2574  {
2575  OnClicked = (btn, userData) =>
2576  {
2577  if (string.IsNullOrEmpty(verificationTextTag))
2578  {
2579  PauseMenuOpen = false;
2580  action?.Invoke();
2581  }
2582  else
2583  {
2584  CreateVerificationPrompt(verificationTextTag, action);
2585  }
2586  return true;
2587  }
2588  };
2589  }
2590 
2591  void CreateVerificationPrompt(string textTag, Action confirmAction)
2592  {
2593  var msgBox = new GUIMessageBox("", TextManager.Get(textTag),
2594  new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") })
2595  {
2596  UserData = "verificationprompt",
2597  DrawOnTop = true
2598  };
2599  msgBox.Buttons[0].OnClicked = (_, __) =>
2600  {
2601  PauseMenuOpen = false;
2602  confirmAction?.Invoke();
2603  return true;
2604  };
2605  msgBox.Buttons[0].OnClicked += msgBox.Close;
2606  msgBox.Buttons[1].OnClicked += msgBox.Close;
2607  }
2608  }
2609 
2610  private static bool TogglePauseMenu(GUIButton button, object obj)
2611  {
2612  PauseMenuOpen = !PauseMenuOpen;
2613  if (!PauseMenuOpen && PauseMenu != null)
2614  {
2615  PauseMenu.RectTransform.Parent = null;
2616  PauseMenu = null;
2617  }
2618  return true;
2619  }
2620 
2625  public static void AddMessage(LocalizedString message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null)
2626  {
2627  AddMessage(message.Value, color, lifeTime, playSound, font);
2628  }
2629 
2630  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)
2631  {
2632  AddMessage(message.Value, color, pos, velocity, lifeTime, playSound, soundType, subId);
2633  }
2634 
2635  public static void AddMessage(string message, Color color, float? lifeTime = null, bool playSound = true, GUIFont font = null)
2636  {
2637  var guiMessage = new GUIMessage(message, color, lifeTime ?? MathHelper.Clamp(message.Length / 5.0f, 3.0f, 10.0f), font ?? GUIStyle.LargeFont);
2638  lock (mutex)
2639  {
2640  if (messages.Any(msg => msg.Text == message)) { return; }
2641  messages.Add(guiMessage);
2642  }
2643  if (playSound) { SoundPlayer.PlayUISound(GUISoundType.UIMessage); }
2644  }
2645 
2646  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)
2647  {
2648  Submarine sub = Submarine.Loaded.FirstOrDefault(s => s.ID == subId);
2649 
2650  var newMessage = new GUIMessage(message, color, pos, velocity, lifeTime, Alignment.Center, GUIStyle.Font, sub: sub);
2651  if (playSound) { SoundPlayer.PlayUISound(soundType); }
2652 
2653  lock (mutex)
2654  {
2655  bool overlapFound = true;
2656  int tries = 0;
2657  while (overlapFound)
2658  {
2659  overlapFound = false;
2660  foreach (var otherMessage in messages)
2661  {
2662  float xDiff = otherMessage.Pos.X - newMessage.Pos.X;
2663  if (Math.Abs(xDiff) > (newMessage.Size.X + otherMessage.Size.X) / 2) { continue; }
2664  float yDiff = otherMessage.Pos.Y - newMessage.Pos.Y;
2665  if (Math.Abs(yDiff) > (newMessage.Size.Y + otherMessage.Size.Y) / 2) { continue; }
2666  Vector2 moveDir = -(new Vector2(xDiff, yDiff) + Rand.Vector(1.0f));
2667  if (moveDir.LengthSquared() > 0.0001f)
2668  {
2669  moveDir = Vector2.Normalize(moveDir);
2670  }
2671  else
2672  {
2673  moveDir = Rand.Vector(1.0f);
2674  }
2675  moveDir.Y = -Math.Abs(moveDir.Y);
2676  newMessage.Pos -= Vector2.UnitY * 10;
2677  }
2678  tries++;
2679  if (tries > 20) { break; }
2680  }
2681  messages.Add(newMessage);
2682  }
2683  }
2684 
2685  public static void ClearMessages()
2686  {
2687  lock (mutex)
2688  {
2689  messages.Clear();
2690  }
2691  }
2692 
2693  public static bool IsFourByThree()
2694  {
2695  float aspectRatio = HorizontalAspectRatio;
2696  return aspectRatio > 1.3f && aspectRatio < 1.4f;
2697  }
2698 
2699  public static void SetSavingIndicatorState(bool enabled)
2700  {
2701  if (enabled)
2702  {
2703  timeUntilSavingIndicatorDisabled = null;
2704  }
2705  isSavingIndicatorEnabled = enabled;
2706  }
2707 
2708  public static void DisableSavingIndicatorDelayed(float delay = 3.0f)
2709  {
2710  if (!isSavingIndicatorEnabled) { return; }
2711  timeUntilSavingIndicatorDisabled = delay;
2712  }
2713 #endregion
2714  }
2715 }
static GameMain Instance
Definition: GameMain.cs:144
readonly string Filename
Definition: Sound.cs:19
static ? SocialOverlay Instance
void AddToGuiUpdateList()
NumberType
Definition: Enums.cs:715
GUISoundType
Definition: GUI.cs:21
CursorState
Definition: GUI.cs:40