Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/DebugConsole.cs
3 using Barotrauma.IO;
7 using Barotrauma.Steam;
8 using Microsoft.Xna.Framework;
9 using Microsoft.Xna.Framework.Input;
10 using System;
11 using System.Collections.Generic;
12 using System.Collections.Immutable;
13 using System.Diagnostics;
14 using System.Globalization;
15 using System.Linq;
16 using System.Text;
17 using System.Threading.Tasks;
18 using System.Xml.Linq;
19 using static Barotrauma.FabricationRecipe;
20 
21 namespace Barotrauma
22 {
23  static partial class DebugConsole
24  {
25  public partial class Command
26  {
30  public Action<string[]> OnClientExecute;
31 
32  public bool RelayToServer = true;
33 
34  public void ClientExecute(string[] args)
35  {
36  bool allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true });
37  if (!allowCheats && !CheatsEnabled && IsCheat)
38  {
39  NewMessage("You need to enable cheats using the command \"enablecheats\" before you can use the command \"" + Names[0] + "\".", Color.Red);
40  NewMessage("Enabling cheats will disable achievements during this play session.", Color.Red);
41  return;
42  }
43 
44  if (OnClientExecute != null)
45  {
46  OnClientExecute(args);
47  }
48  else
49  {
50  OnExecute(args);
51  }
52  }
53  }
54 
55  private static bool isOpen;
56  public static bool IsOpen
57  {
58  get => isOpen;
59  set => isOpen = value;
60  }
61 
62  public static bool Paused = false;
63 
64  private static GUITextBlock activeQuestionText;
65 
66  private static GUIFrame frame;
67  private static GUIListBox listBox;
68  private static GUITextBox textBox;
69 #if DEBUG
70  private const int maxLength = 100000;
71 #else
72  private const int maxLength = 1000;
73 #endif
74 
75  public static GUITextBox TextBox => textBox;
76 
77  private static readonly ChatManager chatManager = new ChatManager(true, 64);
78 
79  public static void Init()
80  {
81  OpenAL.Alc.SetErrorReasonCallback((string msg) => NewMessage(msg, Color.Orange));
82 
83  frame = new GUIFrame(new RectTransform(new Vector2(0.5f, 0.45f), GUI.Canvas) { MinSize = new Point(400, 300), AbsoluteOffset = new Point(10, 10) },
84  color: new Color(0.4f, 0.4f, 0.4f, 0.8f));
85 
86  var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), frame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.01f };
87 
88  var toggleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paddedFrame.RectTransform, Anchor.TopLeft), TextManager.Get("DebugConsoleHelpText"), Color.GreenYellow, GUIStyle.SmallFont, Alignment.CenterLeft, style: null);
89 
90  var closeButton = new GUIButton(new RectTransform(new Vector2(0.025f, 1.0f), toggleText.RectTransform, Anchor.TopRight), "X", style: null)
91  {
92  Color = Color.DarkRed,
93  HoverColor = Color.Red,
94  TextColor = Color.White,
95  OutlineColor = Color.Red
96  };
97  closeButton.OnClicked += (btn, userdata) =>
98  {
99  isOpen = false;
100  GUI.ForceMouseOn(null);
101  textBox.Deselect();
102  return true;
103  };
104 
105  listBox = new GUIListBox(new RectTransform(new Point(paddedFrame.Rect.Width, paddedFrame.Rect.Height - 60), paddedFrame.RectTransform, Anchor.Center)
106  {
107  IsFixedSize = false
108  }, color: Color.Black * 0.9f) { ScrollBarVisible = true };
109 
110  textBox = new GUITextBox(new RectTransform(new Point(paddedFrame.Rect.Width, 30), paddedFrame.RectTransform, Anchor.BottomLeft)
111  {
112  IsFixedSize = false
113  });
114  textBox.MaxTextLength = maxLength;
115  textBox.OnKeyHit += (sender, key) =>
116  {
117  if (key != Keys.Tab && key != Keys.LeftShift)
118  {
119  ResetAutoComplete();
120  }
121  };
122 
123  ChatManager.RegisterKeys(textBox, chatManager);
124  }
125 
126  public static void AddToGUIUpdateList()
127  {
128  if (isOpen)
129  {
130  frame.AddToGUIUpdateList(order: 1);
131  }
132  }
133 
134  public static void Update(float deltaTime)
135  {
136  while (queuedMessages.TryDequeue(out var newMsg))
137  {
138  AddMessage(newMsg);
139 
140  if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
141  {
142  unsavedMessages.Add(newMsg);
143  if (unsavedMessages.Count >= messagesPerFile)
144  {
145  SaveLogs();
146  unsavedMessages.Clear();
147  }
148  }
149  }
150 
151  if (!IsOpen && GUI.KeyboardDispatcher.Subscriber == null)
152  {
153  foreach (var (key, command) in DebugConsoleMapping.Instance.Bindings)
154  {
155  if (key.IsHit())
156  {
157  ExecuteCommand(command);
158  }
159  }
160  }
161 
162  activeQuestionText?.SetAsLastChild();
163 
164  if (PlayerInput.KeyHit(Keys.F3) && !PlayerInput.KeyDown(Keys.LeftControl) && !PlayerInput.KeyDown(Keys.RightControl))
165  {
166  Toggle();
167  }
168  else if (isOpen && PlayerInput.KeyHit(Keys.Escape))
169  {
170  isOpen = false;
171  GUI.ForceMouseOn(null);
172  textBox.Deselect();
173  SoundPlayer.PlayUISound(GUISoundType.Select);
174  }
175 
176  if (isOpen)
177  {
178  frame.UpdateManually(deltaTime);
179 
180  Character.DisableControls = true;
181 
182  if (PlayerInput.KeyHit(Keys.Tab) && !textBox.IsIMEActive)
183  {
184  int increment = PlayerInput.KeyDown(Keys.LeftShift) ? -1 : 1;
185  textBox.Text = AutoComplete(textBox.Text, increment: string.IsNullOrEmpty(currentAutoCompletedCommand) ? 0 : increment );
186  }
187 
188  if (PlayerInput.KeyDown(Keys.LeftControl) || PlayerInput.KeyDown(Keys.RightControl))
189  {
190  if ((PlayerInput.KeyDown(Keys.C) || PlayerInput.KeyDown(Keys.D) || PlayerInput.KeyDown(Keys.Z)) && activeQuestionCallback != null)
191  {
192  activeQuestionCallback = null;
193  activeQuestionText = null;
194  NewMessage(PlayerInput.KeyDown(Keys.C) ? "^C" : PlayerInput.KeyDown(Keys.D) ? "^D" : "^Z", Color.White, true);
195  }
196  }
197 
198  if (PlayerInput.KeyHit(Keys.Enter))
199  {
200  chatManager.Store(textBox.Text);
201  ExecuteCommand(textBox.Text);
202  textBox.Text = "";
203  }
204  }
205  }
206 
207  public static void Toggle()
208  {
209  isOpen = !isOpen;
210  if (isOpen)
211  {
212  textBox.Select(ignoreSelectSound: true);
213  AddToGUIUpdateList();
214  }
215  else
216  {
217  GUI.ForceMouseOn(null);
218  textBox.Deselect();
219  }
220  SoundPlayer.PlayUISound(GUISoundType.Select);
221  }
222 
223  private static bool IsCommandPermitted(Identifier command, GameClient client)
224  {
225  if (GameMain.LuaCs.Game.IsCustomCommandPermitted(command)) { return true; }
226 
227  switch (command.Value.ToLowerInvariant())
228  {
229  case "kick":
230  return client.HasPermission(ClientPermissions.Kick);
231  case "ban":
232  case "banip":
233  case "banaddress":
234  return client.HasPermission(ClientPermissions.Ban);
235  case "unban":
236  case "unbanip":
237  return client.HasPermission(ClientPermissions.Unban);
238  case "netstats":
239  case "help":
240  case "dumpids":
241  case "admin":
242  case "entitylist":
243  case "togglehud":
244  case "toggleupperhud":
245  case "togglecharacternames":
246  case "fpscounter":
247  case "showperf":
248  case "dumptofile":
249  case "findentityids":
250  case "setfreecamspeed":
251  case "togglevoicechatfilters":
252  case "bindkey":
253  case "savebinds":
254  case "unbindkey":
255  case "wikiimage_character":
256  case "wikiimage_sub":
257  case "eosStat":
258  case "eosUnlink":
259  case "eosLoginEpicViaSteam":
260  return true;
261  default:
262  return client.HasConsoleCommandPermission(command);
263  }
264  }
265 
266  public static void DequeueMessages()
267  {
268  while (queuedMessages.TryDequeue(out var newMsg))
269  {
270  if (listBox == null)
271  {
272  //don't attempt to add to the listbox if it hasn't been created yet
273  Messages.Add(newMsg);
274  }
275  else
276  {
277  AddMessage(newMsg);
278  }
279 
280  if (GameSettings.CurrentConfig.SaveDebugConsoleLogs || GameSettings.CurrentConfig.VerboseLogging)
281  {
282  unsavedMessages.Add(newMsg);
283  }
284  }
285  }
286 
287  private static void AddMessage(ColoredText msg)
288  {
289  //listbox not created yet, don't attempt to add
290  if (listBox == null) return;
291 
292  if (listBox.Content.CountChildren > MaxMessages)
293  {
294  listBox.RemoveChild(listBox.Content.Children.First());
295  }
296 
297  Messages.Add(msg);
298  if (Messages.Count > MaxMessages)
299  {
300  Messages.RemoveRange(0, Messages.Count - MaxMessages);
301  }
302 
303  try
304  {
305  if (msg.IsError)
306  {
307  var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform), style: "InnerFrame", color: Color.White)
308  {
309  CanBeFocused = true,
310  OnSecondaryClicked = (component, data) =>
311  {
312  GUIContextMenu.CreateContextMenu(new ContextMenuOption("editor.copytoclipboard", true, () => { Clipboard.SetText(msg.Text); }));
313  return true;
314  }
315  };
316  var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 5, 0), textContainer.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(2, 2) },
317  RichString.Rich(msg.Text), textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true)
318  {
319  CanBeFocused = false,
320  TextColor = msg.Color
321  };
322  textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5);
323  textBlock.SetTextPos();
324  }
325  else
326  {
327  var textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform),
328  RichString.Rich(msg.Text), font: GUIStyle.SmallFont, wrap: true)
329  {
330  CanBeFocused = false,
331  TextColor = msg.Color
332  };
333  }
334 
335  listBox.UpdateScrollBarSize();
336  listBox.BarScroll = 1.0f;
337  }
338  catch (Exception e)
339  {
340  ThrowError("Failed to add a message to the debug console.", e);
341  }
342 
343  chatManager.Clear();
344  }
345 
346  static partial void ShowHelpMessage(Command command)
347  {
348  if (listBox.Content.CountChildren > MaxMessages)
349  {
350  listBox.RemoveChild(listBox.Content.Children.First());
351  }
352 
353  var textContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.0f), listBox.Content.RectTransform),
354  style: "InnerFrame", color: Color.White * 0.6f)
355  {
356  CanBeFocused = false
357  };
358  var textBlock = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width - 170, 0), textContainer.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(20, 0) },
359  command.Help, textAlignment: Alignment.TopLeft, font: GUIStyle.SmallFont, wrap: true)
360  {
361  CanBeFocused = false,
362  TextColor = Color.White
363  };
364  textContainer.RectTransform.NonScaledSize = new Point(textContainer.RectTransform.NonScaledSize.X, textBlock.RectTransform.NonScaledSize.Y + 5);
365  textBlock.SetTextPos();
366  new GUITextBlock(new RectTransform(new Point(150, textContainer.Rect.Height), textContainer.RectTransform),
367  command.Names[0].Value, textAlignment: Alignment.TopLeft);
368 
369  listBox.UpdateScrollBarSize();
370  listBox.BarScroll = 1.0f;
371 
372  chatManager.Clear();
373  }
374 
375  private static void AssignOnClientExecute(string names, Action<string[]> onClientExecute)
376  {
377  Command command = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
378  if (command == null)
379  {
380  throw new Exception("AssignOnClientExecute failed. Command matching the name(s) \"" + names + "\" not found.");
381  }
382  else
383  {
384  command.OnClientExecute = onClientExecute;
385  command.RelayToServer = false;
386  }
387  }
388 
389  private static void AssignRelayToServer(string names, bool relay)
390  {
391  Command command = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
392  if (command == null)
393  {
394  DebugConsole.Log("Could not assign to relay to server: " + names);
395  return;
396  }
397  command.RelayToServer = relay;
398  }
399 
400  private static void InitProjectSpecific()
401  {
402  commands.Add(new Command("eosStat", "Query and display all logged in EOS users. Normally this is at most two users, but in a developer environment it could be more.", args =>
403  {
404  if (!EosInterface.Core.IsInitialized)
405  {
406  NewMessage("EOS not initialized");
407  return;
408  }
409 
410  var loggedInUsers = EosInterface.IdQueries.GetLoggedInPuids();
411  if (!loggedInUsers.Any())
412  {
413  NewMessage("EOS user not logged in");
414  return;
415  }
416 
417  NewMessage("Logged in EOS users:");
418  foreach (var puid in loggedInUsers)
419  {
420  TaskPool.Add(
421  $"eosStat -> {puid}",
422  EosInterface.IdQueries.GetSelfExternalAccountIds(puid),
423  t =>
424  {
425  if (!t.TryGetResult(out Result<ImmutableArray<AccountId>, EosInterface.IdQueries.GetSelfExternalIdError> result)) { return; }
426  NewMessage($" - {puid}");
427 
428  if (result.TryUnwrapSuccess(out var ids))
429  {
430  foreach (var id in ids)
431  {
432  NewMessage($" - {id}");
433  if (id is EpicAccountId eaid)
434  {
435  async Task gameOwnershipTokenTest()
436  {
437  var tokenOption = await EosInterface.Ownership.GetGameOwnershipToken(eaid);
438  var verified = await tokenOption.Bind(t => t.Verify());
439  NewMessage($"Ownership token verify result: {verified}");
440  }
441  _ = gameOwnershipTokenTest(); // Fire and forget!
442  EosInterface.Login.TestEosSessionTimeoutRecovery(puid);
443  }
444  }
445  }
446  else
447  {
448  NewMessage($" - Failed to get external IDs linked to {puid}: {result}");
449  }
450  });
451  }
452  }));
453  AssignRelayToServer("eosStat", false);
454 
455  commands.Add(new Command("eosUnlink", "Unlink the primary logged in external account ID from its corresponding EOS Product User ID and close the game. This is meant to be used to test the EOS consent flow.", args =>
456  {
457  var userId = EosInterface.IdQueries.GetLoggedInPuids().FirstOrDefault();
458  NewMessage($"Unlinking external account from PUID {userId}");
459  GameSettings.SetCurrentConfig(GameSettings.CurrentConfig with { CrossplayChoice = Eos.EosSteamPrimaryLogin.CrossplayChoice.Unknown });
460  GameSettings.SaveCurrentConfig();
461  TaskPool.Add("unlinkTask", EosInterface.Login.UnlinkExternalAccount(userId), _ =>
462  {
463  GameMain.Instance.Exit();
464  });
465  }));
466  AssignRelayToServer("eosUnlink", false);
467 
468  commands.Add(new Command("eosLoginEpicViaSteam", "Log into an Epic account via a link to the currently logged in Steam account",
469  args =>
470  {
471  TaskPool.Add("eosLoginEpicViaSteam",
472  Eos.EosEpicSecondaryLogin.LoginToLinkedEpicAccount(),
473  TaskPool.IgnoredCallback);
474  }));
475  AssignRelayToServer("eosLoginEpicViaSteam", false);
476 
477  commands.Add(new Command("resetgameanalyticsconsent", "Reset whether you've given your consent for the game to send statistics to GameAnalytics. After executing the command, the game should ask for your consent again on relaunch.", args =>
478  {
479  GameAnalyticsManager.ResetConsent();
480  }));
481  AssignRelayToServer("resetgameanalyticsconsent", false);
482 
483  commands.Add(new Command("copyitemnames", "", (string[] args) =>
484  {
485  StringBuilder sb = new StringBuilder();
486  foreach (ItemPrefab mp in ItemPrefab.Prefabs)
487  {
488  sb.AppendLine(mp.Name.Value);
489  }
490  Clipboard.SetText(sb.ToString());
491  }));
492 
493  commands.Add(new Command("autohull", "", (string[] args) =>
494  {
495  if (Screen.Selected != GameMain.SubEditorScreen) return;
496 
497  if (MapEntity.MapEntityList.Any(e => e is Hull || e is Gap))
498  {
499  ShowQuestionPrompt("This submarine already has hulls and/or gaps. This command will delete them. Do you want to continue? Y/N",
500  (option) =>
501  {
502  ShowQuestionPrompt("The automatic hull generation may not work correctly if your submarine uses curved walls. Do you want to continue? Y/N",
503  (option2) =>
504  {
505  if (option2.ToLowerInvariant() == "y") { GameMain.SubEditorScreen.AutoHull(); }
506  });
507  });
508  }
509  else
510  {
511  ShowQuestionPrompt("The automatic hull generation may not work correctly if your submarine uses curved walls. Do you want to continue? Y/N",
512  (option) => { if (option.ToLowerInvariant() == "y") GameMain.SubEditorScreen.AutoHull(); });
513  }
514  }));
515 
516  commands.Add(new Command("enablecheats", "enablecheats: Enables cheat commands and disables achievements during this play session.", (string[] args) =>
517  {
518  CheatsEnabled = true;
519  AchievementManager.CheatsEnabled = true;
520  if (GameMain.GameSession?.Campaign is CampaignMode campaign)
521  {
522  campaign.CheatsEnabled = true;
523  }
524  NewMessage("Enabled cheat commands.", Color.Red);
525  NewMessage("Achievements have been disabled during this play session.", Color.Red);
526  }));
527  AssignRelayToServer("enablecheats", true);
528 
529  commands.Add(new Command("mainmenu|menu", "mainmenu/menu: Go to the main menu.", (string[] args) =>
530  {
531  GameMain.GameSession = null;
532 
533  List<Character> characters = new List<Character>(Character.CharacterList);
534  foreach (Character c in characters)
535  {
536  c.Remove();
537  }
538 
539  GameMain.MainMenuScreen.Select();
540  }));
541 
542  commands.Add(new Command("game", "gamescreen/game: Go to the \"in-game\" view.", (string[] args) =>
543  {
544  if (Screen.Selected == GameMain.SubEditorScreen)
545  {
546  NewMessage("WARNING: Switching directly from the submarine editor to the game view may cause bugs and crashes. Use with caution.", Color.Orange);
547  Entity.Spawner ??= new EntitySpawner();
548  }
549  GameMain.GameScreen.Select();
550  }));
551 
552  commands.Add(new Command("editsubs|subeditor", "editsubs/subeditor: Switch to the Submarine Editor to create or edit submarines.", (string[] args) =>
553  {
554  if (args.Length > 0)
555  {
556  var subInfo = new SubmarineInfo(string.Join(" ", args));
557  Submarine.MainSub = Submarine.Load(subInfo, true);
558  }
559  GameMain.SubEditorScreen.Select(enableAutoSave: Screen.Selected != GameMain.GameScreen);
560  Entity.Spawner?.Remove();
561  Entity.Spawner = null;
562  }, isCheat: true));
563 
564  commands.Add(new Command("editparticles|particleeditor", "editparticles/particleeditor: Switch to the Particle Editor to edit particle effects.", (string[] args) =>
565  {
566  GameMain.ParticleEditorScreen.Select();
567  }));
568 
569  commands.Add(new Command("editlevels|leveleditor", "editlevels/leveleditor: Switch to the Level Editor to edit levels.", (string[] args) =>
570  {
571  GameMain.LevelEditorScreen.Select();
572  }));
573 
574  commands.Add(new Command("editsprites|spriteeditor", "editsprites/spriteeditor: Switch to the Sprite Editor to edit the source rects and origins of sprites.", (string[] args) =>
575  {
576  GameMain.SpriteEditorScreen.Select();
577  }));
578 
579  commands.Add(new Command("editevents|eventeditor", "editevents/eventeditor: Switch to the Event Editor to edit scripted events.", (string[] args) =>
580  {
581  GameMain.EventEditorScreen.Select();
582  }));
583 
584  commands.Add(new Command("editcharacters|charactereditor", "editcharacters/charactereditor: Switch to the Character Editor to edit/create the ragdolls and animations of characters.", (string[] args) =>
585  {
586  if (Screen.Selected == GameMain.GameScreen)
587  {
588  NewMessage("WARNING: Switching between the character editor and the game view may cause odd behaviour or bugs. Use with caution.", Color.Orange);
589  }
590  GameMain.CharacterEditorScreen.Select();
591  }));
592 
593  commands.Add(new Command("quickstart", "Starts a singleplayer sandbox", (string[] args) =>
594  {
595  if (Screen.Selected != GameMain.MainMenuScreen)
596  {
597  ThrowError("This command can only be executed from the main menu.");
598  return;
599  }
600 
601  Identifier subName = (args.Length > 0 ? args[0] : "").ToIdentifier();
602  if (subName.IsEmpty)
603  {
604  ThrowError("No submarine specified.");
605  return;
606  }
607 
608  float difficulty = 40;
609  if (args.Length > 1)
610  {
611  float.TryParse(args[1], out difficulty);
612  }
613 
614  LevelGenerationParams levelGenerationParams = null;
615  if (args.Length > 2)
616  {
617  string levelGenerationIdentifier = args[2];
618  levelGenerationParams = LevelGenerationParams.LevelParams.FirstOrDefault(p => p.Identifier == levelGenerationIdentifier);
619  }
620 
621  if (SubmarineInfo.SavedSubmarines.None(s => s.Name == subName))
622  {
623  ThrowError($"Cannot find a sub that matches the name \"{subName}\".");
624  return;
625  }
626 
627  bool luaCsEnabled = true;
628  if (args.Length > 3)
629  {
630  bool.TryParse(args[3], out luaCsEnabled);
631  }
632 
633  if (luaCsEnabled) { GameMain.LuaCs.Initialize(); }
634 
635  GameMain.MainMenuScreen.QuickStart(fixedSeed: false, subName, difficulty, levelGenerationParams);
636 
637  }, getValidArgs: () => new[] { SubmarineInfo.SavedSubmarines.Select(s => s.Name).Distinct().OrderBy(s => s).ToArray() }));
638 
639  commands.Add(new Command("steamnetdebug", "steamnetdebug: Toggles Steamworks networking debug logging.", (string[] args) =>
640  {
641  SteamManager.SetSteamworksNetworkingDebugLog(!SteamManager.NetworkingDebugLog);
642  }));
643 
644  commands.Add(new Command("readycheck", "Commence a ready check in multiplayer.", (string[] args) =>
645  {
646  NewMessage("Ready checks can only be commenced in multiplayer.", Color.Red);
647  }));
648 
649  commands.Add(new Command("setsalary", "setsalary [0-100] [character/default]: Sets the salary of a certain character or the default salary to a percentage.", (string[] args) =>
650  {
651  ThrowError("This command can only be used in multiplayer campaign.");
652  }, isCheat: true, getValidArgs: () =>
653  {
654  return new[]
655  {
656  new[]{ "0", "100" },
657  Enumerable.Union(
658  new string[] { "default" },
659  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n)).ToArray(),
660  };
661  }));
662 
663  commands.Add(new Command("bindkey", "bindkey [key] [command]: Binds a key to a command.", (string[] args) =>
664  {
665  if (args.Length < 2)
666  {
667  ThrowError("No key or command specified.");
668  return;
669  }
670 
671  string keyString = args[0];
672  string command = args[1];
673 
674  KeyOrMouse key = Enum.TryParse<Keys>(keyString, ignoreCase: true, out var outKey)
675  ? outKey
676  : Enum.TryParse<MouseButton>(keyString, ignoreCase: true, out var outMouseButton)
677  ? outMouseButton
678  : (KeyOrMouse)MouseButton.None;
679 
680  if (key.Key == Keys.None && key.MouseButton == MouseButton.None)
681  {
682  ThrowError($"Invalid key {keyString}.");
683  return;
684  }
685 
686  DebugConsoleMapping.Instance.Set(key, command);
687  NewMessage($"\"{command}\" bound to {key}.", GUIStyle.Green);
688 
689  if (GameSettings.CurrentConfig.KeyMap.Bindings.FirstOrDefault(bind => bind.Value.Key != Keys.None && bind.Value.Key == key) is { } existingBind && existingBind.Value != null)
690  {
691  AddWarning($"\"{key}\" has already been bound to {existingBind.Key}. The keybind will perform both actions when pressed.");
692  }
693 
694  }, isCheat: false, getValidArgs: () => new[] { Enum.GetNames(typeof(Keys)), new[] { "\"\"" } }));
695 
696  commands.Add(new Command("unbindkey", "unbindkey [key]: Unbinds a command.", (string[] args) =>
697  {
698  if (args.Length < 1)
699  {
700  ThrowError("No key specified.");
701  return;
702  }
703 
704  string keyString = args[0];
705 
706  KeyOrMouse key = Enum.TryParse<Keys>(keyString, ignoreCase: true, out var outKey)
707  ? outKey
708  : Enum.TryParse<MouseButton>(keyString, ignoreCase: true, out var outMouseButton)
709  ? outMouseButton
710  : (KeyOrMouse)MouseButton.None;
711 
712  if (key.Key == Keys.None && key.MouseButton == MouseButton.None)
713  {
714  ThrowError($"Invalid key {keyString}.");
715  return;
716  }
718  NewMessage("Keybind unbound.", GUIStyle.Green);
719  return;
720  }, isCheat: false, getValidArgs: () => new[] { DebugConsoleMapping.Instance.Bindings.Keys.Select(keys => keys.ToString()).Distinct().OrderBy(k => k).ToArray() }));
721 
722  commands.Add(new Command("savebinds", "savebinds: Writes current keybinds into the config file.", (string[] args) =>
723  {
724  ShowQuestionPrompt($"Some keybinds may render the game unusable, are you sure you want to make these keybinds persistent? ({DebugConsoleMapping.Instance.Bindings.Count} keybind(s) assigned) Y/N",
725  (option2) =>
726  {
727  if (option2.ToIdentifier() != "y")
728  {
729  NewMessage("Aborted.", GUIStyle.Red);
730  return;
731  }
732 
733  GameSettings.SaveCurrentConfig();
734  });
735  }, isCheat: false));
736 
737  commands.Add(new Command("togglegrid", "Toggle visual snap grid in sub editor.", (string[] args) =>
738  {
739  SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid;
740  NewMessage(SubEditorScreen.ShouldDrawGrid ? "Enabled submarine grid." : "Disabled submarine grid.", GUIStyle.Green);
741  }));
742 
743  commands.Add(new Command("spreadsheetexport", "Export items in format recognized by the spreadsheet importer.", (string[] args) =>
744  {
745  SpreadsheetExport.Export();
746  }));
747 
748  commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) =>
749  {
750  if (Character.Controlled == null) { return; }
751  try
752  {
753  WikiImage.Create(Character.Controlled);
754  }
755  catch (Exception e)
756  {
757  DebugConsole.ThrowError("The command 'wikiimage_character' failed.", e);
758  }
759  }));
760 
761  commands.Add(new Command("wikiimage_sub", "Save an image of the main submarine with a transparent background.", (string[] args) =>
762  {
763  if (Submarine.MainSub == null) { return; }
764  try
765  {
766  MapEntity.SelectedList.Clear();
767  MapEntity.ClearHighlightedEntities();
768  WikiImage.Create(Submarine.MainSub);
769  }
770  catch (Exception e)
771  {
772  DebugConsole.ThrowError("The command 'wikiimage_sub' failed.", e);
773  }
774  }));
775 
776  AssignRelayToServer("kick", false);
777  AssignRelayToServer("kickid", false);
778  AssignRelayToServer("ban", false);
779  AssignRelayToServer("banid", false);
780  AssignRelayToServer("dumpids", false);
781  AssignRelayToServer("dumptofile", false);
782  AssignRelayToServer("findentityids", false);
783  AssignRelayToServer("campaigninfo", false);
784  AssignRelayToServer("help", false);
785  AssignRelayToServer("verboselogging", false);
786  AssignRelayToServer("freecam", false);
787  AssignRelayToServer("steamnetdebug", false);
788  AssignRelayToServer("quickstart", false);
789  AssignRelayToServer("togglegrid", false);
790  AssignRelayToServer("bindkey", false);
791  AssignRelayToServer("unbindkey", false);
792  AssignRelayToServer("savebinds", false);
793  AssignRelayToServer("spreadsheetexport", false);
794 #if DEBUG
795  AssignRelayToServer("listspamfilters", false);
796  AssignRelayToServer("crash", false);
797  AssignRelayToServer("showballastflorasprite", false);
798  AssignRelayToServer("simulatedlatency", false);
799  AssignRelayToServer("simulatedloss", false);
800  AssignRelayToServer("simulatedduplicateschance", false);
801  AssignRelayToServer("simulatedlongloadingtime", false);
802  AssignRelayToServer("storeinfo", false);
803  AssignRelayToServer("sendrawpacket", false);
804 #endif
805 
806  commands.Add(new Command("clientlist", "", (string[] args) => { }));
807  AssignRelayToServer("clientlist", true);
808  commands.Add(new Command("say", "", (string[] args) => { }));
809  AssignRelayToServer("say", true);
810  commands.Add(new Command("msg", "", (string[] args) => { }));
811  AssignRelayToServer("msg", true);
812  commands.Add(new Command("setmaxplayers|maxplayers", "", (string[] args) => { }));
813  AssignRelayToServer("setmaxplayers", true);
814  commands.Add(new Command("setpassword|password", "", (string[] args) => { }));
815  AssignRelayToServer("setpassword", true);
816  commands.Add(new Command("traitorlist", "", (string[] args) => { }));
817  AssignRelayToServer("traitorlist", true);
818  AssignRelayToServer("money", true);
819  AssignRelayToServer("showmoney", true);
820  AssignRelayToServer("setskill", true);
821  AssignRelayToServer("setsalary", true);
822  AssignRelayToServer("readycheck", true);
823  commands.Add(new Command("debugjobassignment", "", (string[] args) => { }));
824  AssignRelayToServer("debugjobassignment", true);
825 
826  AssignRelayToServer("givetalent", true);
827  AssignRelayToServer("unlocktalents", true);
828  AssignRelayToServer("giveexperience", true);
829 
830  AssignOnExecute("control", (string[] args) =>
831  {
832  if (args.Length < 1) { return; }
833  if (GameMain.NetworkMember != null)
834  {
835  GameMain.Client?.SendConsoleCommand("control " + string.Join(' ', args[0]));
836  return;
837  }
838  var character = FindMatchingCharacter(args, true);
839  if (character != null)
840  {
841  Character.Controlled = character;
842  }
843  });
844  AssignRelayToServer("control", true);
845 
846  commands.Add(new Command("shake", "", (string[] args) =>
847  {
848  GameMain.GameScreen.Cam.Shake = 10.0f;
849  }));
850 
851  AssignOnExecute("explosion", (string[] args) =>
852  {
853  Vector2 explosionPos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition);
854  float range = 500, force = 10, damage = 50, structureDamage = 20, itemDamage = 100, empStrength = 0.0f, ballastFloraStrength = 50f;
855  if (args.Length > 0) float.TryParse(args[0], out range);
856  if (args.Length > 1) float.TryParse(args[1], out force);
857  if (args.Length > 2) float.TryParse(args[2], out damage);
858  if (args.Length > 3) float.TryParse(args[3], out structureDamage);
859  if (args.Length > 4) float.TryParse(args[4], out itemDamage);
860  if (args.Length > 5) float.TryParse(args[5], out empStrength);
861  if (args.Length > 6) float.TryParse(args[6], out ballastFloraStrength);
862  new Explosion(range, force, damage, structureDamage, itemDamage, empStrength, ballastFloraStrength).Explode(explosionPos, null);
863  });
864 
865  AssignOnExecute("teleportcharacter|teleport", (string[] args) =>
866  {
867  Vector2 cursorWorldPos = GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition);
868  TeleportCharacter(cursorWorldPos, Character.Controlled, args);
869  });
870 
871  AssignOnExecute("spawn|spawncharacter", (string[] args) =>
872  {
873  SpawnCharacter(args, GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition), out string errorMsg);
874  if (!string.IsNullOrWhiteSpace(errorMsg))
875  {
876  ThrowError(errorMsg);
877  }
878  });
879 
880  AssignOnExecute("los", (string[] args) =>
881  {
882  if (args.None() || !bool.TryParse(args[0], out bool state))
883  {
884  state = !GameMain.LightManager.LosEnabled;
885  }
886  GameMain.LightManager.LosEnabled = state;
887  NewMessage("Line of sight effect " + (GameMain.LightManager.LosEnabled ? "enabled" : "disabled"), Color.Yellow);
888  });
889  AssignRelayToServer("los", false);
890 
891  AssignOnExecute("lighting|lights", (string[] args) =>
892  {
893  if (args.None() || !bool.TryParse(args[0], out bool state))
894  {
895  state = !GameMain.LightManager.LightingEnabled;
896  }
897  GameMain.LightManager.LightingEnabled = state;
898  NewMessage("Lighting " + (GameMain.LightManager.LightingEnabled ? "enabled" : "disabled"), Color.Yellow);
899  });
900  AssignRelayToServer("lighting|lights", false);
901 
902  AssignOnExecute("ambientlight", (string[] args) =>
903  {
904  bool add = string.Equals(args.LastOrDefault(), "add");
905  string colorString = string.Join(",", add ? args.SkipLast(1) : args);
906  if (colorString.Equals("restore", StringComparison.OrdinalIgnoreCase))
907  {
908  foreach (Hull hull in Hull.HullList)
909  {
910  if (hull.OriginalAmbientLight != null)
911  {
912  hull.AmbientLight = hull.OriginalAmbientLight.Value;
913  hull.OriginalAmbientLight = null;
914  }
915  }
916  NewMessage("Restored all hull ambient lights", Color.Yellow);
917  return;
918  }
919 
920  Color color = XMLExtensions.ParseColor(colorString);
921  if (Level.Loaded != null)
922  {
923  Level.Loaded.GenerationParams.AmbientLightColor = color;
924  }
925  else
926  {
927  GameMain.LightManager.AmbientLight = add ? GameMain.LightManager.AmbientLight.Add(color) : color;
928  }
929 
930  foreach (Hull hull in Hull.HullList)
931  {
932  hull.OriginalAmbientLight ??= hull.AmbientLight;
933  hull.AmbientLight = add ? hull.AmbientLight.Add(color) : color;
934  }
935 
936  if (add)
937  {
938  NewMessage($"Set ambient light color to {color}.", Color.Yellow);
939  }
940  else
941  {
942  NewMessage($"Increased ambient light by {color}.", Color.Yellow);
943  }
944  });
945  AssignRelayToServer("ambientlight", false);
946 
947  commands.Add(new Command("multiplylights", "Multiplies the colors of all the static lights in the sub with the given Vector4 value (for example, 1,1,1,0.5).", (string[] args) =>
948  {
949  if (Screen.Selected != GameMain.SubEditorScreen || args.Length < 1)
950  {
951  ThrowError("The multiplylights command can only be used in the submarine editor.");
952  }
953  if (args.Length < 1) return;
954 
955  //join args in case there's spaces between the components
956  Vector4 value = XMLExtensions.ParseVector4(string.Join("", args));
957  foreach (Item item in Item.ItemList)
958  {
959  if (item.ParentInventory != null || item.body != null) continue;
960  var lightComponent = item.GetComponent<LightComponent>();
961  if (lightComponent != null) lightComponent.LightColor =
962  new Color(
963  (lightComponent.LightColor.R / 255.0f) * value.X,
964  (lightComponent.LightColor.G / 255.0f) * value.Y,
965  (lightComponent.LightColor.B / 255.0f) * value.Z,
966  (lightComponent.LightColor.A / 255.0f) * value.W);
967  }
968  }, isCheat: false));
969 
970  commands.Add(new Command("color|colour", "Change color (as bytes from 0 to 255) of the selected item/structure instances. Applied only in the subeditor.", (string[] args) =>
971  {
972  if (Screen.Selected == GameMain.SubEditorScreen)
973  {
974  if (!MapEntity.SelectedAny)
975  {
976  ThrowError("You have to select item(s)/structure(s) first!");
977  }
978  else
979  {
980  if (args.Length < 3)
981  {
982  ThrowError("Not enough arguments provided! At least three required.");
983  return;
984  }
985  if (!byte.TryParse(args[0], out byte r))
986  {
987  ThrowError($"Failed to parse value for RED from {args[0]}");
988  }
989  if (!byte.TryParse(args[1], out byte g))
990  {
991  ThrowError($"Failed to parse value for GREEN from {args[1]}");
992  }
993  if (!byte.TryParse(args[2], out byte b))
994  {
995  ThrowError($"Failed to parse value for BLUE from {args[2]}");
996  }
997  Color color = new Color(r, g, b);
998  if (args.Length > 3)
999  {
1000  if (!byte.TryParse(args[3], out byte a))
1001  {
1002  ThrowError($"Failed to parse value for ALPHA from {args[3]}");
1003  }
1004  else
1005  {
1006  color.A = a;
1007  }
1008  }
1009  foreach (var mapEntity in MapEntity.SelectedList)
1010  {
1011  if (mapEntity is Structure s)
1012  {
1013  s.SpriteColor = color;
1014  }
1015  else if (mapEntity is Item i)
1016  {
1017  i.SpriteColor = color;
1018  }
1019  }
1020  }
1021  }
1022  }, isCheat: true));
1023 
1024  commands.Add(new Command("listcloudfiles", "Lists all of your files on the Steam Cloud.", args =>
1025  {
1026  int i = 0;
1027  foreach (var file in Steamworks.SteamRemoteStorage.Files)
1028  {
1029  NewMessage($"* {i}: {file.Filename}, {file.Size} bytes", Color.Orange);
1030  i++;
1031  }
1032  NewMessage($"Bytes remaining: {Steamworks.SteamRemoteStorage.QuotaRemainingBytes}/{Steamworks.SteamRemoteStorage.QuotaBytes}", Color.Yellow);
1033  }));
1034 
1035  commands.Add(new Command("removefromcloud", "Removes a file from Steam Cloud.", args =>
1036  {
1037  if (args.Length < 1) { return; }
1038  var files = Steamworks.SteamRemoteStorage.Files;
1039  Steamworks.SteamRemoteStorage.RemoteFile file;
1040  if (int.TryParse(args[0], out int index) && index>=0 && index<files.Count)
1041  {
1042  file = files[index];
1043  }
1044  else
1045  {
1046  file = files.Find(f => f.Filename.Equals(args[0], StringComparison.InvariantCultureIgnoreCase));
1047  }
1048 
1049  if (!string.IsNullOrEmpty(file.Filename))
1050  {
1051  if (file.Delete())
1052  {
1053  NewMessage($"Deleting {file.Filename}", Color.Orange);
1054  }
1055  else
1056  {
1057  ThrowError($"Failed to delete {file.Filename}");
1058  }
1059  }
1060  }));
1061 
1062  commands.Add(new Command("resetall", "Reset all items and structures to prefabs. Only applicable in the subeditor.", args =>
1063  {
1064  if (Screen.Selected == GameMain.SubEditorScreen)
1065  {
1066  Item.ItemList.ForEach(i => i.Reset());
1067  Structure.WallList.ForEach(s => s.Reset());
1068  foreach (MapEntity entity in MapEntity.SelectedList)
1069  {
1070  if (entity is Item item)
1071  {
1072  item.CreateEditingHUD();
1073  break;
1074  }
1075  else if (entity is Structure structure)
1076  {
1077  structure.CreateEditingHUD();
1078  break;
1079  }
1080  }
1081  }
1082  }));
1083 
1084  commands.Add(new Command("resetentitiesbyidentifier", "resetentitiesbyidentifier [tag/identifier]: Reset items and structures with the given tag/identifier to prefabs. Only applicable in the subeditor.", args =>
1085  {
1086  if (args.Length == 0) { return; }
1087  if (Screen.Selected == GameMain.SubEditorScreen)
1088  {
1089  bool entityFound = false;
1090  foreach (MapEntity entity in MapEntity.MapEntityList)
1091  {
1092  if (entity is Item item)
1093  {
1094  if (item.Prefab.Identifier != args[0] && !item.Tags.Contains(args[0])) { continue; }
1095  item.Reset();
1096  if (MapEntity.SelectedList.Contains(item)) { item.CreateEditingHUD(); }
1097  entityFound = true;
1098  }
1099  else if (entity is Structure structure)
1100  {
1101  if (structure.Prefab.Identifier != args[0] && !structure.Tags.Contains(args[0])) { continue; }
1102  structure.Reset();
1103  if (MapEntity.SelectedList.Contains(structure)) { structure.CreateEditingHUD(); }
1104  entityFound = true;
1105  }
1106  else
1107  {
1108  continue;
1109  }
1110  NewMessage($"Reset {entity.Name}.");
1111  }
1112  if (!entityFound)
1113  {
1114  if (MapEntity.SelectedList.Count == 0)
1115  {
1116  NewMessage("No entities selected.");
1117  return;
1118  }
1119  }
1120  }
1121  }, () =>
1122  {
1123  return new string[][]
1124  {
1125  MapEntityPrefab.List.Select(me => me.Identifier.Value).ToArray()
1126  };
1127  }));
1128 
1129  commands.Add(new Command("resetselected", "Reset selected items and structures to prefabs. Only applicable in the subeditor.", args =>
1130  {
1131  if (Screen.Selected == GameMain.SubEditorScreen)
1132  {
1133  if (MapEntity.SelectedList.Count == 0)
1134  {
1135  NewMessage("No entities selected.");
1136  return;
1137  }
1138 
1139  foreach (MapEntity entity in MapEntity.SelectedList)
1140  {
1141  if (entity is Item item)
1142  {
1143  item.Reset();
1144  }
1145  else if (entity is Structure structure)
1146  {
1147  structure.Reset();
1148  }
1149  else
1150  {
1151  continue;
1152  }
1153  NewMessage($"Reset {entity.Name}.");
1154  }
1155  foreach (MapEntity entity in MapEntity.SelectedList)
1156  {
1157  if (entity is Item item)
1158  {
1159  item.CreateEditingHUD();
1160  break;
1161  }
1162  else if (entity is Structure structure)
1163  {
1164  structure.CreateEditingHUD();
1165  break;
1166  }
1167  }
1168  }
1169  }));
1170 
1171  commands.Add(new Command("alpha", "Change the alpha (as bytes from 0 to 255) of the selected item/structure instances. Applied only in the subeditor.", (string[] args) =>
1172  {
1173  if (Screen.Selected == GameMain.SubEditorScreen)
1174  {
1175  if (!MapEntity.SelectedAny)
1176  {
1177  ThrowError("You have to select item(s)/structure(s) first!");
1178  }
1179  else
1180  {
1181  if (args.Length > 0)
1182  {
1183  if (!byte.TryParse(args[0], out byte a))
1184  {
1185  ThrowError($"Failed to parse value for ALPHA from {args[0]}");
1186  }
1187  else
1188  {
1189  foreach (var mapEntity in MapEntity.SelectedList)
1190  {
1191  if (mapEntity is Structure s)
1192  {
1193  s.SpriteColor = new Color(s.SpriteColor.R, s.SpriteColor.G, s.SpriteColor.G, a);
1194  }
1195  else if (mapEntity is Item i)
1196  {
1197  i.SpriteColor = new Color(i.SpriteColor.R, i.SpriteColor.G, i.SpriteColor.G, a);
1198  }
1199  }
1200  }
1201  }
1202  else
1203  {
1204  ThrowError("Not enough arguments provided! One required!");
1205  }
1206  }
1207  }
1208  }, isCheat: true));
1209 
1210  commands.Add(new Command("cleansub", "", (string[] args) =>
1211  {
1212  for (int i = MapEntity.MapEntityList.Count - 1; i >= 0; i--)
1213  {
1214  MapEntity me = MapEntity.MapEntityList[i];
1215 
1216  if (me.SimPosition.Length() > 2000.0f)
1217  {
1218  NewMessage("Removed " + me.Name + " (simposition " + me.SimPosition + ")", Color.Orange);
1219  MapEntity.MapEntityList.RemoveAt(i);
1220  }
1221  else if (!me.ShouldBeSaved)
1222  {
1223  NewMessage("Removed " + me.Name + " (!ShouldBeSaved)", Color.Orange);
1224  MapEntity.MapEntityList.RemoveAt(i);
1225  }
1226  else if (me is Item)
1227  {
1228  Item item = me as Item;
1229  var wire = item.GetComponent<Wire>();
1230  if (wire == null) continue;
1231 
1232  if (wire.GetNodes().Count > 0 && !wire.Connections.Any(c => c != null))
1233  {
1234  wire.Item.Drop(null);
1235  NewMessage("Dropped wire (ID: " + wire.Item.ID + ") - attached on wall but no connections found", Color.Orange);
1236  }
1237  }
1238  }
1239  }, isCheat: true));
1240 
1241  commands.Add(new Command("messagebox|guimessagebox", "messagebox [header] [msg] [default/ingame]: Creates a message box.", (string[] args) =>
1242  {
1243  var msgBox = new GUIMessageBox(
1244  args.Length > 0 ? args[0] : "",
1245  args.Length > 1 ? args[1] : "",
1246  buttons: new LocalizedString[] { "OK" },
1247  type: args.Length < 3 || args[2] == "default" ? GUIMessageBox.Type.Default : GUIMessageBox.Type.InGame);
1248 
1249  msgBox.Buttons[0].OnClicked = msgBox.Close;
1250  }));
1251 
1252  AssignOnExecute("debugdraw", (string[] args) =>
1253  {
1254  if (args.None() || !bool.TryParse(args[0], out bool state))
1255  {
1256  state = !GameMain.DebugDraw;
1257  }
1258  GameMain.DebugDraw = state;
1259  NewMessage("Debug draw mode " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow);
1260  });
1261  AssignRelayToServer("debugdraw", false);
1262 
1263  AssignOnExecute("debugdrawlos", (string[] args) =>
1264  {
1265  if (args.None() || !bool.TryParse(args[0], out bool state))
1266  {
1267  state = !GameMain.LightManager.DebugLos;
1268  }
1269  GameMain.LightManager.DebugLos = state;
1270  NewMessage("Los debug draw mode " + (GameMain.LightManager.DebugLos ? "enabled" : "disabled"), Color.Yellow);
1271  });
1272  AssignOnExecute("debugwiring", (string[] args) =>
1273  {
1274  if (args.None() || !bool.TryParse(args[0], out bool state))
1275  {
1276  state = !ConnectionPanel.DebugWiringMode;
1277  }
1279  NewMessage("Wiring debug mode " + (ConnectionPanel.DebugWiringMode ? "enabled" : "disabled"), Color.Yellow);
1280  });
1281  AssignRelayToServer("debugdraw", false);
1282 
1283  AssignOnExecute("devmode", (string[] args) =>
1284  {
1285  if (args.None() || !bool.TryParse(args[0], out bool state))
1286  {
1287  state = !GameMain.DevMode;
1288  }
1289  GameMain.DevMode = state;
1290  if (GameMain.DevMode)
1291  {
1292  GameMain.LightManager.LightingEnabled = false;
1293  GameMain.LightManager.LosEnabled = false;
1294  }
1295  else
1296  {
1297  GameMain.LightManager.LightingEnabled = true;
1298  GameMain.LightManager.LosEnabled = true;
1299  GameMain.LightManager.LosAlpha = 1f;
1300  }
1301  NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.Yellow);
1302  });
1303  AssignRelayToServer("devmode", false);
1304 
1305  AssignOnExecute("debugdrawlocalization", (string[] args) =>
1306  {
1307  if (args.None() || !bool.TryParse(args[0], out bool state))
1308  {
1309  state = !TextManager.DebugDraw;
1310  }
1311  TextManager.DebugDraw = state;
1312  NewMessage("Localization debug draw mode " + (TextManager.DebugDraw ? "enabled" : "disabled"), Color.Yellow);
1313  });
1314  AssignRelayToServer("debugdraw", false);
1315 
1316  AssignOnExecute("togglevoicechatfilters", (string[] args) =>
1317  {
1318  if (args.None() || !bool.TryParse(args[0], out bool state))
1319  {
1320  state = !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
1321  }
1322  var config = GameSettings.CurrentConfig;
1323  config.Audio.DisableVoiceChatFilters = state;
1324  GameSettings.SetCurrentConfig(config);
1325  NewMessage("Voice chat filters " + (GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters ? "disabled" : "enabled"), Color.Yellow);
1326  });
1327  AssignRelayToServer("togglevoicechatfilters", false);
1328 
1329  commands.Add(new Command("fpscounter", "fpscounter: Toggle the FPS counter.", (string[] args) =>
1330  {
1331  GameMain.ShowFPS = !GameMain.ShowFPS;
1332  NewMessage("FPS counter " + (GameMain.DebugDraw ? "enabled" : "disabled"), Color.Yellow);
1333  }));
1334  commands.Add(new Command("showperf", "showperf: Toggle performance statistics on/off.", (string[] args) =>
1335  {
1336  GameMain.ShowPerf = !GameMain.ShowPerf;
1337  NewMessage("Performance statistics " + (GameMain.ShowPerf ? "enabled" : "disabled"), Color.Yellow);
1338  }));
1339 
1340  AssignOnClientExecute("netstats", (string[] args) =>
1341  {
1342  if (GameMain.Client == null) return;
1343  GameMain.Client.ShowNetStats = !GameMain.Client.ShowNetStats;
1344  });
1345 
1346  commands.Add(new Command("hudlayoutdebugdraw|debugdrawhudlayout", "hudlayoutdebugdraw: Toggle the debug drawing mode of HUD layout areas on/off.", (string[] args) =>
1347  {
1348  HUDLayoutSettings.DebugDraw = !HUDLayoutSettings.DebugDraw;
1349  NewMessage("HUD layout debug draw mode " + (HUDLayoutSettings.DebugDraw ? "enabled" : "disabled"), Color.Yellow);
1350  }));
1351 
1352  commands.Add(new Command("interactdebugdraw|debugdrawinteract", "interactdebugdraw: Toggle the debug drawing mode of item interaction ranges on/off.", (string[] args) =>
1353  {
1354  Character.DebugDrawInteract = !Character.DebugDrawInteract;
1355  NewMessage("Interact debug draw mode " + (Character.DebugDrawInteract ? "enabled" : "disabled"), Color.Yellow);
1356  }, isCheat: true));
1357 
1358  AssignOnExecute("togglehud|hud", (string[] args) =>
1359  {
1360  GUI.DisableHUD = !GUI.DisableHUD;
1361  GameMain.Instance.IsMouseVisible = !GameMain.Instance.IsMouseVisible;
1362  NewMessage(GUI.DisableHUD ? "Disabled HUD" : "Enabled HUD", Color.Yellow);
1363  });
1364  AssignRelayToServer("togglehud|hud", false);
1365 
1366  AssignOnExecute("toggleupperhud", (string[] args) =>
1367  {
1368  GUI.DisableUpperHUD = !GUI.DisableUpperHUD;
1369  NewMessage(GUI.DisableUpperHUD ? "Disabled upper HUD" : "Enabled upper HUD", Color.Yellow);
1370  });
1371  AssignRelayToServer("toggleupperhud", false);
1372 
1373  AssignOnExecute("toggleitemhighlights", (string[] args) =>
1374  {
1375  GUI.DisableItemHighlights = !GUI.DisableItemHighlights;
1376  NewMessage(GUI.DisableItemHighlights ? "Disabled item highlights" : "Enabled item highlights", Color.Yellow);
1377  });
1378  AssignRelayToServer("toggleitemhighlights", false);
1379 
1380  AssignOnExecute("togglecharacternames", (string[] args) =>
1381  {
1382  GUI.DisableCharacterNames = !GUI.DisableCharacterNames;
1383  NewMessage(GUI.DisableCharacterNames ? "Disabled character names" : "Enabled character names", Color.Yellow);
1384  });
1385  AssignRelayToServer("togglecharacternames", false);
1386 
1387  AssignOnExecute("followsub", (string[] args) =>
1388  {
1389  Camera.FollowSub = !Camera.FollowSub;
1390  NewMessage(Camera.FollowSub ? "Set the camera to follow the closest submarine" : "Disabled submarine following.", Color.Yellow);
1391  });
1392  AssignRelayToServer("followsub", false);
1393 
1394  AssignOnExecute("toggleaitargets|aitargets", (string[] args) =>
1395  {
1396  AITarget.ShowAITargets = !AITarget.ShowAITargets;
1397  NewMessage(AITarget.ShowAITargets ? "Enabled AI target drawing" : "Disabled AI target drawing", Color.Yellow);
1398  });
1399  AssignRelayToServer("toggleaitargets|aitargets", false);
1400 
1401  AssignOnExecute("debugai", (string[] args) =>
1402  {
1403  HumanAIController.DebugAI = !HumanAIController.DebugAI;
1404  if (HumanAIController.DebugAI)
1405  {
1406  GameMain.DevMode = true;
1407  GameMain.DebugDraw = true;
1408  GameMain.LightManager.LightingEnabled = false;
1409  GameMain.LightManager.LosEnabled = false;
1410  }
1411  else
1412  {
1413  GameMain.DevMode = false;
1414  GameMain.DebugDraw = false;
1415  GameMain.LightManager.LightingEnabled = true;
1416  GameMain.LightManager.LosEnabled = true;
1417  GameMain.LightManager.LosAlpha = 1f;
1418  }
1419  NewMessage(HumanAIController.DebugAI ? "AI debug info visible" : "AI debug info hidden", Color.Yellow);
1420  });
1421  AssignRelayToServer("debugai", false);
1422 
1423  AssignOnExecute("showmonsters", (string[] args) =>
1424  {
1425  CreatureMetrics.UnlockAll = true;
1426  CreatureMetrics.Save();
1427  NewMessage("All monsters are now visible in the character editor.", Color.Yellow);
1428  if (Screen.Selected == GameMain.CharacterEditorScreen)
1429  {
1430  GameMain.CharacterEditorScreen.Deselect();
1431  GameMain.CharacterEditorScreen.Select();
1432  }
1433  });
1434  AssignRelayToServer("showmonsters", false);
1435 
1436  AssignOnExecute("hidemonsters", (string[] args) =>
1437  {
1438  CreatureMetrics.UnlockAll = false;
1439  CreatureMetrics.Save();
1440  NewMessage("All monsters that haven't yet been encountered in the game are now hidden in the character editor.", Color.Yellow);
1441  if (Screen.Selected == GameMain.CharacterEditorScreen)
1442  {
1443  GameMain.CharacterEditorScreen.Deselect();
1444  GameMain.CharacterEditorScreen.Select();
1445  }
1446  });
1447  AssignRelayToServer("hidemonsters", false);
1448 
1449  AssignRelayToServer("water|editwater", false);
1450  AssignRelayToServer("fire|editfire", false);
1451 #if DEBUG
1452  AssignRelayToServer("debugvoip", true);
1453 #endif
1454 
1455  commands.Add(new Command("mute", "mute [name]: Prevent the client from speaking to anyone through the voice chat. Using this command requires a permission from the server host.",
1456  null,
1457  () =>
1458  {
1459  if (GameMain.Client == null) return null;
1460  return new string[][]
1461  {
1462  GameMain.Client.ConnectedClients.Select(c => c.Name).ToArray()
1463  };
1464  }));
1465  commands.Add(new Command("unmute", "unmute [name]: Allow the client to speak to anyone through the voice chat. Using this command requires a permission from the server host.",
1466  null,
1467  () =>
1468  {
1469  if (GameMain.Client == null) return null;
1470  return new string[][]
1471  {
1472  GameMain.Client.ConnectedClients.Select(c => c.Name).ToArray()
1473  };
1474  }));
1475 
1476  commands.Add(new Command("checkcrafting", "checkcrafting: Checks item deconstruction & crafting recipes for inconsistencies.", (string[] args) =>
1477  {
1478  List<FabricationRecipe> fabricableItems = new List<FabricationRecipe>();
1479  foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
1480  {
1481  fabricableItems.AddRange(itemPrefab.FabricationRecipes.Values);
1482  }
1483  foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
1484  {
1485  int? minCost = itemPrefab.GetMinPrice();
1486  int? fabricationCost = null;
1487  int? deconstructProductCost = null;
1488 
1489  var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == itemPrefab && f.RequiredItems.Any());
1490  if (fabricationRecipe != null)
1491  {
1492  foreach (var ingredient in fabricationRecipe.RequiredItems)
1493  {
1494  int? ingredientPrice = ingredient.ItemPrefabs.Min(ip => ip.GetMinPrice());
1495  if (ingredientPrice.HasValue)
1496  {
1497  if (!fabricationCost.HasValue) { fabricationCost = 0; }
1498  float useAmount = ingredient.UseCondition ? ingredient.MinCondition : 1.0f;
1499  fabricationCost += (int)(ingredientPrice.Value * ingredient.Amount * useAmount);
1500  }
1501  }
1502  }
1503 
1504  foreach (var deconstructItem in itemPrefab.DeconstructItems)
1505  {
1506  if (!(MapEntityPrefab.Find(null, deconstructItem.ItemIdentifier, showErrorMessages: false) is ItemPrefab targetItem))
1507  {
1508  ThrowErrorLocalized("Error in item \"" + itemPrefab.Name + "\" - could not find deconstruct item \"" + deconstructItem.ItemIdentifier + "\"!");
1509  continue;
1510  }
1511 
1512  float avgOutCondition = (deconstructItem.OutConditionMin + deconstructItem.OutConditionMax) / 2;
1513 
1514  int? deconstructProductPrice = targetItem.GetMinPrice();
1515  if (deconstructProductPrice.HasValue)
1516  {
1517  if (!deconstructProductCost.HasValue) { deconstructProductCost = 0; }
1518  deconstructProductCost += (int)(deconstructProductPrice * avgOutCondition);
1519  }
1520 
1521  if (fabricationRecipe != null)
1522  {
1523  var ingredient = fabricationRecipe.RequiredItems.Find(r => r.ItemPrefabs.Contains(targetItem));
1524 
1525  if (ingredient == null)
1526  {
1527  foreach (var requiredItem in fabricationRecipe.RequiredItems)
1528  {
1529  foreach (var itemPrefab2 in requiredItem.ItemPrefabs)
1530  {
1531  foreach (var recipe in itemPrefab2.FabricationRecipes.Values)
1532  {
1533  ingredient ??= recipe.RequiredItems.Find(r => r.ItemPrefabs.Contains(targetItem));
1534  }
1535  }
1536  }
1537  }
1538 
1539  if (ingredient == null)
1540  {
1541  NewMessage("Deconstructing \"" + itemPrefab.Name + "\" produces \"" + deconstructItem.ItemIdentifier + "\", which isn't required in the fabrication recipe of the item.", Color.Red);
1542  }
1543  else if (ingredient.UseCondition && ingredient.MinCondition < avgOutCondition)
1544  {
1545  NewMessage($"Deconstructing \"{itemPrefab.Name}\" produces more \"{deconstructItem.ItemIdentifier}\", than what's required to fabricate the item (required: {targetItem.Name} {(int)(ingredient.MinCondition * 100)}%, output: {deconstructItem.ItemIdentifier} {(int)(avgOutCondition * 100)}%)", Color.Red);
1546  }
1547  }
1548  }
1549 
1550  if (fabricationCost.HasValue && minCost.HasValue)
1551  {
1552  if (fabricationCost.Value < minCost * 0.9f)
1553  {
1554  float ratio = (float)fabricationCost.Value / minCost.Value;
1555  Color color = ToolBox.GradientLerp(ratio, Color.Red, Color.Yellow, Color.Green);
1556  NewMessage("The fabrication ingredients of \"" + itemPrefab.Name + "\" only cost " + (int)(ratio * 100) + "% of the price of the item. Item price: " + minCost.Value + ", ingredient prices: " + fabricationCost.Value, color);
1557  }
1558  else if (fabricationCost.Value > minCost * 1.1f)
1559  {
1560  float ratio = (float)fabricationCost.Value / minCost.Value;
1561  Color color = ToolBox.GradientLerp(ratio - 1.0f, Color.Green, Color.Yellow, Color.Red);
1562  NewMessage("The fabrication ingredients of \"" + itemPrefab.Name + "\" cost " + (int)(ratio * 100 - 100) + "% more than the price of the item. Item price: " + minCost.Value + ", ingredient prices: " + fabricationCost.Value, color);
1563  }
1564  }
1565  if (deconstructProductCost.HasValue && minCost.HasValue)
1566  {
1567  if (deconstructProductCost.Value < minCost * 0.8f)
1568  {
1569  float ratio = (float)deconstructProductCost.Value / minCost.Value;
1570  Color color = ToolBox.GradientLerp(ratio, Color.Red, Color.Yellow, Color.Green);
1571  NewMessage("The deconstruction output of \"" + itemPrefab.Name + "\" is only worth " + (int)(ratio * 100) + "% of the price of the item. Item price: " + minCost.Value + ", output value: " + deconstructProductCost.Value, color);
1572  }
1573  else if (deconstructProductCost.Value > minCost * 1.1f)
1574  {
1575  float ratio = (float)deconstructProductCost.Value / minCost.Value;
1576  Color color = ToolBox.GradientLerp(ratio - 1.0f, Color.Green, Color.Yellow, Color.Red);
1577  NewMessage("The deconstruction output of \"" + itemPrefab.Name + "\" is worth " + (int)(ratio * 100 - 100) + "% more than the price of the item. Item price: " + minCost.Value + ", output value: " + deconstructProductCost.Value, color);
1578  }
1579  }
1580  }
1581  }, isCheat: false));
1582 
1583  commands.Add(new Command("analyzeitem", "analyzeitem: Analyzes one item for exploits.", (string[] args) =>
1584  {
1585  if (args.Length < 1) { return; }
1586 
1587  List<FabricationRecipe> fabricableItems = new List<FabricationRecipe>();
1588  foreach (ItemPrefab iPrefab in ItemPrefab.Prefabs)
1589  {
1590  fabricableItems.AddRange(iPrefab.FabricationRecipes.Values);
1591  }
1592 
1593  string itemNameOrId = args[0].ToLowerInvariant();
1594 
1595  ItemPrefab itemPrefab =
1596  (MapEntityPrefab.FindByName(itemNameOrId) ??
1597  MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab;
1598 
1599  if (itemPrefab == null)
1600  {
1601  NewMessage("Item not found for analyzing.");
1602  return;
1603  }
1604  if (itemPrefab.DefaultPrice == null)
1605  {
1606  NewMessage($"Item \"{itemPrefab.Name}\" is not sellable/purchaseable.");
1607  return;
1608  }
1609  NewMessage("Analyzing item " + itemPrefab.Name + " with base cost " + itemPrefab.DefaultPrice.Price);
1610 
1611  var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == itemPrefab);
1612  // omega nesting incoming
1613  if (fabricationRecipe != null)
1614  {
1615  foreach (var priceInfo in itemPrefab.GetSellPricesOver(0))
1616  {
1617  NewMessage($" If bought at {GetSeller(priceInfo.Value)} it costs {priceInfo.Value.Price}");
1618  int totalPrice = 0;
1619  int? totalBestPrice = 0;
1620  foreach (var ingredient in fabricationRecipe.RequiredItems)
1621  {
1622  foreach (ItemPrefab ingredientItemPrefab in ingredient.ItemPrefabs)
1623  {
1624  int defaultPrice = ingredientItemPrefab.DefaultPrice?.Price ?? 0;
1625  NewMessage($" Its ingredient {ingredientItemPrefab.Name} has base cost {defaultPrice}");
1626  totalPrice += defaultPrice;
1627  totalBestPrice += ingredientItemPrefab.GetMinPrice();
1628  int basePrice = defaultPrice;
1629  foreach (var ingredientItemPriceInfo in ingredientItemPrefab.GetBuyPricesUnder())
1630  {
1631  if (basePrice > ingredientItemPriceInfo.Value.Price)
1632  {
1633  NewMessage($" {GetSeller(ingredientItemPriceInfo.Value).CapitaliseFirstInvariant()} sells ingredient {ingredientItemPrefab.Name} for cheaper, {ingredientItemPriceInfo.Value.Price}", Color.Yellow);
1634  }
1635  else
1636  {
1637  NewMessage($" {GetSeller(ingredientItemPriceInfo.Value).CapitaliseFirstInvariant()} sells ingredient {ingredientItemPrefab.Name} for more, {ingredientItemPriceInfo.Value.Price}", Color.Teal);
1638  }
1639  }
1640  }
1641  }
1642  int costDifference = itemPrefab.DefaultPrice.Price - totalPrice;
1643  NewMessage($" Constructing the item from store-bought items provides {costDifference} profit with default values.");
1644 
1645  if (totalBestPrice.HasValue)
1646  {
1647  int? bestDifference = priceInfo.Value.Price - totalBestPrice;
1648  NewMessage($" Constructing the item from store-bought items provides {bestDifference} profit with best-case scenario values.");
1649  }
1650 
1651  static string GetSeller(PriceInfo priceInfo) => $"store with identifier \"{priceInfo.StoreIdentifier}\"";
1652  }
1653  }
1654  },
1655  () =>
1656  {
1657  return new string[][] { ItemPrefab.Prefabs.SelectMany(p => p.Aliases).Concat(ItemPrefab.Prefabs.Select(p => p.Identifier.Value)).ToArray() };
1658  }, isCheat: false));
1659 
1660  commands.Add(new Command("checkcraftingexploits", "checkcraftingexploits: Finds outright item exploits created by buying store-bought ingredients and constructing them into sellable items.", (string[] args) =>
1661  {
1662  List<FabricationRecipe> fabricableItems = new List<FabricationRecipe>();
1663  foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
1664  {
1665  fabricableItems.AddRange(itemPrefab.FabricationRecipes.Values);
1666  }
1667  List<Tuple<string, int>> costDifferences = new List<Tuple<string, int>>();
1668 
1669  int maximumAllowedCost = 5;
1670 
1671  if (args.Length > 0)
1672  {
1673  Int32.TryParse(args[0], out maximumAllowedCost);
1674  }
1675  foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
1676  {
1677  int? defaultCost = itemPrefab.DefaultPrice?.Price;
1678  int? fabricationCostStore = null;
1679 
1680  var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == itemPrefab);
1681  if (fabricationRecipe == null)
1682  {
1683  continue;
1684  }
1685 
1686  bool canBeBought = true;
1687 
1688  foreach (var ingredient in fabricationRecipe.RequiredItems)
1689  {
1690  int? ingredientPrice = ingredient.ItemPrefabs.Where(p => p.CanBeBought).Min(ip => ip.DefaultPrice?.Price);
1691  if (ingredientPrice.HasValue)
1692  {
1693  if (!fabricationCostStore.HasValue) { fabricationCostStore = 0; }
1694  float useAmount = ingredient.UseCondition ? ingredient.MinCondition : 1.0f;
1695  fabricationCostStore += (int)(ingredientPrice.Value * ingredient.Amount * useAmount);
1696  }
1697  else
1698  {
1699  canBeBought = false;
1700  }
1701  }
1702  if (fabricationCostStore.HasValue && defaultCost.HasValue && canBeBought)
1703  {
1704  int costDifference = defaultCost.Value - fabricationCostStore.Value;
1705  if (costDifference > maximumAllowedCost || costDifference < 0f)
1706  {
1707  float ratio = (float)fabricationCostStore.Value / defaultCost.Value;
1708  string message = $"Fabricating \"{itemPrefab.Name}\" costs {(int)(ratio * 100)}% of the price of the item, or {costDifference} more. Item price: {defaultCost.Value}, ingredient prices: {fabricationCostStore.Value}";
1709  costDifferences.Add(new Tuple<string, int>(message, costDifference));
1710  }
1711  }
1712  }
1713 
1714  costDifferences.Sort((x, y) => x.Item2.CompareTo(y.Item2));
1715 
1716  foreach (Tuple<string, int> costDifference in costDifferences)
1717  {
1718  Color color = Color.Yellow;
1719  NewMessage(costDifference.Item1, color);
1720  }
1721  }, isCheat: false));
1722 
1723  commands.Add(new Command("adjustprice", "adjustprice: Recursively prints out expected price adjustments for items derived from this item.", (string[] args) =>
1724  {
1725  List<FabricationRecipe> fabricableItems = new List<FabricationRecipe>();
1726  foreach (ItemPrefab iP in ItemPrefab.Prefabs)
1727  {
1728  fabricableItems.AddRange(iP.FabricationRecipes.Values);
1729  }
1730  if (args.Length < 2)
1731  {
1732  NewMessage("Item or value not defined.");
1733  return;
1734  }
1735  string itemNameOrId = args[0].ToLowerInvariant();
1736 
1737  ItemPrefab materialPrefab =
1738  (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ??
1739  MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab;
1740 
1741  if (materialPrefab == null)
1742  {
1743  NewMessage("Item not found for price adjustment.");
1744  return;
1745  }
1746 
1747  AdjustItemTypes adjustItemType = AdjustItemTypes.NoAdjustment;
1748  if (args.Length > 2)
1749  {
1750  switch (args[2].ToLowerInvariant())
1751  {
1752  case "add":
1753  adjustItemType = AdjustItemTypes.Additive;
1754  break;
1755  case "mult":
1756  adjustItemType = AdjustItemTypes.Multiplicative;
1757  break;
1758  }
1759  }
1760 
1761  if (Int32.TryParse(args[1].ToLowerInvariant(), out int newPrice))
1762  {
1763  Dictionary<ItemPrefab, int> newPrices = new Dictionary<ItemPrefab, int>();
1764  PrintItemCosts(newPrices, materialPrefab, fabricableItems, newPrice, true, adjustItemType: adjustItemType);
1765  PrintItemCosts(newPrices, materialPrefab, fabricableItems, newPrice, false, adjustItemType: adjustItemType);
1766  }
1767 
1768  }, isCheat: false));
1769 
1770  commands.Add(new Command("deconstructvalue", "deconstructvalue: Views and compares deconstructed component prices for this item.", (string[] args) =>
1771  {
1772  List<FabricationRecipe> fabricableItems = new List<FabricationRecipe>();
1773  foreach (ItemPrefab iP in ItemPrefab.Prefabs)
1774  {
1775  fabricableItems.AddRange(iP.FabricationRecipes.Values);
1776  }
1777  if (args.Length < 1)
1778  {
1779  NewMessage("Item not defined.");
1780  return;
1781  }
1782  string itemNameOrId = args[0].ToLowerInvariant();
1783 
1784  ItemPrefab parentItem =
1785  (MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ??
1786  MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab;
1787 
1788  if (parentItem == null)
1789  {
1790  NewMessage("Item not found for price adjustment.");
1791  return;
1792  }
1793 
1794  var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == parentItem);
1795  int totalValue = 0;
1796  NewMessage(parentItem.Name + " has the price " + (parentItem.DefaultPrice?.Price ?? 0));
1797  if (fabricationRecipe != null)
1798  {
1799  NewMessage(" It constructs from:");
1800 
1801  foreach (RequiredItem requiredItem in fabricationRecipe.RequiredItems)
1802  {
1803  foreach (ItemPrefab itemPrefab in requiredItem.ItemPrefabs)
1804  {
1805  int defaultPrice = itemPrefab.DefaultPrice?.Price ?? 0;
1806  NewMessage(" " + itemPrefab.Name + " has the price " + defaultPrice);
1807  totalValue += defaultPrice;
1808  }
1809  }
1810  NewMessage("Its total value was: " + totalValue);
1811  totalValue = 0;
1812  }
1813  NewMessage(" The item deconstructs into:");
1814  foreach (DeconstructItem deconstructItem in parentItem.DeconstructItems)
1815  {
1816  ItemPrefab itemPrefab =
1817  (MapEntityPrefab.Find(deconstructItem.ItemIdentifier.Value, identifier: null, showErrorMessages: false) ??
1818  MapEntityPrefab.Find(null, identifier: deconstructItem.ItemIdentifier, showErrorMessages: false)) as ItemPrefab;
1819  if (itemPrefab == null)
1820  {
1821  ThrowError($" Couldn't find deconstruct product \"{deconstructItem.ItemIdentifier}\"!");
1822  continue;
1823  }
1824 
1825  int defaultPrice = itemPrefab.DefaultPrice?.Price ?? 0;
1826  NewMessage(" " + itemPrefab.Name + " has the price " + defaultPrice);
1827  totalValue += defaultPrice;
1828  }
1829  NewMessage("Its deconstruct value was: " + totalValue);
1830 
1831  }, isCheat: false));
1832 
1833  commands.Add(new Command("setentityproperties", "setentityproperties [property name] [value]: Sets the value of some property on all selected items/structures in the sub editor.", (string[] args) =>
1834  {
1835  if (args.Length != 2 || Screen.Selected != GameMain.SubEditorScreen) { return; }
1836  foreach (MapEntity me in MapEntity.SelectedList)
1837  {
1838  bool propertyFound = false;
1839  if (!(me is ISerializableEntity serializableEntity)) { continue; }
1840  if (serializableEntity.SerializableProperties == null) { continue; }
1841 
1842  if (serializableEntity.SerializableProperties.TryGetValue(args[0].ToIdentifier(), out SerializableProperty property))
1843  {
1844  propertyFound = true;
1845  object prevValue = property.GetValue(me);
1846  if (property.TrySetValue(me, args[1]))
1847  {
1848  NewMessage($"Changed the value \"{args[0]}\" from {(prevValue?.ToString() ?? null)} to {args[1]} on entity \"{me.ToString()}\".", Color.LightGreen);
1849  }
1850  else
1851  {
1852  NewMessage($"Failed to set the value of \"{args[0]}\" to \"{args[1]}\" on the entity \"{me.ToString()}\".", Color.Orange);
1853  }
1854  }
1855  if (me is Item item)
1856  {
1857  foreach (ItemComponent ic in item.Components)
1858  {
1859  ic.SerializableProperties.TryGetValue(args[0].ToIdentifier(), out SerializableProperty componentProperty);
1860  if (componentProperty == null) { continue; }
1861  propertyFound = true;
1862  object prevValue = componentProperty.GetValue(ic);
1863  if (componentProperty.TrySetValue(ic, args[1]))
1864  {
1865  NewMessage($"Changed the value \"{args[0]}\" from {prevValue} to {args[1]} on item \"{me.ToString()}\", component \"{ic.GetType().Name}\".", Color.LightGreen);
1866  }
1867  else
1868  {
1869  NewMessage($"Failed to set the value of \"{args[0]}\" to \"{args[1]}\" on the item \"{me.ToString()}\", component \"{ic.GetType().Name}\".", Color.Orange);
1870  }
1871  }
1872  }
1873  if (!propertyFound)
1874  {
1875  NewMessage($"Property \"{args[0]}\" not found in the entity \"{me.ToString()}\".", Color.Orange);
1876  }
1877  }
1878  },
1879  () =>
1880  {
1881  List<Identifier> propertyList = new List<Identifier>();
1882  foreach (MapEntity me in MapEntity.SelectedList)
1883  {
1884  if (!(me is ISerializableEntity serializableEntity)) { continue; }
1885  if (serializableEntity.SerializableProperties == null) { continue; }
1886  propertyList.AddRange(serializableEntity.SerializableProperties.Select(p => p.Key));
1887  if (me is Item item)
1888  {
1889  foreach (ItemComponent ic in item.Components)
1890  {
1891  propertyList.AddRange(ic.SerializableProperties.Select(p => p.Key));
1892  }
1893  }
1894  }
1895 
1896  return new string[][]
1897  {
1898  propertyList.Distinct().Select(i => i.Value).OrderBy(n => n).ToArray(),
1899  Array.Empty<string>()
1900  };
1901  }));
1902 
1903  commands.Add(new Command("checkmissingloca", "", (string[] args) =>
1904  {
1905  void SwapLanguage(LanguageIdentifier language)
1906  {
1907  var config = GameSettings.CurrentConfig;
1908  config.Language = language;
1909  GameSettings.SetCurrentConfig(config);
1910  }
1911 
1912  HashSet<string> missingTexts = new HashSet<string>();
1913 
1914  //key = text tag, value = list of languages the tag is missing from
1915  Dictionary<Identifier, HashSet<LanguageIdentifier>> missingTags = new Dictionary<Identifier, HashSet<LanguageIdentifier>>();
1916  Dictionary<LanguageIdentifier, HashSet<Identifier>> tags = new Dictionary<LanguageIdentifier, HashSet<Identifier>>();
1917  foreach (LanguageIdentifier language in TextManager.AvailableLanguages)
1918  {
1919  SwapLanguage(language);
1920  tags.Add(language, new HashSet<Identifier>(TextManager.GetAllTagTextPairs().Select(t => t.Key)));
1921  }
1922 
1923  foreach (LanguageIdentifier language in TextManager.AvailableLanguages)
1924  {
1925  //check missing mission texts
1926  foreach (var missionPrefab in MissionPrefab.Prefabs)
1927  {
1928  Identifier missionId = missionPrefab.ConfigElement.GetAttribute("textidentifier") == null ?
1929  missionPrefab.Identifier :
1930  missionPrefab.ConfigElement.GetAttributeIdentifier("textidentifier", Identifier.Empty);
1931 
1932  if (!tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("name", Identifier.Empty)))
1933  {
1934  addIfMissing($"missionname.{missionId}".ToIdentifier(), language);
1935  }
1936 
1937  if (missionPrefab.Type == MissionType.Combat)
1938  {
1939  addIfMissing($"MissionDescriptionNeutral.{missionId}".ToIdentifier(), language);
1940  addIfMissing($"MissionDescription1.{missionId}".ToIdentifier(), language);
1941  addIfMissing($"MissionDescription2.{missionId}".ToIdentifier(), language);
1942  addIfMissing($"MissionTeam1.{missionId}".ToIdentifier(), language);
1943  addIfMissing($"MissionTeam2.{missionId}".ToIdentifier(), language);
1944  }
1945  else
1946  {
1947  if (!tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("description", Identifier.Empty)))
1948  {
1949  addIfMissing($"missiondescription.{missionId}".ToIdentifier(), language);
1950  }
1951  if (!tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("successmessage", Identifier.Empty)))
1952  {
1953  addIfMissing($"missionsuccess.{missionId}".ToIdentifier(), language);
1954  }
1955  //only check failure message if there's something defined in the xml (otherwise we just use the generic "missionfailed" text)
1956  if (missionPrefab.ConfigElement.GetAttribute("failuremessage") != null &&
1957  !tags[language].Contains(missionPrefab.ConfigElement.GetAttributeIdentifier("failuremessage", Identifier.Empty)))
1958  {
1959  addIfMissing($"missionfailure.{missionId}".ToIdentifier(), language);
1960  }
1961  }
1962  for (int i = 0; i < missionPrefab.Messages.Length; i++)
1963  {
1964  if (missionPrefab.Messages[i].IsNullOrWhiteSpace() || (missionPrefab.Messages[i] as FallbackLString)?.GetLastFallback() is FallbackLString { PrimaryIsLoaded: false })
1965  {
1966  addIfMissing($"MissionMessage{i}.{missionId}".ToIdentifier(), language);
1967  }
1968  }
1969  }
1970 
1971  foreach (var eventPrefab in EventPrefab.Prefabs)
1972  {
1973  if (eventPrefab is not TraitorEventPrefab traitorEventPrefab) { continue; }
1974  addIfMissing($"eventname.{traitorEventPrefab.Identifier}".ToIdentifier(), language);
1975  }
1976 
1977  foreach (Type itemComponentType in typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent))))
1978  {
1979  checkSerializableEntityType(itemComponentType);
1980  }
1981  checkSerializableEntityType(typeof(Item));
1982  checkSerializableEntityType(typeof(Hull));
1983  checkSerializableEntityType(typeof(Structure));
1984 
1985  void checkSerializableEntityType(Type t)
1986  {
1987  foreach (var property in t.GetProperties())
1988  {
1989  if (!property.IsDefined(typeof(Editable), false)) { continue; }
1990 
1991  string propertyTag = $"{property.DeclaringType.Name}.{property.Name}";
1992 
1993  if (addIfMissingAll(language,
1994  propertyTag.ToIdentifier(),
1995  property.Name.ToIdentifier(),
1996  $"sp.{property.Name}.name".ToIdentifier(),
1997  $"sp.{propertyTag}.name".ToIdentifier()) && language == "English".ToLanguageIdentifier())
1998  {
1999  missingTexts.Add($"<sp.{propertyTag.ToLower()}.name>{property.Name.FormatCamelCaseWithSpaces()}</sp.{propertyTag.ToLower()}.name>");
2000  }
2001 
2002  var description = (property.GetCustomAttributes(true).First(a => a is Serialize) as Serialize).Description;
2003 
2004  if (addIfMissingAll(language,
2005  $"sp.{propertyTag}.description".ToIdentifier(),
2006  $"sp.{property.Name}.description".ToIdentifier(),
2007  $"{property.Name.ToIdentifier()}.description".ToIdentifier()) && language == "English".ToLanguageIdentifier())
2008  {
2009  missingTexts.Add($"<sp.{propertyTag.ToLower()}.description>{description}</sp.{propertyTag.ToLower()}.description>");
2010  }
2011  }
2012  }
2013 
2014  foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines)
2015  {
2016  if (sub.Type != SubmarineType.Player || !sub.IsVanillaSubmarine()) { continue; }
2017 
2018  addIfMissing($"submarine.name.{sub.Name}".ToIdentifier(), language);
2019  addIfMissing(("submarine.description." + sub.Name).ToIdentifier(), language);
2020  }
2021 
2022  foreach (AfflictionPrefab affliction in AfflictionPrefab.List)
2023  {
2024  if (affliction.ShowIconThreshold > affliction.MaxStrength &&
2025  affliction.ShowIconToOthersThreshold > affliction.MaxStrength &&
2026  affliction.ShowInHealthScannerThreshold > affliction.MaxStrength)
2027  {
2028  //hidden affliction, no need for localization
2029  continue;
2030  }
2031 
2032  Identifier afflictionId = affliction.TranslationIdentifier;
2033  addIfMissing($"afflictionname.{afflictionId}".ToIdentifier(), language);
2034 
2035  if (affliction.Descriptions.Any())
2036  {
2037  foreach (var description in affliction.Descriptions)
2038  {
2039  addIfMissing(description.TextTag, language);
2040  }
2041  }
2042  else
2043  {
2044  addIfMissing($"afflictiondescription.{afflictionId}".ToIdentifier(), language);
2045  }
2046  }
2047 
2048  foreach (var talentTree in TalentTree.JobTalentTrees)
2049  {
2050  foreach (var talentSubTree in talentTree.TalentSubTrees)
2051  {
2052  addIfMissing($"talenttree.{talentSubTree.Identifier}".ToIdentifier(), language);
2053  }
2054  }
2055 
2056  foreach (var talent in TalentPrefab.TalentPrefabs)
2057  {
2058  addIfMissing($"talentname.{talent.Identifier}".ToIdentifier(), language);
2059  }
2060 
2061  //check missing entity names
2062  foreach (MapEntityPrefab me in MapEntityPrefab.List)
2063  {
2064  Identifier nameIdentifier = ("entityname." + me.Identifier).ToIdentifier();
2065  if (tags[language].Contains(nameIdentifier)) { continue; }
2066  if (me.HideInMenus) { continue; }
2067 
2068  ContentXElement configElement = null;
2069 
2070  if (me is ItemPrefab itemPrefab)
2071  {
2072  configElement = itemPrefab.ConfigElement;
2073  }
2074  else if (me is StructurePrefab structurePrefab)
2075  {
2076  configElement = structurePrefab.ConfigElement;
2077  }
2078  if (configElement != null)
2079  {
2080  var overrideIdentifier = configElement.GetAttributeIdentifier("nameidentifier", null);
2081  if (overrideIdentifier != null && tags[language].Contains("entityname." + overrideIdentifier)) { continue; }
2082  }
2083 
2084  addIfMissing(nameIdentifier, language);
2085  }
2086  }
2087 
2088  foreach (Identifier englishTag in tags[TextManager.DefaultLanguage])
2089  {
2090  foreach (LanguageIdentifier language in TextManager.AvailableLanguages)
2091  {
2092  if (language == TextManager.DefaultLanguage) { continue; }
2093  addIfMissing(englishTag, language);
2094  }
2095  }
2096 
2097  List<string> lines = new List<string>
2098  {
2099  "Missing from English:"
2100  };
2101 
2102  Dictionary<string, List<string>> missingByLanguages = new Dictionary<string, List<string>>();
2103  List<string> missingFromEnglish = new List<string>();
2104  foreach (KeyValuePair<Identifier, HashSet<LanguageIdentifier>> kvp in missingTags)
2105  {
2106  if (kvp.Value.Contains(TextManager.DefaultLanguage))
2107  {
2108  missingFromEnglish.Add(kvp.Key.Value);
2109  }
2110  else
2111  {
2112  string languagesStr = string.Join(", ", kvp.Value.OrderBy(v => v.Value.Value));
2113  if (!missingByLanguages.ContainsKey(languagesStr))
2114  {
2115  missingByLanguages.Add(languagesStr, new List<string>());
2116  }
2117  missingByLanguages[languagesStr].Add(kvp.Key.Value);
2118  }
2119  }
2120  foreach (string text in missingFromEnglish.OrderBy(v => v))
2121  {
2122  lines.Add(text);
2123  }
2124 
2125  foreach (KeyValuePair<string, List<string>> missingByLanguage in missingByLanguages)
2126  {
2127  lines.Add(string.Empty);
2128  lines.Add($"Missing from {missingByLanguage.Key}");
2129  foreach (string text in missingByLanguage.Value.OrderBy(v => v))
2130  {
2131  lines.Add(text);
2132  }
2133  }
2134 
2135  string filePath = "missingloca.txt";
2136  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
2137  File.WriteAllLines(filePath, lines);
2138  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
2139  ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
2140  SwapLanguage(TextManager.DefaultLanguage);
2141 
2142  if (missingTexts.Any())
2143  {
2144  ShowQuestionPrompt("Dump the property names and descriptions missing from English to a new xml file? Y/N",
2145  (option) =>
2146  {
2147  if (option.ToLowerInvariant() == "y")
2148  {
2149  string path = "newtexts.txt";
2150  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
2151  File.WriteAllLines(path, missingTexts);
2152  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
2153  ToolBox.OpenFileWithShell(Path.GetFullPath(path));
2154  SwapLanguage(TextManager.DefaultLanguage);
2155  }
2156  });
2157  }
2158 
2159  void addIfMissing(Identifier tag, LanguageIdentifier language)
2160  {
2161  if (!tags[language].Contains(tag))
2162  {
2163  if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet<LanguageIdentifier>(); }
2164  missingTags[tag].Add(language);
2165  }
2166  }
2167  bool addIfMissingAll(LanguageIdentifier language, params Identifier[] potentialTags)
2168  {
2169  if (!potentialTags.Any(t => tags[language].Contains(t)))
2170  {
2171  var tag = potentialTags.First();
2172  if (!missingTags.ContainsKey(tag)) { missingTags[tag] = new HashSet<LanguageIdentifier>(); }
2173  missingTags[tag].Add(language);
2174  return true;
2175  }
2176  return false;
2177  }
2178  }));
2179 
2180 
2181  commands.Add(new Command("checkduplicateloca", "", (string[] args) =>
2182  {
2183  if (args.Length < 1)
2184  {
2185  ThrowError("Please specify a file path.");
2186  return;
2187  }
2188  XDocument doc1 = XMLExtensions.TryLoadXml(args[0]);
2189  if (doc1?.Root == null)
2190  {
2191  ThrowError($"Could not load the file \"{args[0]}\"");
2192  return;
2193  }
2194  List<(string tag, string text)> texts = new List<(string tag, string text)>();
2195 
2196  bool duplicatesFound = false;
2197  foreach (XElement element in doc1.Root.Elements())
2198  {
2199  string tag = element.Name.ToString();
2200  string text = element.ElementInnerText();
2201  if (texts.Any(t => t.tag == tag))
2202  {
2203  ThrowError($"Duplicate tag \"{tag}\".");
2204  duplicatesFound = true;
2205  }
2206  }
2207  if (duplicatesFound)
2208  {
2209  ThrowError($"Aborting, please fix duplicate tags in the file and try again.");
2210  return;
2211  }
2212 
2213  foreach (XElement element in doc1.Root.Elements())
2214  {
2215  string tag = element.Name.ToString();
2216  string text = element.ElementInnerText();
2217  if (texts.Any(t => t.text == text))
2218  {
2219  if (tag.StartsWith("sp."))
2220  {
2221  string[] split = tag.Split('.');
2222  if (split.Length > 3)
2223  {
2224  texts.RemoveAll(t => t.text == text);
2225  string newTag = $"sp.{split[2]}.{split[3]}";
2226  texts.Add((newTag, text));
2227  NewMessage($"Duplicate text \"{tag}\", merging to \"{newTag}\".");
2228  }
2229  else
2230  {
2231  NewMessage($"Duplicate text \"{tag}\", using existing one \"{texts.Find(t => t.text == text).tag}\".");
2232  }
2233  }
2234  else
2235  {
2236  texts.Add((tag, text));
2237  ThrowError($"Duplicate text \"{tag}\". Could not determine if the text can be merged with an existing one, please check it manually.");
2238  }
2239  }
2240  else
2241  {
2242  texts.Add((tag, text));
2243  }
2244  }
2245 
2246  string filePath = "uniquetexts.xml";
2247  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
2248  File.WriteAllLines(filePath, texts.Select(t => $"<{t.tag}>{t.text}</{t.tag}>"));
2249  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
2250  ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
2251  }));
2252 
2253  commands.Add(new Command("comparelocafiles", "comparelocafiles [file1] [file2]", (string[] args) =>
2254  {
2255  if (args.Length < 2)
2256  {
2257  ThrowError("Please specify two files two compare.");
2258  return;
2259  }
2260 
2261  XDocument doc1 = XMLExtensions.TryLoadXml(args[0]);
2262  if (doc1?.Root == null)
2263  {
2264  ThrowError($"Could not load the file \"{args[0]}\"");
2265  return;
2266  }
2267  XDocument doc2 = XMLExtensions.TryLoadXml(args[1]);
2268  if (doc2?.Root == null)
2269  {
2270  ThrowError($"Could not load the file \"{args[1]}\"");
2271  return;
2272  }
2273 
2274  var content1 = getContent(doc1.Root);
2275  var language1 = doc1.Root.GetAttributeIdentifier("language", string.Empty);
2276 
2277  var content2 = getContent(doc2.Root);
2278  var language2 = doc2.Root.GetAttributeIdentifier("language", string.Empty);
2279 
2280  foreach (KeyValuePair<string, string> kvp in content1)
2281  {
2282  if (!content2.ContainsKey(kvp.Key))
2283  {
2284  ThrowError($"File 2 doesn't contain the text tag \"{kvp.Key}\"");
2285  }
2286  else if (language1 == language2 && content2[kvp.Key] != kvp.Value)
2287  {
2288  ThrowError($"Texts for the tag \"{kvp.Key}\" don't match:\n1. {kvp.Value}\n2. {content2[kvp.Key]}");
2289  }
2290  }
2291  foreach (KeyValuePair<string, string> kvp in content2)
2292  {
2293  if (!content1.ContainsKey(kvp.Key))
2294  {
2295  ThrowError($"File 1 doesn't contain the text tag \"{kvp.Key}\"");
2296  }
2297  }
2298 
2299  static Dictionary<string, string> getContent(XElement element)
2300  {
2301  Dictionary<string, string> content = new Dictionary<string, string>();
2302  foreach (XElement subElement in element.Elements())
2303  {
2304  string key = subElement.Name.ToString().ToLowerInvariant();
2305  if (content.ContainsKey(key)) { continue; }
2306  content.Add(key, subElement.ElementInnerText());
2307  }
2308  return content;
2309  }
2310  }));
2311 
2312  commands.Add(new Command("eventstats", "", (string[] args) =>
2313  {
2314  List<string> debugLines;
2315  if (args.Length > 0)
2316  {
2317  if (!Enum.TryParse(args[0], ignoreCase: true, out Level.PositionType spawnType))
2318  {
2319  var enums = Enum.GetNames(typeof(Level.PositionType));
2320  ThrowError($"\"{args[0]}\" is not a valid Level.PositionType. Available options are: {string.Join(", ", enums)}");
2321  return;
2322  }
2323  bool fullLog = false;
2324  if (args.Length > 1)
2325  {
2326  bool.TryParse(args[1], out fullLog);
2327  }
2328  debugLines = EventSet.GetDebugStatistics(filter: monsterEvent => monsterEvent.SpawnPosType.HasFlag(spawnType), fullLog: fullLog);
2329  }
2330  else
2331  {
2332  debugLines = EventSet.GetDebugStatistics();
2333  }
2334  string filePath = "eventstats.txt";
2335  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
2336  File.WriteAllLines(filePath, debugLines);
2337  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
2338  ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
2339  }));
2340 
2341  commands.Add(new Command("setfreecamspeed", "setfreecamspeed [speed]: Set the camera movement speed when not controlling a character. Defaults to 1.", (string[] args) =>
2342  {
2343  if (args.Length > 0)
2344  {
2345  float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out float speed);
2346  Screen.Selected.Cam.FreeCamMoveSpeed = speed;
2347  }
2348  }));
2349 
2350  commands.Add(new Command("converttowreck", "", (string[] args) =>
2351  {
2352  if (Screen.Selected is not SubEditorScreen)
2353  {
2354  ThrowError("The command can only be used in the submarine editor.");
2355  return;
2356  }
2357  if (Submarine.MainSub == null)
2358  {
2359  ThrowError("Load a submarine first to convert it to a wreck.");
2360  return;
2361  }
2362  if (Submarine.MainSub.Info.SubmarineElement == null)
2363  {
2364  ThrowError("The submarine must be saved before you can convert it to a wreck.");
2365  return;
2366  }
2367  var wreckedSubmarineInfo = new SubmarineInfo(filePath: string.Empty, element: WreckConverter.ConvertToWreck(Submarine.MainSub.Info.SubmarineElement));
2368  wreckedSubmarineInfo.Name += "_Wrecked";
2369  wreckedSubmarineInfo.Type = SubmarineType.Wreck;
2370  GameMain.SubEditorScreen.LoadSub(wreckedSubmarineInfo);
2371  }));
2372 
2373 #if DEBUG
2374  commands.Add(new Command("deathprompt", "Shows the death prompt for testing purposes.", (string[] args) =>
2375  {
2376  DeathPrompt.Create(delay: 1.0f);
2377  }));
2378 
2379  commands.Add(new Command("listspamfilters", "Lists filters that are in the global spam filter.", (string[] args) =>
2380  {
2381  if (!SpamServerFilters.GlobalSpamFilter.TryUnwrap(out var filter))
2382  {
2383  ThrowError("Global spam list is not initialized.");
2384  return;
2385  }
2386 
2387  if (!filter.Filters.Any())
2388  {
2389  NewMessage("Global spam list is empty.", GUIStyle.Green);
2390  return;
2391  }
2392 
2393  StringBuilder sb = new();
2394 
2395  foreach (var f in filter.Filters)
2396  {
2397  sb.AppendLine(f.ToString());
2398  }
2399 
2400  NewMessage(sb.ToString(), GUIStyle.Green);
2401  }));
2402 
2403  commands.Add(new Command("setplanthealth", "setplanthealth [value]: Sets the health of the selected plant in sub editor.", (string[] args) =>
2404  {
2405  if (1 > args.Length || Screen.Selected != GameMain.SubEditorScreen) { return; }
2406 
2407  string arg = args[0];
2408 
2409  if (!float.TryParse(arg, out float value))
2410  {
2411  ThrowError($"{arg} is not a valid value.");
2412  return;
2413  }
2414 
2415  foreach (MapEntity me in MapEntity.SelectedList)
2416  {
2417  if (me is Item it)
2418  {
2419  if (it.GetComponent<Planter>() is { } planter)
2420  {
2421  foreach (Growable seed in planter.GrowableSeeds.Where(s => s != null))
2422  {
2423  NewMessage($"Set the health of {seed.Name} to {value} (from {seed.Health})");
2424  seed.Health = value;
2425  }
2426  }
2427  else if (it.GetComponent<Growable>() is { } seed)
2428  {
2429  NewMessage($"Set the health of {seed.Name} to {value} (from {seed.Health})");
2430  seed.Health = value;
2431  }
2432  }
2433  }
2434  }));
2435 
2436  commands.Add(new Command("showballastflorasprite", "", (string[] args) =>
2437  {
2438  BallastFloraBehavior.AlwaysShowBallastFloraSprite = !BallastFloraBehavior.AlwaysShowBallastFloraSprite;
2439  NewMessage("ok", GUIStyle.Green);
2440  }));
2441 
2442  commands.Add(new Command("printreceivertransfers", "", (string[] args) =>
2443  {
2444  GameMain.Client.PrintReceiverTransters();
2445  }));
2446 
2447  commands.Add(new Command("spamchatmessages", "", (string[] args) =>
2448  {
2449  int msgCount = 1000;
2450  if (args.Length > 0) int.TryParse(args[0], out msgCount);
2451  int msgLength = 50;
2452  if (args.Length > 1) int.TryParse(args[1], out msgLength);
2453 
2454  for (int i = 0; i < msgCount; i++)
2455  {
2456  if (GameMain.Client != null)
2457  {
2458  GameMain.Client.SendChatMessage(ToolBox.RandomSeed(msgLength));
2459  }
2460  }
2461  }));
2462 
2463  commands.Add(new Command("getprefabinfo", "", (string[] args) =>
2464  {
2465  var prefab = MapEntityPrefab.Find(null, args[0]);
2466  if (prefab != null)
2467  {
2468  NewMessage(prefab.Name + " " + prefab.Identifier + " " + prefab.GetType().ToString());
2469  }
2470  }));
2471 
2472  commands.Add(new Command("copycharacterinfotoclipboard", "", (string[] args) =>
2473  {
2474  if (Character.Controlled?.Info != null)
2475  {
2476  XElement element = Character.Controlled?.Info.Save(null);
2477  Clipboard.SetText(element.ToString());
2478  DebugConsole.NewMessage($"Copied the characterinfo of {Character.Controlled.Name} to clipboard.");
2479  }
2480  }));
2481 
2482  commands.Add(new Command("spawnallitems", "", (string[] args) =>
2483  {
2484  var cursorPos = Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? Vector2.Zero;
2485  foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
2486  {
2487  Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, cursorPos);
2488  }
2489  }));
2490 
2491  commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) =>
2492  {
2493  float defaultZoom = Screen.Selected.Cam.DefaultZoom;
2494  if (args.Length > 0) float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out defaultZoom);
2495 
2496  float zoomSmoothness = Screen.Selected.Cam.ZoomSmoothness;
2497  if (args.Length > 1) float.TryParse(args[1], NumberStyles.Number, CultureInfo.InvariantCulture, out zoomSmoothness);
2498  float moveSmoothness = Screen.Selected.Cam.MoveSmoothness;
2499  if (args.Length > 2) float.TryParse(args[2], NumberStyles.Number, CultureInfo.InvariantCulture, out moveSmoothness);
2500 
2501  float minZoom = Screen.Selected.Cam.MinZoom;
2502  if (args.Length > 3) float.TryParse(args[3], NumberStyles.Number, CultureInfo.InvariantCulture, out minZoom);
2503  float maxZoom = Screen.Selected.Cam.MaxZoom;
2504  if (args.Length > 4) float.TryParse(args[4], NumberStyles.Number, CultureInfo.InvariantCulture, out maxZoom);
2505 
2506  Screen.Selected.Cam.DefaultZoom = defaultZoom;
2507  Screen.Selected.Cam.ZoomSmoothness = zoomSmoothness;
2508  Screen.Selected.Cam.MoveSmoothness = moveSmoothness;
2509  Screen.Selected.Cam.MinZoom = minZoom;
2510  Screen.Selected.Cam.MaxZoom = maxZoom;
2511  }));
2512 
2513  commands.Add(new Command("waterparams", "waterparams [distortionscalex] [distortionscaley] [distortionstrengthx] [distortionstrengthy] [bluramount]: default 0.5 0.5 0.5 0.5 1", (string[] args) =>
2514  {
2515  float distortScaleX = 0.5f, distortScaleY = 0.5f;
2516  float distortStrengthX = 0.5f, distortStrengthY = 0.5f;
2517  float blurAmount = 0.0f;
2518  if (args.Length > 0) float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out distortScaleX);
2519  if (args.Length > 1) float.TryParse(args[1], NumberStyles.Number, CultureInfo.InvariantCulture, out distortScaleY);
2520  if (args.Length > 2) float.TryParse(args[2], NumberStyles.Number, CultureInfo.InvariantCulture, out distortStrengthX);
2521  if (args.Length > 3) float.TryParse(args[3], NumberStyles.Number, CultureInfo.InvariantCulture, out distortStrengthY);
2522  if (args.Length > 4) float.TryParse(args[4], NumberStyles.Number, CultureInfo.InvariantCulture, out blurAmount);
2523  WaterRenderer.DistortionScale = new Vector2(distortScaleX, distortScaleY);
2524  WaterRenderer.DistortionStrength = new Vector2(distortStrengthX, distortStrengthY);
2525  WaterRenderer.BlurAmount = blurAmount;
2526  }));
2527 
2528  commands.Add(new Command("generatelevels", "generatelevels [amount]: generate a bunch of levels with the currently selected parameters in the level editor.", (string[] args) =>
2529  {
2530  if (GameMain.GameSession == null)
2531  {
2532  int amount = 1;
2533  if (args.Length > 0) { int.TryParse(args[0], out amount); }
2534  GameMain.LevelEditorScreen.TestLevelGenerationForErrors(amount);
2535  }
2536  else
2537  {
2538  NewMessage("Can't use the command while round is running.");
2539  }
2540  }));
2541 
2542  commands.Add(new Command("listcontainertags", "Lists all container tags on the submarine.", (string[] args) =>
2543  {
2544  if (Screen.Selected != GameMain.SubEditorScreen)
2545  {
2546  ThrowError("This command can only be used in the sub editor.");
2547  return;
2548  }
2549 
2550  HashSet<Identifier> allContainerTagsInTheGame = new();
2551 
2552  foreach (var itemPrefab in ItemPrefab.Prefabs)
2553  {
2554  foreach (var pc in itemPrefab.PreferredContainers)
2555  {
2556  foreach (Identifier identifier in Enumerable.Union(pc.Primary, pc.Secondary))
2557  {
2558  allContainerTagsInTheGame.Add(identifier);
2559  }
2560  }
2561  }
2562 
2563  Dictionary<Identifier, float> prefab = new();
2564 
2565  foreach (Item it in Item.ItemList)
2566  {
2567  foreach (var tag in allContainerTagsInTheGame)
2568  {
2569  if (it.GetTags().All(t => tag != t)) { continue; }
2570 
2571  prefab.TryAdd(tag, 0.0f);
2572  prefab[tag]++;
2573  }
2574  }
2575 
2576  StringBuilder sb = new();
2577  foreach (var (tag, amount) in prefab.OrderByDescending(kvp => kvp.Value))
2578  {
2579  sb.AppendLine($"{tag}: {amount}");
2580  }
2581 
2582  NewMessage(sb.ToString());
2583  }, isCheat: false));
2584 
2585  commands.Add(new Command("refreshrect", "Updates the dimensions of the selected items to match the ones defined in the prefab. Applied only in the subeditor.", (string[] args) =>
2586  {
2587  //TODO: maybe do this automatically during loading when possible?
2588  if (Screen.Selected == GameMain.SubEditorScreen)
2589  {
2590  if (!MapEntity.SelectedAny)
2591  {
2592  ThrowError("You have to select item(s) first!");
2593  }
2594  else
2595  {
2596  foreach (var mapEntity in MapEntity.SelectedList)
2597  {
2598  if (mapEntity is Item item)
2599  {
2600  item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y,
2601  (int)(item.Prefab.Sprite.size.X * item.Prefab.Scale),
2602  (int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale));
2603  }
2604  else if (mapEntity is Structure structure)
2605  {
2606  if (!structure.ResizeHorizontal)
2607  {
2608  structure.Rect = structure.DefaultRect = new Rectangle(structure.Rect.X, structure.Rect.Y,
2609  (int)structure.Prefab.ScaledSize.X,
2610  structure.Rect.Height);
2611  }
2612  if (!structure.ResizeVertical)
2613  {
2614  structure.Rect = structure.DefaultRect = new Rectangle(structure.Rect.X, structure.Rect.Y,
2615  structure.Rect.Width,
2616  (int)structure.Prefab.ScaledSize.Y);
2617  }
2618 
2619  }
2620  }
2621  }
2622  }
2623  }, isCheat: false));
2624 
2625  commands.Add(new Command("flip", "Flip the currently controlled character.", (string[] args) =>
2626  {
2627  Character.Controlled?.AnimController.Flip();
2628  }, isCheat: false));
2629  commands.Add(new Command("mirror", "Mirror the currently controlled character.", (string[] args) =>
2630  {
2631  (Character.Controlled?.AnimController as FishAnimController)?.Mirror(lerp: false);
2632  }, isCheat: false));
2633  commands.Add(new Command("forcetimeout", "Immediately cause the client to time out if one is running.", (string[] args) =>
2634  {
2635  GameMain.Client?.ForceTimeOut();
2636  }, isCheat: false));
2637  commands.Add(new Command("bumpitem", "", (string[] args) =>
2638  {
2639  float vel = 10.0f;
2640  if (args.Length > 0)
2641  {
2642  float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out vel);
2643  }
2644  Character.Controlled?.FocusedItem?.body?.ApplyLinearImpulse(Rand.Vector(vel));
2645  }, isCheat: false));
2646 
2647 #endif
2648 
2649  commands.Add(new Command("dumptexts", "dumptexts [filepath]: Extracts all the texts from the given text xml and writes them into a file (using the same filename, but with the .txt extension). If the filepath is omitted, the EnglishVanilla.xml file is used.", (string[] args) =>
2650  {
2651  string filePath = args.Length > 0 ? args[0] : "Content/Texts/EnglishVanilla.xml";
2652  var doc = XMLExtensions.TryLoadXml(filePath);
2653  if (doc == null) { return; }
2654  List<string> lines = new List<string>();
2655  foreach (XElement element in doc.Root.Elements())
2656  {
2657  lines.Add(element.ElementInnerText());
2658  }
2659  File.WriteAllLines(Path.GetFileNameWithoutExtension(filePath) + ".txt", lines);
2660  },
2661  () =>
2662  {
2663  var files = TextManager.GetTextFiles().Select(f => f.CleanUpPath());
2664  return new string[][]
2665  {
2666  TextManager.GetTextFiles().Where(f => Path.GetExtension(f)==".xml").ToArray()
2667  };
2668  }));
2669 
2670  commands.Add(new Command("loadtexts", "loadtexts [sourcefile] [destinationfile]: Loads all lines of text from a given .txt file and inserts them sequientially into the elements of an xml file. If the file paths are omitted, EnglishVanilla.txt and EnglishVanilla.xml are used.", (string[] args) =>
2671  {
2672  string sourcePath = args.Length > 0 ? args[0] : "Content/Texts/EnglishVanilla.txt";
2673  string destinationPath = args.Length > 1 ? args[1] : "Content/Texts/EnglishVanilla.xml";
2674 
2675  string[] lines;
2676  try
2677  {
2678  lines = File.ReadAllLines(sourcePath);
2679  }
2680  catch (Exception e)
2681  {
2682  ThrowError("Reading the file \"" + sourcePath + "\" failed.", e);
2683  return;
2684  }
2685  var doc = XMLExtensions.TryLoadXml(destinationPath);
2686  if (doc == null) { return; }
2687  int i = 0;
2688  foreach (XElement element in doc.Root.Elements())
2689  {
2690  if (i >= lines.Length)
2691  {
2692  ThrowError("Error while loading texts to the xml file. The xml has more elements than the number of lines in the text file.");
2693  return;
2694  }
2695  element.Value = lines[i];
2696  i++;
2697  }
2698  doc.SaveSafe(destinationPath);
2699  },
2700  () =>
2701  {
2702  var files = TextManager.GetTextFiles().Select(f => f.CleanUpPath());
2703  return new string[][]
2704  {
2705  files.Where(f => Path.GetExtension(f)==".txt").ToArray(),
2706  files.Where(f => Path.GetExtension(f)==".xml").ToArray()
2707  };
2708  }));
2709 
2710  commands.Add(new Command("updatetextfile", "updatetextfile [sourcefile] [destinationfile]: Inserts all the xml elements that are only present in the source file into the destination file. Can be used to update outdated translation files more easily.", (string[] args) =>
2711  {
2712  if (args.Length < 2) return;
2713  string sourcePath = args[0];
2714  string destinationPath = args[1];
2715 
2716  var sourceDoc = XMLExtensions.TryLoadXml(sourcePath);
2717  var destinationDoc = XMLExtensions.TryLoadXml(destinationPath);
2718 
2719  if (sourceDoc == null || destinationDoc == null) { return; }
2720 
2721  XElement destinationElement = destinationDoc.Root.Elements().First();
2722  foreach (XElement element in sourceDoc.Root.Elements())
2723  {
2724  if (destinationDoc.Root.Element(element.Name) == null)
2725  {
2726  element.Value = "!!!!!!!!!!!!!" + element.Value;
2727  destinationElement.AddAfterSelf(element);
2728  }
2729  XNode nextNode = destinationElement.NextNode;
2730  while ((!(nextNode is XElement) || nextNode == element) && nextNode != null) nextNode = nextNode.NextNode;
2731  destinationElement = nextNode as XElement;
2732  }
2733  destinationDoc.SaveSafe(destinationPath);
2734  },
2735  () =>
2736  {
2737  var files = TextManager.GetTextFiles().Where(f => Path.GetExtension(f) == ".xml").Select(f => f.CleanUpPath()).ToArray();
2738  return new string[][]
2739  {
2740  files,
2741  files
2742  };
2743  }));
2744 
2745  commands.Add(new Command("dumpentitytexts", "dumpentitytexts [filepath]: gets the names and descriptions of all entity prefabs and writes them into a file along with xml tags that can be used in translation files. If the filepath is omitted, the file is written to Content/Texts/EntityTexts.txt", (string[] args) =>
2746  {
2747  string filePath = args.Length > 0 ? args[0] : "Content/Texts/EntityTexts.txt";
2748  List<string> lines = new List<string>();
2749  foreach (MapEntityPrefab me in MapEntityPrefab.List)
2750  {
2751  lines.Add($"<EntityName.{me.Identifier}>{me.Name}</EntityName.{me.Identifier}>");
2752  lines.Add($"<EntityDescription.{me.Identifier}>{me.Description}</EntityDescription.{me.Identifier}>");
2753  }
2754  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
2755  File.WriteAllLines(filePath, lines);
2756  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
2757  }));
2758 
2759  commands.Add(new Command("dumpeventtexts", "dumpeventtexts [filepath]: gets the texts from event files and and writes them into a file along with xml tags that can be used in translation files. If the filepath is omitted, the file is written to Content/Texts/EventTexts.txt", (string[] args) =>
2760  {
2761  string filePath = args.Length > 0 ? args[0] : "Content/Texts/EventTexts.txt";
2762  List<string> lines = new List<string>();
2763  HashSet<XDocument> docs = new HashSet<XDocument>();
2764  HashSet<string> textIds = new HashSet<string>();
2765 
2766  Dictionary<string, string> existingTexts = new Dictionary<string, string>();
2767 
2768  foreach (EventPrefab eventPrefab in EventSet.GetAllEventPrefabs())
2769  {
2770  if (eventPrefab is not TraitorEventPrefab) { continue; }
2771  if (eventPrefab.Identifier.IsEmpty)
2772  {
2773  continue;
2774  }
2775  docs.Add(eventPrefab.ConfigElement.Document);
2776  getTextsFromElement(eventPrefab.ConfigElement, lines, eventPrefab.Identifier.Value);
2777  }
2778  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
2779  File.WriteAllLines(filePath, lines);
2780  try
2781  {
2782  ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
2783  }
2784  catch (Exception e)
2785  {
2786  ThrowError($"Failed to open the file \"{filePath}\".", e);
2787  }
2788 
2789  System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings
2790  {
2791  Indent = true,
2792  NewLineOnAttributes = false
2793  };
2794  foreach (XDocument doc in docs)
2795  {
2796  using (var writer = XmlWriter.Create(new System.Uri(doc.BaseUri).LocalPath, settings))
2797  {
2798  doc.WriteTo(writer);
2799  writer.Flush();
2800  }
2801  }
2802  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
2803 
2804  void getTextsFromElement(XElement element, List<string> list, string parentName)
2805  {
2806  string text = element.GetAttributeString("text", null);
2807  string textAttribute = "text";
2808  XElement textElement = element;
2809  if (text == null)
2810  {
2811  var subTextElement = element?.Element("Text");
2812  if (subTextElement != null)
2813  {
2814  textAttribute = "tag";
2815  text = subTextElement?.GetAttributeString(textAttribute, null);
2816  textElement = subTextElement;
2817  }
2818  if (text == null)
2819  {
2820  AddWarning("Failed to find text from the element " + element.ToString());
2821  }
2822  }
2823 
2824  string textId = $"EventText.{parentName}";
2825  if (!string.IsNullOrEmpty(text) && !text.Contains("EventText.", StringComparison.OrdinalIgnoreCase))
2826  {
2827  if (existingTexts.TryGetValue(text, out string existingTextId))
2828  {
2829  textElement.SetAttributeValue(textAttribute, existingTextId);
2830  }
2831  else
2832  {
2833  textIds.Add(parentName);
2834  list.Add($"<{textId}>{text}</{textId}>");
2835  existingTexts.Add(text, textId);
2836  textElement.SetAttributeValue(textAttribute, textId);
2837  }
2838  }
2839 
2840  int conversationIndex = 1;
2841  int objectiveIndex = 1;
2842  foreach (var subElement in element.Elements())
2843  {
2844  string elementName = parentName;
2845  bool ignore = false;
2846  switch (subElement.Name.ToString().ToLowerInvariant())
2847  {
2848  case "conversationaction":
2849  while (textIds.Contains(elementName + ".c" + conversationIndex))
2850  {
2851  conversationIndex++;
2852  }
2853  elementName += ".c" + conversationIndex;
2854  break;
2855  case "eventlogaction":
2856  while (textIds.Contains(elementName + ".objective" + objectiveIndex))
2857  {
2858  objectiveIndex++;
2859  }
2860  elementName += ".objective" + objectiveIndex;
2861  break;
2862  case "option":
2863  while (textIds.Contains(elementName.Substring(0, elementName.Length - 3) + ".o" + conversationIndex))
2864  {
2865  conversationIndex++;
2866  }
2867  elementName = elementName.Substring(0, elementName.Length - 3) + ".o" + conversationIndex;
2868  break;
2869  case "text":
2870  ignore = true;
2871  break;
2872  }
2873  if (ignore) { continue; }
2874  getTextsFromElement(subElement, list, elementName);
2875  }
2876  }
2877  }));
2878 
2879  commands.Add(new Command("itemcomponentdocumentation", "", (string[] args) =>
2880  {
2881  Dictionary<string, string> typeNames = new Dictionary<string, string>
2882  {
2883  { "Single", "Float"},
2884  { "Int32", "Integer"},
2885  { "Boolean", "True/False"},
2886  { "String", "Text"},
2887  };
2888 
2889  var itemComponentTypes = typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent))).ToList();
2890  itemComponentTypes.Sort((i1, i2) => { return i1.Name.CompareTo(i2.Name); });
2891 
2892  itemComponentTypes.Insert(0, typeof(ItemComponent));
2893 
2894  string filePath = args.Length > 0 ? args[0] : "ItemComponentDocumentation.txt";
2895  List<string> lines = new List<string>();
2896  foreach (Type t in itemComponentTypes)
2897  {
2898 
2899  lines.Add($"[h1]{t.Name}[/h1]");
2900  lines.Add("");
2901 
2902  var properties = t.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly).ToList();//.Cast<System.ComponentModel.PropertyDescriptor>();
2903  Type baseType = t.BaseType;
2904  while (baseType != null && baseType != typeof(ItemComponent))
2905  {
2906  properties.AddRange(baseType.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.DeclaredOnly));
2907  baseType = baseType.BaseType;
2908  }
2909 
2910  if (!properties.Any(p => p.GetCustomAttributes(true).Any(a => a is Serialize)))
2911  {
2912  lines.Add("No editable properties.");
2913  lines.Add("");
2914  continue;
2915  }
2916 
2917  lines.Add("[table]");
2918  lines.Add(" [tr]");
2919 
2920  lines.Add(" [th]Name[/th]");
2921  lines.Add(" [th]Type[/th]");
2922  lines.Add(" [th]Default value[/th]");
2923  //lines.Add(" [th]Range[/th]");
2924  lines.Add(" [th]Description[/th]");
2925 
2926  lines.Add(" [/tr]");
2927 
2928 
2929 
2930  Dictionary<Identifier, SerializableProperty> dictionary = new Dictionary<Identifier, SerializableProperty>();
2931  foreach (var property in properties)
2932  {
2933  object[] attributes = property.GetCustomAttributes(true);
2934  Serialize serialize = attributes.FirstOrDefault(a => a is Serialize) as Serialize;
2935  if (serialize == null) { continue; }
2936 
2937  string propertyTypeName = property.PropertyType.Name;
2938  if (typeNames.ContainsKey(propertyTypeName))
2939  {
2940  propertyTypeName = typeNames[propertyTypeName];
2941  }
2942  else if (property.PropertyType.IsEnum)
2943  {
2944  List<string> valueNames = new List<string>();
2945  foreach (object enumValue in Enum.GetValues(property.PropertyType))
2946  {
2947  valueNames.Add(enumValue.ToString());
2948  }
2949  propertyTypeName = string.Join("/", valueNames);
2950  }
2951  string defaultValueString = serialize.DefaultValue?.ToString() ?? "";
2952  if (property.PropertyType == typeof(float))
2953  {
2954  defaultValueString = ((float)serialize.DefaultValue).ToString(CultureInfo.InvariantCulture);
2955  }
2956 
2957  lines.Add(" [tr]");
2958 
2959  lines.Add($" [td]{property.Name}[/td]");
2960  lines.Add($" [td]{propertyTypeName}[/td]");
2961  lines.Add($" [td]{defaultValueString}[/td]");
2962 
2963  Editable editable = attributes.FirstOrDefault(a => a is Editable) as Editable;
2964  string rangeText = "-";
2965  if (editable != null)
2966  {
2967  if (editable.MinValueFloat > float.MinValue || editable.MaxValueFloat < float.MaxValue)
2968  {
2969  rangeText = editable.MinValueFloat + "-" + editable.MaxValueFloat;
2970  }
2971  else if (editable.MinValueInt > int.MinValue || editable.MaxValueInt < int.MaxValue)
2972  {
2973  rangeText = editable.MinValueInt + "-" + editable.MaxValueInt;
2974  }
2975  }
2976  //lines.Add($" [td]{rangeText}[/td]");
2977 
2978  if (!string.IsNullOrEmpty(serialize.Description))
2979  {
2980  lines.Add($" [td]{serialize.Description}[/td]");
2981  }
2982 
2983  lines.Add(" [/tr]");
2984  }
2985  lines.Add("[/table]");
2986  lines.Add("");
2987  }
2988  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
2989  File.WriteAllLines(filePath, lines);
2990  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
2991  ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
2992  }));
2993 
2994 #if DEBUG
2995  commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) =>
2996  {
2997  if (args.Length != 1) { return; }
2998  TextManager.CheckForDuplicates(args[0].ToIdentifier().ToLanguageIdentifier());
2999  }));
3000 
3001  commands.Add(new Command("writetocsv|xmltocsv", "Writes the default language (English) to a .csv file.", (string[] args) =>
3002  {
3003  TextManager.WriteToCSV();
3004  NPCConversation.WriteToCSV();
3005  }));
3006 
3007  commands.Add(new Command("csvtoxml", "csvtoxml -> Converts .csv localization files Content/Texts/Texts.csv and Content/Texts/NPCConversations.csv to .xml for use in-game.", (string[] args) =>
3008  {
3009  ShowQuestionPrompt("Do you want to save the text files to the project folder (../../../BarotraumaShared/Content/Texts/)? If not, they are saved in the current working directory. Y/N",
3010  (option1) =>
3011  {
3012  ShowQuestionPrompt("Do you want to convert the NPC conversations as well? Y/N",
3013  (option2) =>
3014  {
3015  LocalizationCSVtoXML.ConvertMasterLocalizationKit(
3016  option1.ToLowerInvariant() == "y" ? "../../../BarotraumaShared/Content/Texts/" : "Content/Texts",
3017  option1.ToLowerInvariant() == "y" ? "../../../BarotraumaShared/Content/NPCConversations/" : "Content/NPCConversations",
3018  convertConversations: option2.ToLowerInvariant() == "y");
3019  });
3020  });
3021  }));
3022 
3023  commands.Add(new Command("printproperties", "Goes through the currently collected property list for missing localizations and writes them to a file.", (string[] args) =>
3024  {
3025  string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\propertylocalization.txt";
3026  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
3027  File.WriteAllLines(path, SerializableEntityEditor.MissingLocalizations);
3028  Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
3029  }));
3030 
3031  commands.Add(new Command("getproperties", "Goes through the MapEntity prefabs and checks their serializable properties for localization issues.", (string[] args) =>
3032  {
3033  if (Screen.Selected != GameMain.SubEditorScreen) return;
3034  foreach (MapEntityPrefab ep in MapEntityPrefab.List)
3035  {
3036  ep.DebugCreateInstance();
3037  }
3038 
3039  for (int i = 0; i < MapEntity.MapEntityList.Count; i++)
3040  {
3041  var entity = MapEntity.MapEntityList[i] as ISerializableEntity;
3042  if (entity != null)
3043  {
3044  List<(object obj, SerializableProperty property)> allProperties = new List<(object obj, SerializableProperty property)>();
3045 
3046  if (entity is Item item)
3047  {
3048  allProperties.AddRange(item.GetProperties<Editable>());
3049  allProperties.AddRange(item.GetProperties<InGameEditable>());
3050  }
3051  else
3052  {
3053  var properties = new List<SerializableProperty>();
3054  properties.AddRange(SerializableProperty.GetProperties<Editable>(entity));
3055  properties.AddRange(SerializableProperty.GetProperties<InGameEditable>(entity));
3056 
3057  for (int k = 0; k < properties.Count; k++)
3058  {
3059  allProperties.Add((entity, properties[k]));
3060  }
3061  }
3062 
3063  for (int j = 0; j < allProperties.Count; j++)
3064  {
3065  var property = allProperties[j].property;
3066  string propertyName = (allProperties[j].obj.GetType().Name + "." + property.PropertyInfo.Name).ToLowerInvariant();
3067  LocalizedString displayName = TextManager.Get($"sp.{propertyName}.name");
3068  if (displayName.IsNullOrEmpty())
3069  {
3070  displayName = property.Name.FormatCamelCaseWithSpaces();
3071 
3072  Editable editable = property.GetAttribute<Editable>();
3073  if (editable != null)
3074  {
3075  if (!SerializableEntityEditor.MissingLocalizations.Contains($"sp.{propertyName}.name|{displayName}"))
3076  {
3077  NewMessage("Missing Localization for property: " + propertyName);
3078  SerializableEntityEditor.MissingLocalizations.Add($"sp.{propertyName}.name|{displayName}");
3079  SerializableEntityEditor.MissingLocalizations.Add($"sp.{propertyName}.description|{property.GetAttribute<Serialize>().Description}");
3080  }
3081  }
3082  }
3083  }
3084  }
3085  }
3086  }));
3087 #endif
3088 
3089  commands.Add(new Command("reloadcorepackage", "", (string[] args) =>
3090  {
3091  if (args.Length < 1)
3092  {
3093  if (Screen.Selected == GameMain.GameScreen)
3094  {
3095  ThrowError("Reloading the core package while in GameScreen WILL break everything; to do it anyway, type 'reloadcorepackage force'");
3096  return;
3097  }
3098 
3099  if (Screen.Selected == GameMain.SubEditorScreen)
3100  {
3101  ThrowError("Reloading the core package while in sub editor WILL break everything; to do it anyway, type 'reloadcorepackage force'");
3102  return;
3103  }
3104  }
3105 
3106  if (GameMain.NetworkMember != null)
3107  {
3108  ThrowError("Cannot change content packages while playing online");
3109  return;
3110  }
3111 
3112  ContentPackageManager.EnabledPackages.ReloadCore();
3113  }));
3114 
3115  commands.Add(new Command("reloadpackage", "reloapackage [name]: reloads a content package.", (string[] args) =>
3116  {
3117  if (args.Length < 1)
3118  {
3119  ThrowError("Please specify the name of the package to reload.");
3120  return;
3121  }
3122 
3123  if (args.Length < 2)
3124  {
3125  if (Screen.Selected == GameMain.GameScreen)
3126  {
3127  ThrowError("Reloading the package while in GameScreen may break things; to do it anyway, type 'reloadpackage [name] force'");
3128  return;
3129  }
3130  if (Screen.Selected == GameMain.SubEditorScreen)
3131  {
3132  ThrowError("Reloading the core package while in sub editor may break things; to do it anyway, type 'reloadpackage [name] force'");
3133  return;
3134  }
3135  }
3136 
3137  if (GameMain.NetworkMember != null)
3138  {
3139  ThrowError("Cannot change content packages while playing online");
3140  return;
3141  }
3142 
3143  var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]);
3144  if (package == null)
3145  {
3146  ThrowError($"Could not find the package {args[0]}!");
3147  return;
3148  }
3149  ContentPackageManager.EnabledPackages.ReloadPackage(package);
3150  }, getValidArgs: () => new[]
3151  {
3152  ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
3153  }));
3154 
3155 #if WINDOWS
3156  commands.Add(new Command("startdedicatedserver", "", (string[] args) =>
3157  {
3158  Process.Start("DedicatedServer.exe");
3159  }));
3160 
3161  commands.Add(new Command("editserversettings", "", (string[] args) =>
3162  {
3163  if (Process.GetProcessesByName("DedicatedServer").Length > 0)
3164  {
3165  NewMessage("Can't be edited if DedicatedServer.exe is already running", Color.Red);
3166  }
3167  else
3168  {
3169  Process.Start("notepad.exe", "serversettings.xml");
3170  }
3171  }));
3172 #endif
3173 
3174 #warning TODO: reimplement?
3175  /*commands.Add(new Command("ingamemodswap", "", (string[] args) =>
3176  {
3177  ContentPackage.IngameModSwap = !ContentPackage.IngameModSwap;
3178  if (ContentPackage.IngameModSwap)
3179  {
3180  NewMessage("Enabled ingame mod swapping");
3181  }
3182  else
3183  {
3184  NewMessage("Disabled ingame mod swapping");
3185  }
3186  }));*/
3187 
3188  AssignOnClientExecute(
3189  "giveperm",
3190  (string[] args) =>
3191  {
3192  if (args.Length < 1) { return; }
3193 
3194  NewMessage("Valid permissions are:", Color.White);
3195  foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions)))
3196  {
3197  NewMessage(" - " + permission.ToString(), Color.White);
3198  }
3199  ShowQuestionPrompt("Permission to grant to client " + args[0] + "?", (perm) =>
3200  {
3201  GameMain.Client?.SendConsoleCommand("giveperm \"" + args[0] + "\" " + perm);
3202  }, args, 1);
3203  }
3204  );
3205 
3206  AssignOnClientExecute(
3207  "revokeperm",
3208  (string[] args) =>
3209  {
3210  if (args.Length < 1) { return; }
3211 
3212  if (args.Length < 2)
3213  {
3214  NewMessage("Valid permissions are:", Color.White);
3215  foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions)))
3216  {
3217  NewMessage(" - " + permission.ToString(), Color.White);
3218  }
3219  }
3220 
3221  ShowQuestionPrompt("Permission to revoke from client " + args[0] + "?", (perm) =>
3222  {
3223  GameMain.Client?.SendConsoleCommand("revokeperm \"" + args[0] + "\" " + perm);
3224  }, args, 1);
3225  }
3226  );
3227 
3228  AssignOnClientExecute(
3229  "giverank",
3230  (string[] args) =>
3231  {
3232  if (args.Length < 1) return;
3233 
3234  NewMessage("Valid ranks are:", Color.White);
3235  foreach (PermissionPreset permissionPreset in PermissionPreset.List)
3236  {
3237  NewMessage(" - " + permissionPreset.DisplayName, Color.White);
3238  }
3239  ShowQuestionPrompt("Rank to grant to client " + args[0] + "?", (rank) =>
3240  {
3241  GameMain.Client?.SendConsoleCommand("giverank \"" + args[0] + "\" " + rank);
3242  }, args, 1);
3243  }
3244  );
3245 
3246  AssignOnClientExecute(
3247  "givecommandperm",
3248  (string[] args) =>
3249  {
3250  if (args.Length < 1) return;
3251 
3252  ShowQuestionPrompt("Console command permissions to grant to client " + args[0] + "? You may enter multiple commands separated with a space or use \"all\" to give the permission to use all console commands.", (commandNames) =>
3253  {
3254  GameMain.Client?.SendConsoleCommand("givecommandperm \"" + args[0] + "\" " + commandNames);
3255  }, args, 1);
3256  }
3257  );
3258 
3259  AssignOnClientExecute(
3260  "revokecommandperm",
3261  (string[] args) =>
3262  {
3263  if (args.Length < 1) return;
3264 
3265  ShowQuestionPrompt("Console command permissions to revoke from client " + args[0] + "? You may enter multiple commands separated with a space or use \"all\" to revoke the permission to use any console commands.", (commandNames) =>
3266  {
3267  GameMain.Client?.SendConsoleCommand("revokecommandperm \"" + args[0] + "\" " + commandNames);
3268  }, args, 1);
3269  }
3270  );
3271 
3272  AssignOnClientExecute(
3273  "showperm",
3274  (string[] args) =>
3275  {
3276  if (args.Length < 1) return;
3277 
3278  GameMain.Client.SendConsoleCommand("showperm " + args[0]);
3279  }
3280  );
3281 
3282  AssignOnClientExecute(
3283  "banaddress|banip",
3284  (string[] args) =>
3285  {
3286  if (GameMain.Client == null || args.Length == 0) return;
3287  ShowQuestionPrompt("Reason for banning the endpoint \"" + args[0] + "\"? (Enter c to cancel)", (reason) =>
3288  {
3289  if (reason == "c" || reason == "C") { return; }
3290  ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (Enter c to cancel)", (duration) =>
3291  {
3292  if (duration == "c" || duration == "C") { return; }
3293  TimeSpan? banDuration = null;
3294  if (!string.IsNullOrWhiteSpace(duration))
3295  {
3296  if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration))
3297  {
3298  ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\".");
3299  return;
3300  }
3301  banDuration = parsedBanDuration;
3302  }
3303 
3304  GameMain.Client?.SendConsoleCommand(
3305  "banaddress " +
3306  args[0] + " " +
3307  (banDuration.HasValue ? banDuration.Value.TotalSeconds.ToString() : "0") + " " +
3308  reason);
3309  });
3310  });
3311  }
3312  );
3313 
3314  commands.Add(new Command("unban", "unban [name]: Unban a specific client.", (string[] args) =>
3315  {
3316  if (GameMain.Client == null || args.Length == 0) return;
3317  string clientName = string.Join(" ", args);
3318  GameMain.Client.UnbanPlayer(clientName);
3319  }));
3320 
3321  commands.Add(new Command("unbanaddress", "unbanaddress [endpoint]: Unban a specific endpoint.", (string[] args) =>
3322  {
3323  if (GameMain.Client == null || args.Length == 0) return;
3324  if (Endpoint.Parse(args[0]).TryUnwrap(out var endpoint))
3325  {
3326  GameMain.Client.UnbanPlayer(endpoint);
3327  }
3328  }));
3329 
3330  AssignOnClientExecute(
3331  "campaigndestination|setcampaigndestination",
3332  (string[] args) =>
3333  {
3334  var campaign = GameMain.GameSession?.GameMode as CampaignMode;
3335  if (campaign == null)
3336  {
3337  ThrowError("No campaign active!");
3338  return;
3339  }
3340 
3341  if (args.Length == 0)
3342  {
3343  int i = 0;
3344  foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections)
3345  {
3346  NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White);
3347  i++;
3348  }
3349  ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) =>
3350  {
3351  int destinationIndex = -1;
3352  if (!int.TryParse(selectedDestination, out destinationIndex)) return;
3353  if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count)
3354  {
3355  NewMessage("Index out of bounds!", Color.Red);
3356  return;
3357  }
3358  GameMain.Client?.SendConsoleCommand("campaigndestination " + destinationIndex);
3359  });
3360  }
3361  else
3362  {
3363  int destinationIndex = -1;
3364  if (!int.TryParse(args[0], out destinationIndex)) return;
3365  if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count)
3366  {
3367  NewMessage("Index out of bounds!", Color.Red);
3368  return;
3369  }
3370  GameMain.Client.SendConsoleCommand("campaigndestination " + destinationIndex);
3371  }
3372  }
3373  );
3374 
3375 #if DEBUG
3376  commands.Add(new Command("setcurrentlocationtype", "setcurrentlocationtype [location type]: Change the type of the current location.", (string[] args) =>
3377  {
3378  var character = Character.Controlled;
3379  if (GameMain.GameSession?.Campaign == null)
3380  {
3381  ThrowError("Campaign not active!");
3382  return;
3383  }
3384  if (args.Length == 0)
3385  {
3386  ThrowError("Please give the location type after the command.");
3387  return;
3388  }
3389  var locationType = LocationType.Prefabs.Find(lt => lt.Identifier == args[0]);
3390  if (locationType == null)
3391  {
3392  ThrowError($"Could not find the location type \"{args[0]}\".");
3393  return;
3394  }
3395  GameMain.GameSession.Campaign.Map.CurrentLocation.ChangeType(GameMain.GameSession.Campaign, locationType);
3396  },
3397  () =>
3398  {
3399  return new string[][]
3400  {
3401  LocationType.Prefabs.Select(lt => lt.Identifier.Value).ToArray()
3402  };
3403  }));
3404 
3405  commands.Add(new Command("sendrawpacket", "sendrawpacket [data]: Send a string of hex values as raw binary data to the server", (string[] args) =>
3406  {
3407  if (GameMain.NetworkMember is null)
3408  {
3409  ThrowError("Not connected to a server");
3410  return;
3411  }
3412 
3413  if (args.Length == 0)
3414  {
3415  ThrowError("No data provided");
3416  return;
3417  }
3418 
3419  string dataString = string.Join(" ", args);
3420 
3421  try
3422  {
3423  byte[] bytes = ToolBox.HexStringToBytes(dataString);
3424  IWriteMessage msg = new WriteOnlyMessage();
3425  foreach (byte b in bytes) { msg.WriteByte(b); }
3426  GameMain.Client?.ClientPeer?.DebugSendRawMessage(msg);
3427  NewMessage($"Sent {bytes.Length} byte(s)", Color.Green);
3428  }
3429  catch (Exception e)
3430  {
3431  ThrowError("Failed to parse the data", e);
3432  }
3433  }));
3434 #endif
3435 
3436  commands.Add(new Command("limbscale", "Define the limbscale for the controlled character. Provide id or name if you want to target another character. Note: the changes are not saved!", (string[] args) =>
3437  {
3438  var character = Character.Controlled;
3439  if (character == null)
3440  {
3441  ThrowError("Not controlling any character!");
3442  return;
3443  }
3444  if (args.Length == 0)
3445  {
3446  ThrowError("Please give the value after the command.");
3447  return;
3448  }
3449  if (!float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out float value))
3450  {
3451  ThrowError("Failed to parse float value from the arguments");
3452  return;
3453  }
3454  RagdollParams ragdollParams = character.AnimController.RagdollParams;
3455  ragdollParams.LimbScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE);
3456  var pos = character.WorldPosition;
3457  character.AnimController.Recreate();
3458  character.TeleportTo(pos);
3459  }, isCheat: true));
3460 
3461  commands.Add(new Command("jointscale", "Define the jointscale for the controlled character. Provide id or name if you want to target another character. Note: the changes are not saved!", (string[] args) =>
3462  {
3463  var character = Character.Controlled;
3464  if (character == null)
3465  {
3466  ThrowError("Not controlling any character!");
3467  return;
3468  }
3469  if (args.Length == 0)
3470  {
3471  ThrowError("Please give the value after the command.");
3472  return;
3473  }
3474  if (!float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out float value))
3475  {
3476  ThrowError("Failed to parse float value from the arguments");
3477  return;
3478  }
3479  RagdollParams ragdollParams = character.AnimController.RagdollParams;
3480  ragdollParams.JointScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE);
3481  var pos = character.WorldPosition;
3482  character.AnimController.Recreate();
3483  character.TeleportTo(pos);
3484  }, isCheat: true));
3485 
3486  commands.Add(new Command("ragdollscale", "Rescale the ragdoll of the controlled character. Provide id or name if you want to target another character. Note: the changes are not saved!", (string[] args) =>
3487  {
3488  var character = Character.Controlled;
3489  if (character == null)
3490  {
3491  ThrowError("Not controlling any character!");
3492  return;
3493  }
3494  if (args.Length == 0)
3495  {
3496  ThrowError("Please give the value after the command.");
3497  return;
3498  }
3499  if (!float.TryParse(args[0], NumberStyles.Number, CultureInfo.InvariantCulture, out float value))
3500  {
3501  ThrowError("Failed to parse float value from the arguments");
3502  return;
3503  }
3504  RagdollParams ragdollParams = character.AnimController.RagdollParams;
3505  ragdollParams.LimbScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE);
3506  ragdollParams.JointScale = MathHelper.Clamp(value, RagdollParams.MIN_SCALE, RagdollParams.MAX_SCALE);
3507  var pos = character.WorldPosition;
3508  character.AnimController.Recreate();
3509  character.TeleportTo(pos);
3510  }, isCheat: true));
3511 
3512  commands.Add(new Command("recreateragdoll", "Recreate the ragdoll of the controlled character. Provide id or name if you want to target another character.", (string[] args) =>
3513  {
3514  var character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, true);
3515  if (character == null)
3516  {
3517  ThrowError("Not controlling any character!");
3518  return;
3519  }
3520  var pos = character.WorldPosition;
3521  character.AnimController.Recreate();
3522  character.TeleportTo(pos);
3523  }, isCheat: true));
3524 
3525  commands.Add(new Command("resetragdoll", "Reset the ragdoll of the controlled character. Provide id or name if you want to target another character.", (string[] args) =>
3526  {
3527  var character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, true);
3528  if (character == null)
3529  {
3530  ThrowError("Not controlling any character!");
3531  return;
3532  }
3533  character.AnimController.ResetRagdoll(forceReload: true);
3534  }, isCheat: true));
3535 
3536  commands.Add(new Command("loadanimation", "Loads an animation variation by name for the controlled character. The animation file has to be in the correct animations folder. Note: the changes are not saved!", (string[] args) =>
3537  {
3538  var character = Character.Controlled;
3539  if (character == null)
3540  {
3541  ThrowError("Not controlling any character!");
3542  return;
3543  }
3544  if (args.Length < 2)
3545  {
3546  ThrowError("Insufficient parameters: Have to pass the type of animation (Walk, Run, SwimSlow, SwimFast, or Crouch) and the filename!");
3547  return;
3548  }
3549  string type = args[0];
3550  if (!Enum.TryParse(type, ignoreCase: true, out AnimationType animationType))
3551  {
3552  ThrowError($"Failed to parse animation type from {type}. Supported types are Walk, Run, SwimSlow, SwimFast, and Crouch!");
3553  return;
3554  }
3555  string fileName = args[1];
3556  character.AnimController.TryLoadAnimation(animationType, Path.GetFileNameWithoutExtension(fileName), out _, throwErrors: true);
3557  }, isCheat: true));
3558 
3559  commands.Add(new Command("reloadwearables", "Reloads the sprites of all limbs and wearable sprites (clothing) of the controlled character. Provide id or name if you want to target another character.", args =>
3560  {
3561  var character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, true);
3562  if (character == null)
3563  {
3564  ThrowError("Not controlling any character or no matching character found with the provided arguments.");
3565  return;
3566  }
3567  ReloadWearables(character);
3568  }, isCheat: true));
3569 
3570  commands.Add(new Command("loadwearable", "Force select certain variant for the selected character.", args =>
3571  {
3572  var character = Character.Controlled;
3573  if (character == null)
3574  {
3575  ThrowError("Not controlling any character.");
3576  return;
3577  }
3578  if (args.Length == 0)
3579  {
3580  ThrowError("No arguments provided! Give an index number for the variant starting from 1.");
3581  return;
3582  }
3583  if (int.TryParse(args[0], out int variant))
3584  {
3585  ReloadWearables(character, variant);
3586  }
3587 
3588  }, isCheat: true));
3589 
3590  commands.Add(new Command("reloadsprite|reloadsprites", "Reloads the sprites of the selected item(s)/structure(s) (hovering over or selecting in the subeditor) or the controlled character. Can also reload sprites by entity id or by the name attribute (sprite element). Example 1: reloadsprite id itemid. Example 2: reloadsprite name \"Sprite name\"", args =>
3591  {
3592  if (Screen.Selected is SpriteEditorScreen)
3593  {
3594  return;
3595  }
3596  else if (args.Length > 1)
3597  {
3598  TryDoActionOnSprite(args[0], args[1], s =>
3599  {
3600  s.ReloadXML();
3601  s.ReloadTexture();
3602  });
3603  }
3604  else if (Screen.Selected is SubEditorScreen)
3605  {
3606  if (!MapEntity.SelectedAny)
3607  {
3608  ThrowError("You have to select item(s)/structure(s) first!");
3609  }
3610  MapEntity.SelectedList.ForEach(e =>
3611  {
3612  if (e.Sprite != null)
3613  {
3614  e.Sprite.ReloadXML();
3615  e.Sprite.ReloadTexture();
3616  }
3617  });
3618  }
3619  else
3620  {
3621  var character = Character.Controlled;
3622  if (character == null)
3623  {
3624  ThrowError("Please provide the mode (name or id) and the value so that I can find the sprite for you!");
3625  return;
3626  }
3627  var item = character.FocusedItem;
3628  if (item != null)
3629  {
3630  item.Sprite.ReloadXML();
3631  item.Sprite.ReloadTexture();
3632  }
3633  else
3634  {
3635  ReloadWearables(character);
3636  }
3637  }
3638  }, isCheat: true));
3639 
3640  commands.Add(new Command("flipx", "flipx: mirror the main submarine horizontally", (string[] args) =>
3641  {
3642  if (GameMain.NetworkMember != null)
3643  {
3644  ThrowError("Cannot use the flipx command while playing online.");
3645  return;
3646  }
3647  if (Submarine.MainSub.SubBody != null) { Submarine.MainSub?.FlipX(); }
3648  }, isCheat: true));
3649 
3650  commands.Add(new Command("head", "Load the head sprite and the wearables (hair etc). Required argument: head id. Optional arguments: hair index, beard index, moustache index, face attachment index.", args =>
3651  {
3652  var character = Character.Controlled;
3653  if (character == null)
3654  {
3655  ThrowError("Not controlling any character!");
3656  return;
3657  }
3658  if (args.Length == 0)
3659  {
3660  ThrowError("No head id provided!");
3661  return;
3662  }
3663  if (int.TryParse(args[0], out int id))
3664  {
3665  int hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex;
3666  hairIndex = beardIndex = moustacheIndex = faceAttachmentIndex = -1;
3667  if (args.Length > 1)
3668  {
3669  int.TryParse(args[1], out hairIndex);
3670  }
3671  if (args.Length > 2)
3672  {
3673  int.TryParse(args[2], out beardIndex);
3674  }
3675  if (args.Length > 3)
3676  {
3677  int.TryParse(args[3], out moustacheIndex);
3678  }
3679  if (args.Length > 4)
3680  {
3681  int.TryParse(args[4], out faceAttachmentIndex);
3682  }
3683  character.ReloadHead(id, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
3684  foreach (var limb in character.AnimController.Limbs)
3685  {
3686  if (limb.type != LimbType.Head)
3687  {
3688  limb.RecreateSprites();
3689  }
3690  }
3691  }
3692  }, isCheat: true));
3693 
3694  commands.Add(new Command("spawnsub", "spawnsub [subname] [is thalamus]: Spawn a submarine at the position of the cursor", (string[] args) =>
3695  {
3696  if (GameMain.NetworkMember != null)
3697  {
3698  ThrowError("Cannot spawn additional submarines during a multiplayer session.");
3699  return;
3700  }
3701  if (args.Length == 0)
3702  {
3703  ThrowError("Please enter the name of the submarine.");
3704  return;
3705  }
3706  try
3707  {
3708  var subInfo = SubmarineInfo.SavedSubmarines.FirstOrDefault(s =>
3709  //accept both the localized and the non-localized name of the sub
3710  s.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase) ||
3711  s.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase));
3712  if (subInfo == null)
3713  {
3714  ThrowError($"Could not find a submarine with the name \"{args[0]}\".");
3715  }
3716  else
3717  {
3718  Submarine spawnedSub = Submarine.Load(subInfo, false);
3719  spawnedSub.SetPosition(GameMain.GameScreen.Cam.ScreenToWorld(PlayerInput.MousePosition));
3720  if (subInfo.Type == SubmarineType.Wreck)
3721  {
3722  spawnedSub.MakeWreck();
3723  if (args.Length > 1 && bool.TryParse(args[1], out bool isThalamus))
3724  {
3725  if (isThalamus)
3726  {
3727  spawnedSub.CreateWreckAI();
3728  }
3729  else
3730  {
3731  spawnedSub.DisableWreckAI();
3732  }
3733  }
3734  else
3735  {
3736  spawnedSub.DisableWreckAI();
3737  }
3738  }
3739  }
3740  }
3741  catch (Exception e)
3742  {
3743  string errorMsg = "Failed to spawn a submarine. Arguments: \"" + string.Join(" ", args) + "\".";
3744  ThrowError(errorMsg, e);
3745  GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnSubmarine:Error", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace());
3746  }
3747  },
3748  () =>
3749  {
3750  return new string[][]
3751  {
3752  SubmarineInfo.SavedSubmarines.Select(s => s.DisplayName.Value).ToArray()
3753  };
3754  },
3755  isCheat: true));
3756 
3757  commands.Add(new Command("pause", "Toggles the pause state when playing offline", (string[] args) =>
3758  {
3759  if (GameMain.NetworkMember == null)
3760  {
3761  Paused = !Paused;
3762  DebugConsole.NewMessage("Game paused: " + Paused);
3763  }
3764  else
3765  {
3766  DebugConsole.NewMessage("Cannot pause when a multiplayer session is active.");
3767  }
3768  }));
3769 
3770  AssignOnClientExecute("showseed|showlevelseed", (string[] args) =>
3771  {
3772  if (Level.Loaded == null)
3773  {
3774  ThrowError("No level loaded.");
3775  }
3776  else
3777  {
3778  NewMessage("Level seed: " + Level.Loaded.Seed);
3779  NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier);
3780  NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()));
3781  NewMessage("Mirrored: " + Level.Loaded.Mirrored);
3782  NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y);
3783  NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown"));
3784  }
3785  });
3786 
3787  commands.Add(new Command("cl_lua", $"cl_lua: Runs a string on the client.", (string[] args) =>
3788  {
3789  if (GameMain.Client != null && !GameMain.Client.HasPermission(ClientPermissions.ConsoleCommands))
3790  {
3791  ThrowError("Command not permitted.");
3792  return;
3793  }
3794 
3795  if (GameMain.LuaCs.Lua == null)
3796  {
3797  ThrowError("LuaCs not initialized, use the console command cl_reloadluacs to force initialization.");
3798  return;
3799  }
3800 
3801  try
3802  {
3803  GameMain.LuaCs.Lua.DoString(string.Join(" ", args));
3804  }
3805  catch(Exception ex)
3806  {
3807  LuaCsLogger.HandleException(ex, LuaCsMessageOrigin.LuaMod);
3808  }
3809  }));
3810 
3811  commands.Add(new Command("cl_reloadlua|cl_reloadcs|cl_reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) =>
3812  {
3813  GameMain.LuaCs.Initialize();
3814  }));
3815 
3816  commands.Add(new Command("cl_toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) =>
3817  {
3818  int port = 41912;
3819 
3820  if (args.Length > 0)
3821  {
3822  int.TryParse(args[0], out port);
3823  }
3824 
3825  GameMain.LuaCs.ToggleDebugger(port);
3826  }));
3827  }
3828 
3829  private static void ReloadWearables(Character character, int variant = 0)
3830  {
3831  foreach (var limb in character.AnimController.Limbs)
3832  {
3833  limb.Sprite?.ReloadTexture();
3834  limb.DamagedSprite?.ReloadTexture();
3835  limb.DeformSprite?.Sprite.ReloadTexture();
3836  foreach (var wearable in limb.WearingItems)
3837  {
3838  if (variant > 0 && wearable.Variant > 0)
3839  {
3840  wearable.Variant = variant;
3841  }
3842  wearable.ParsePath(true);
3843  wearable.Sprite.ReloadXML();
3844  wearable.Sprite.ReloadTexture();
3845  }
3846  foreach (var wearable in limb.OtherWearables)
3847  {
3848  wearable.ParsePath(true);
3849  wearable.Sprite.ReloadXML();
3850  wearable.Sprite.ReloadTexture();
3851  }
3852  if (limb.HuskSprite != null)
3853  {
3854  limb.HuskSprite.Sprite.ReloadXML();
3855  limb.HuskSprite.Sprite.ReloadTexture();
3856  }
3857  if (limb.HerpesSprite != null)
3858  {
3859  limb.HerpesSprite.Sprite.ReloadXML();
3860  limb.HerpesSprite.Sprite.ReloadTexture();
3861  }
3862  }
3863  }
3864 
3865  private static bool TryDoActionOnSprite(string firstArg, string secondArg, Action<Sprite> action)
3866  {
3867  switch (firstArg)
3868  {
3869  case "name":
3870  var sprites = Sprite.LoadedSprites.Where(s => s.Name != null && s.Name.Equals(secondArg, StringComparison.OrdinalIgnoreCase));
3871  if (sprites.Any())
3872  {
3873  foreach (var s in sprites)
3874  {
3875  action(s);
3876  }
3877  return true;
3878  }
3879  else
3880  {
3881  ThrowError("Cannot find any matching sprites by the name: " + secondArg);
3882  return false;
3883  }
3884  case "identifier":
3885  case "id":
3886  sprites = Sprite.LoadedSprites.Where(s => s.EntityIdentifier != null && s.EntityIdentifier == secondArg);
3887  if (sprites.Any())
3888  {
3889  foreach (var s in sprites)
3890  {
3891  action(s);
3892  }
3893  return true;
3894  }
3895  else
3896  {
3897  ThrowError("Cannot find any matching sprites by the id: " + secondArg);
3898  return false;
3899  }
3900  default:
3901  ThrowError("The first argument must be either 'name' or 'id'");
3902  return false;
3903  }
3904  }
3905 
3906 
3907  private enum AdjustItemTypes
3908  {
3909  NoAdjustment,
3910  Additive,
3911  Multiplicative
3912  }
3913 
3914  private static void PrintItemCosts(Dictionary<ItemPrefab, int> newPrices, ItemPrefab materialPrefab, List<FabricationRecipe> fabricableItems, int newPrice, bool adjustDown, string depth = "", AdjustItemTypes adjustItemType = AdjustItemTypes.NoAdjustment)
3915  {
3916  if (newPrice < 1)
3917  {
3918  NewMessage(depth + materialPrefab.Name + " cannot be adjusted to this price, because it would become less than 1.");
3919  return;
3920  }
3921 
3922  depth += " ";
3923  newPrices.TryAdd(materialPrefab, newPrice);
3924 
3925  int componentCost = 0;
3926  int newComponentCost = 0;
3927 
3928  var fabricationRecipe = fabricableItems.Find(f => f.TargetItem == materialPrefab);
3929 
3930  if (fabricationRecipe != null)
3931  {
3932  foreach (RequiredItem requiredItem in fabricationRecipe.RequiredItems)
3933  {
3934  foreach (ItemPrefab itemPrefab in requiredItem.ItemPrefabs)
3935  {
3936  GetAdjustedPrice(itemPrefab, ref componentCost, ref newComponentCost, newPrices);
3937  }
3938  }
3939  }
3940  string componentCostMultiplier = "";
3941  if (componentCost > 0)
3942  {
3943  componentCostMultiplier = $" (Relative difference to component cost {GetComponentCostDifference(materialPrefab.DefaultPrice.Price, componentCost)} => {GetComponentCostDifference(newPrice, newComponentCost)}, or flat profit {(int)(materialPrefab.DefaultPrice.Price - (int)componentCost)} => {newPrice - newComponentCost})";
3944  }
3945  string priceAdjustment = "";
3946  if (newPrice != materialPrefab.DefaultPrice.Price)
3947  {
3948  priceAdjustment = ", Suggested price adjustment is " + materialPrefab.DefaultPrice.Price + " => " + newPrice;
3949  }
3950  NewMessage(depth + materialPrefab.Name + "(" + materialPrefab.DefaultPrice.Price + ") " + priceAdjustment + componentCostMultiplier);
3951 
3952  if (adjustDown)
3953  {
3954  if (componentCost > 0)
3955  {
3956  double newPriceMult = (double)newPrice / (double)(materialPrefab.DefaultPrice.Price);
3957  int newPriceDiff = componentCost + newPrice - materialPrefab.DefaultPrice.Price;
3958 
3959  switch (adjustItemType)
3960  {
3961  case AdjustItemTypes.Additive:
3962  NewMessage(depth + materialPrefab.Name + "'s components should be adjusted " + componentCost + " => " + newPriceDiff);
3963  break;
3964  case AdjustItemTypes.Multiplicative:
3965  NewMessage(depth + materialPrefab.Name + "'s components should be adjusted " + componentCost + " => " + Math.Round(newPriceMult * componentCost));
3966  break;
3967  }
3968 
3969  if (fabricationRecipe != null)
3970  {
3971  foreach (RequiredItem requiredItem in fabricationRecipe.RequiredItems)
3972  {
3973  foreach (ItemPrefab itemPrefab in requiredItem.ItemPrefabs)
3974  {
3975  if (itemPrefab.DefaultPrice != null)
3976  {
3977  switch (adjustItemType)
3978  {
3979  case AdjustItemTypes.NoAdjustment:
3980  PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price, adjustDown, depth, adjustItemType);
3981  break;
3982  case AdjustItemTypes.Additive:
3983  PrintItemCosts(newPrices, itemPrefab, fabricableItems, itemPrefab.DefaultPrice.Price + (int)((newPrice - materialPrefab.DefaultPrice.Price) / (double)fabricationRecipe.RequiredItems.Length), adjustDown, depth, adjustItemType);
3984  break;
3985  case AdjustItemTypes.Multiplicative:
3986  PrintItemCosts(newPrices, itemPrefab, fabricableItems, (int)(itemPrefab.DefaultPrice.Price * newPriceMult), adjustDown, depth, adjustItemType);
3987  break;
3988  }
3989  }
3990  }
3991  }
3992  }
3993  }
3994  }
3995  else
3996  {
3997  var fabricationRecipes = fabricableItems.Where(f => f.RequiredItems.Any(x => x.ItemPrefabs.Contains(materialPrefab)));
3998 
3999  foreach (FabricationRecipe fabricationRecipeParent in fabricationRecipes)
4000  {
4001  if (fabricationRecipeParent.TargetItem.DefaultPrice != null)
4002  {
4003  int targetComponentCost = 0;
4004  int newTargetComponentCost = 0;
4005 
4006  foreach (RequiredItem requiredItem in fabricationRecipeParent.RequiredItems)
4007  {
4008  foreach (ItemPrefab itemPrefab in requiredItem.ItemPrefabs)
4009  {
4010  GetAdjustedPrice(itemPrefab, ref targetComponentCost, ref newTargetComponentCost, newPrices);
4011  }
4012  }
4013  switch (adjustItemType)
4014  {
4015  case AdjustItemTypes.NoAdjustment:
4016  PrintItemCosts(newPrices, fabricationRecipeParent.TargetItem, fabricableItems, fabricationRecipeParent.TargetItem.DefaultPrice.Price, adjustDown, depth, adjustItemType);
4017  break;
4018  case AdjustItemTypes.Additive:
4019  PrintItemCosts(newPrices, fabricationRecipeParent.TargetItem, fabricableItems, fabricationRecipeParent.TargetItem.DefaultPrice.Price + newPrice - materialPrefab.DefaultPrice.Price, adjustDown, depth, adjustItemType);
4020  break;
4021  case AdjustItemTypes.Multiplicative:
4022  double maintainedMultiplier = GetComponentCostDifference(fabricationRecipeParent.TargetItem.DefaultPrice.Price, targetComponentCost);
4023  PrintItemCosts(newPrices, fabricationRecipeParent.TargetItem, fabricableItems, (int)(newTargetComponentCost * maintainedMultiplier), adjustDown, depth, adjustItemType);
4024  break;
4025  }
4026  }
4027  }
4028  }
4029  }
4030 
4031  private static double GetComponentCostDifference(int itemCost, int componentCost)
4032  {
4033  return Math.Round((double)(itemCost / (double)componentCost), 2);
4034  }
4035 
4036  private static void GetAdjustedPrice(ItemPrefab itemPrefab, ref int componentCost, ref int newComponentCost, Dictionary<ItemPrefab, int> newPrices)
4037  {
4038  if (newPrices.TryGetValue(itemPrefab, out int newPrice))
4039  {
4040  newComponentCost += newPrice;
4041  }
4042  else if (itemPrefab.DefaultPrice != null)
4043  {
4044  newComponentCost += itemPrefab.DefaultPrice.Price;
4045  }
4046  if (itemPrefab.DefaultPrice != null)
4047  {
4048  componentCost += itemPrefab.DefaultPrice.Price;
4049  }
4050  }
4051  }
4052 }
void Set(KeyOrMouse key, string command)
IReadOnlyDictionary< KeyOrMouse, string > Bindings
readonly bool IsCheat
Using a command that's considered a cheat disables achievements
Action< string[]> OnClientExecute
Executed when a client uses the command. If not set, the command is relayed to the server as-is.
readonly ImmutableArray< RequiredItem > RequiredItems
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
static XmlWriter Create(string path, System.Xml.XmlWriterSettings settings)
Definition: SafeIO.cs:163
void Drop(Character dropper, bool createNetworkEvent=true, bool setTransform=true)
The base class for components holding the different functionalities of the item
static Option< Endpoint > Parse(string str)
bool HasPermission(ClientPermissions permission)
Definition: GameClient.cs:2620
bool HasConsoleCommandPermission(Identifier commandName)
Definition: GameClient.cs:2625
static readonly List< PermissionPreset > List
float MinValueFloat
Definition: Editable.cs:12
int MinValueInt
Definition: Editable.cs:11
static void SetErrorReasonCallback(ErrorReasonCallback callback)
Definition: Alc.cs:78
GUISoundType
Definition: GUI.cs:21
Definition: Al.cs:36