Client LuaCsForBarotrauma
BarotraumaShared/SharedSource/DebugConsole.cs
4 using Microsoft.Xna.Framework;
5 using System;
6 using System.Collections.Concurrent;
7 using System.Collections.Generic;
8 using System.Collections.Immutable;
9 using System.ComponentModel;
10 using System.Globalization;
11 using Barotrauma.IO;
12 using System.Linq;
13 using System.Threading.Tasks;
15 using System.Text;
16 
17 
18 namespace Barotrauma
19 {
20  readonly struct ColoredText
21  {
22  public readonly string Text;
23  public readonly Color Color;
24  public readonly bool IsCommand;
25  public readonly bool IsError;
26 
27  public readonly string Time;
28 
29  public ColoredText(string text, Color color, bool isCommand, bool isError)
30  {
31  this.Text = text;
32  this.Color = color;
33  this.IsCommand = isCommand;
34  this.IsError = isError;
35 
36  Time = DateTime.Now.ToString(CultureInfo.InvariantCulture);
37  }
38  }
39 
40  static partial class DebugConsole
41  {
42 
43  public partial class Command
44  {
45  public readonly ImmutableArray<Identifier> Names;
46  public readonly string Help;
47 
48  public Action<string[]> OnExecute;
49 
50  public Func<string[][]> GetValidArgs;
51 
55  public readonly bool IsCheat;
56 
60  public Command(string name, string help, Action<string[]> onExecute, Func<string[][]> getValidArgs = null, bool isCheat = false)
61  {
62  Names = name.Split('|').ToIdentifiers().ToImmutableArray();
63  this.Help = help;
64 
65  this.OnExecute = onExecute;
66 
67  this.GetValidArgs = getValidArgs;
68  this.IsCheat = isCheat;
69  }
70 
71  public void Execute(string[] args)
72  {
73  if (OnExecute == null) { return; }
74 
75  bool allowCheats = false;
76 #if CLIENT
77  allowCheats = GameMain.NetworkMember == null && (GameMain.GameSession?.GameMode is TestGameMode || Screen.Selected is { IsEditor: true });
78 #endif
79  if (!allowCheats && !CheatsEnabled && IsCheat)
80  {
81  NewMessage(
82  $"You need to enable cheats using the command \"enablecheats\" before you can use the command \"{Names.First()}\".", Color.Red);
83  NewMessage("Enabling cheats will disable Steam achievements during this play session.", Color.Red);
84  return;
85  }
86 
87  OnExecute(args);
88  }
89 
90  public override int GetHashCode()
91  {
92  return Names.First().GetHashCode();
93  }
94  }
95 
96  private static readonly ConcurrentQueue<ColoredText> queuedMessages
97  = new ConcurrentQueue<ColoredText>();
98 
99  public static readonly NamedEvent<ColoredText> MessageHandler = new NamedEvent<ColoredText>();
100 
101  public struct ErrorCatcher : IDisposable
102  {
103  private readonly List<ColoredText> errors;
104  private readonly bool wasConsoleOpen;
105  private Identifier handlerId;
106  public IReadOnlyList<ColoredText> Errors => errors;
107 
108  private ErrorCatcher(Identifier handlerId)
109  {
110  this.handlerId = handlerId;
111 #if CLIENT
112  this.wasConsoleOpen = IsOpen;
113 #else
114  this.wasConsoleOpen = false;
115 #endif
116  this.errors = new List<ColoredText>();
117 
118  //create a local variable that can be captured by lambdas
119  var errs = this.errors;
120 
121  MessageHandler.Register(handlerId, msg =>
122  {
123  if (!msg.IsError) { return; }
124  errs.Add(msg);
125  });
126  }
127 
128  public static ErrorCatcher Create()
129  => new ErrorCatcher(ToolBox.RandomSeed(25).ToIdentifier());
130 
131  public void Dispose()
132  {
133  if (handlerId.IsEmpty) { return; }
134  MessageHandler.Deregister(handlerId);
135  handlerId = Identifier.Empty;
136 #if CLIENT
137  DebugConsole.IsOpen = wasConsoleOpen;
138 #endif
139  }
140  }
141 
142  static partial void ShowHelpMessage(Command command);
143 
144  const int MaxMessages = 300;
145 
146  public static readonly List<ColoredText> Messages = new List<ColoredText>();
147 
148  public delegate void QuestionCallback(string answer);
149  private static QuestionCallback activeQuestionCallback;
150 
151  private static readonly List<Command> commands = new List<Command>();
152  public static List<Command> Commands
153  {
154  get { return commands; }
155  }
156 
157  private static string currentAutoCompletedCommand;
158  private static int currentAutoCompletedIndex;
159 
160  public static bool CheatsEnabled;
161 
162  private static readonly List<ColoredText> unsavedMessages = new List<ColoredText>();
163  private static readonly int messagesPerFile = 800;
164  public const string SavePath = "ConsoleLogs";
165 
166  public static void AssignOnExecute(string names, Action<string[]> onExecute)
167  {
168  var matchingCommand = commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
169  if (matchingCommand == null)
170  {
171  throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found.");
172  }
173  else
174  {
175  matchingCommand.OnExecute = onExecute;
176  }
177  }
178 
179  static DebugConsole()
180  {
181 #if DEBUG
182  CheatsEnabled = true;
183 #endif
184  commands.Add(new Command("help", "", (string[] args) =>
185  {
186  if (args.Length == 0)
187  {
188  foreach (Command c in commands)
189  {
190  if (string.IsNullOrEmpty(c.Help)) continue;
191  ShowHelpMessage(c);
192  }
193  }
194  else
195  {
196  var matchingCommand = commands.Find(c => c.Names.Any(name => name == args[0]));
197  if (matchingCommand == null)
198  {
199  NewMessage("Command " + args[0] + " not found.", Color.Red);
200  }
201  else
202  {
203  ShowHelpMessage(matchingCommand);
204  }
205  }
206  },
207  () =>
208  {
209  return new string[][]
210  {
211  commands.SelectMany(c => c.Names).Select(n => n.Value).ToArray(),
212  Array.Empty<string>()
213  };
214  }));
215 
216  void printMapEntityPrefabs<T>(IEnumerable<T> prefabs) where T : MapEntityPrefab
217  {
218  NewMessage("***************", Color.Cyan);
219  foreach (T prefab in prefabs)
220  {
221  if (prefab.Name.IsNullOrEmpty()) { continue; }
222  string text = $"- {prefab.Name}";
223  if (prefab.Tags.Any())
224  {
225  text += $" ({string.Join(", ", prefab.Tags)})";
226  }
227  if (prefab.AllowedLinks?.Any() ?? false)
228  {
229  text += $", Links: {string.Join(", ", prefab.AllowedLinks)}";
230  }
231  NewMessage(text, prefab.ContentPackage == ContentPackageManager.VanillaCorePackage ? Color.Cyan : Color.Purple);
232  }
233  NewMessage("***************", Color.Cyan);
234  }
235 
236  commands.Add(new Command("items|itemlist", "itemlist: List all the item prefabs available for spawning.", (string[] args) =>
237  {
238  printMapEntityPrefabs(ItemPrefab.Prefabs);
239  }));
240 
241  commands.Add(new Command("itemassemblies", "itemassemblies: List all the item assemblies available for spawning.", (string[] args) =>
242  {
243  printMapEntityPrefabs(ItemAssemblyPrefab.Prefabs);
244  }));
245 
246 
247  commands.Add(new Command("netstats", "netstats: Toggles the visibility of the network statistics UI.", (string[] args) =>
248  {
249  if (GameMain.NetworkMember == null) return;
250  GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats;
251  }));
252 
253  commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
254  () =>
255  {
256  string[] creatureAndJobNames =
257  CharacterPrefab.Prefabs.Select(p => p.Identifier.Value)
258  .Concat(JobPrefab.Prefabs.Select(p => p.Identifier.Value))
259  .OrderBy(s => s)
260  .ToArray();
261 
262  return new string[][]
263  {
264  creatureAndJobNames.ToArray(),
265  new string[] { "near", "inside", "outside", "cursor" },
266  new string[] { "0", "1", "2", "3" },
267  new string[] { "true", "false" },
268 
269  };
270  }, isCheat: true));
271 
272  commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the last parameter is omitted or \"random\".",
273  (string[] args) =>
274  {
275  try
276  {
277 #if CLIENT
278  SpawnItem(args, Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? PlayerInput.MousePosition, Character.Controlled, out string errorMsg);
279 #elif SERVER
280  SpawnItem(args, Vector2.Zero, null, out string errorMsg);
281 #endif
282  if (!string.IsNullOrWhiteSpace(errorMsg))
283  {
284  ThrowError(errorMsg);
285  }
286  }
287  catch (Exception e)
288  {
289  string errorMsg = "Failed to spawn an item. Arguments: \"" + string.Join(" ", args) + "\".";
290  ThrowError(errorMsg, e);
291  GameAnalyticsManager.AddErrorEventOnce("DebugConsole.SpawnItem:Error", GameAnalyticsManager.ErrorSeverity.Error, errorMsg + '\n' + e.Message + '\n' + e.StackTrace.CleanupStackTrace());
292  }
293  },
294  () =>
295  {
296  List<string> itemNames = new List<string>();
297  foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
298  {
299  if (!itemNames.Contains(itemPrefab.Name.Value))
300  {
301  itemNames.Add(itemPrefab.Name.Value);
302  }
303  }
304 
305  List<string> spawnPosParams = new List<string>() { "cursor", "inventory" };
306 #if SERVER
307  if (GameMain.Server != null) spawnPosParams.AddRange(GameMain.Server.ConnectedClients.Select(c => c.Name));
308 #endif
309  spawnPosParams.AddRange(Character.CharacterList.Where(c => c.Inventory != null).Select(c => c.Name).Distinct());
310 
311  return new string[][]
312  {
313  itemNames.ToArray(),
314  spawnPosParams.ToArray()
315  };
316  }, isCheat: true));
317 
318  commands.Add(new Command("disablecrewai", "disablecrewai: Disable the AI of the NPCs in the crew.", (string[] args) =>
319  {
320  HumanAIController.DisableCrewAI = true;
321  NewMessage("Crew AI disabled", Color.Red);
322  }, isCheat: true));
323 
324  commands.Add(new Command("enablecrewai", "enablecrewai: Enable the AI of the NPCs in the crew.", (string[] args) =>
325  {
326  HumanAIController.DisableCrewAI = false;
327  NewMessage("Crew AI enabled", Color.Green);
328  }, isCheat: true));
329 
330  commands.Add(new Command("disableenemyai", "disableenemyai: Disable the AI of the Enemy characters (monsters).", (string[] args) =>
331  {
332  EnemyAIController.DisableEnemyAI = true;
333  NewMessage("Enemy AI disabled", Color.Red);
334  }, isCheat: true));
335 
336  commands.Add(new Command("enableenemyai", "enableenemyai: Enable the AI of the Enemy characters (monsters).", (string[] args) =>
337  {
338  EnemyAIController.DisableEnemyAI = false;
339  NewMessage("Enemy AI enabled", Color.Green);
340  }, isCheat: true));
341 
342  commands.Add(new Command("triggertraitorevent|starttraitoreventimmediately", "triggertraitorevent [eventidentifier]: Skip the initial delay of the traitor events and start one immediately. You can optionally specify which event to start (otherwise a random event is chosen).", null,
343  () =>
344  {
345  return new string[][]
346  {
347  EventPrefab.Prefabs.Where(p => p is TraitorEventPrefab).Select(p => p.Identifier.ToString()).ToArray()
348  };
349  }));
350 
351  commands.Add(new Command("botcount", "botcount [x]: Set the number of bots in the crew in multiplayer.", null));
352 
353  commands.Add(new Command("botspawnmode", "botspawnmode [fill/normal]: Set how bots are spawned in the multiplayer.", null));
354 
355  commands.Add(new Command("killdisconnectedtimer", "killdisconnectedtimer [seconds]: Set the time after which disconnect players' characters get automatically killed.", null));
356 
357  commands.Add(new Command("autorestart", "autorestart [true/false]: Enable or disable round auto-restart.", null));
358 
359  commands.Add(new Command("autorestartinterval", "autorestartinterval [seconds]: Set how long the server waits between rounds before automatically starting a new one. If set to 0, autorestart is disabled.", null));
360 
361  commands.Add(new Command("autorestarttimer", "autorestarttimer [seconds]: Set the current autorestart countdown to the specified value.", null));
362 
363  commands.Add(new Command("startwhenclientsready", "startwhenclientsready [true/false]: Enable or disable automatically starting the round when clients are ready to start.", null));
364 
365  commands.Add(new Command("giveperm", "giveperm [id/steamid/endpoint/name]: Grants administrative permissions to the specified client.", null,
366  () =>
367  {
368  if (GameMain.NetworkMember == null) return null;
369 
370  return new string[][]
371  {
372  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
373  Enum.GetValues(typeof(ClientPermissions)).Cast<ClientPermissions>().Select(v => v.ToString()).ToArray()
374  };
375  }));
376 
377  commands.Add(new Command("revokeperm", "revokeperm [id/steamid/endpoint/name]: Revokes administrative permissions from the specified client.", null,
378  () =>
379  {
380  if (GameMain.NetworkMember == null) return null;
381 
382  return new string[][]
383  {
384  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
385  Enum.GetValues(typeof(ClientPermissions)).Cast<ClientPermissions>().Select(v => v.ToString()).ToArray()
386  };
387  }));
388 
389  commands.Add(new Command("giverank", "giverank [id/steamid/endpoint/name]: Assigns a specific rank (= a set of administrative permissions) to the specified client.", null,
390  () =>
391  {
392  if (GameMain.NetworkMember == null) return null;
393 
394  return new string[][]
395  {
396  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
397  PermissionPreset.List.Select(pp => pp.DisplayName.Value).ToArray()
398  };
399  }));
400 
401  commands.Add(new Command("givecommandperm", "givecommandperm [id/steamid/endpoint/name]: Gives the specified client the permission to use the specified console commands.", null,
402  () =>
403  {
404  if (GameMain.NetworkMember == null) return null;
405 
406  return new string[][]
407  {
408  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
409  commands.Select(c => c.Names.First().Value).Union(new []{ "All" }).ToArray()
410  };
411  }));
412 
413  commands.Add(new Command("revokecommandperm", "revokecommandperm [id/steamid/endpoint/name]: Revokes permission to use the specified console commands from the specified client.", null,
414  () =>
415  {
416  if (GameMain.NetworkMember == null) return null;
417 
418  return new string[][]
419  {
420  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
421  commands.Select(c => c.Names.First().Value).Union(new []{ "All" }).ToArray()
422  };
423  }));
424 
425  commands.Add(new Command("showperm", "showperm [id/steamid/endpoint/name]: Shows the current administrative permissions of the specified client.", null,
426  () =>
427  {
428  if (GameMain.NetworkMember == null) return null;
429 
430  return new string[][]
431  {
432  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray()
433  };
434  }));
435 
436  commands.Add(new Command("respawnnow", "respawnnow: Trigger a respawn immediately if there are any clients waiting to respawn.", null));
437 
438  commands.Add(new Command("showkarma", "showkarma: Show the current karma values of the players.", null));
439  commands.Add(new Command("togglekarma", "togglekarma: Toggle the karma system on/off.", null));
440  commands.Add(new Command("resetkarma", "resetkarma [client]: Resets the karma value of the specified client to 100.", null,
441  () =>
442  {
443  if (GameMain.NetworkMember?.ConnectedClients == null) { return null; }
444  return new string[][]
445  {
446  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray()
447  };
448  }));
449  commands.Add(new Command("setkarma", "setkarma [client] [0-100]: Sets the karma of the specified client to the specified value.", null,
450  () =>
451  {
452  if (GameMain.NetworkMember?.ConnectedClients == null) { return null; }
453  return new string[][]
454  {
455  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
456  new string[] { "50" }
457  };
458  }));
459  commands.Add(new Command("togglekarmatestmode", "togglekarmatestmode: Toggle the karma test mode on/off. When test mode is enabled, clients get notified when their karma value changes (including the reason for the increase/decrease) and the server doesn't ban clients whose karma decreases below the ban threshold.", null));
460 
461  commands.Add(new Command("kick", "kick [name]: Kick a player out of the server.", (string[] args) =>
462  {
463  if (GameMain.NetworkMember == null || args.Length == 0) { return; }
464 
465  string playerName = string.Join(" ", args);
466 
467  ShowQuestionPrompt("Reason for kicking \"" + playerName + "\"? (Enter c to cancel)", (reason) =>
468  {
469  if (reason == "c" || reason == "C") { return; }
470  GameMain.NetworkMember.KickPlayer(playerName, reason);
471  });
472  },
473  () =>
474  {
475  if (GameMain.NetworkMember == null) { return null; }
476 
477  return new string[][]
478  {
479  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray()
480  };
481  }));
482 
483  commands.Add(new Command("kickid", "kickid [id]: Kick the player with the specified client ID out of the server. You can see the IDs of the clients using the command \"clientlist\".", (string[] args) =>
484  {
485  if (GameMain.NetworkMember == null || args.Length == 0) return;
486 
487  int.TryParse(args[0], out int id);
488  var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == id);
489  if (client == null)
490  {
491  ThrowError("Client id \"" + id + "\" not found.");
492  return;
493  }
494 
495  ShowQuestionPrompt("Reason for kicking \"" + client.Name + "\"? (Enter c to cancel)", (reason) =>
496  {
497  if (reason == "c" || reason == "C") { return; }
498  GameMain.NetworkMember.KickPlayer(client.Name, reason);
499  });
500  }));
501 
502  commands.Add(new Command("ban", "ban [name]: Kick and ban the player from the server.", (string[] args) =>
503  {
504  if (GameMain.NetworkMember == null || args.Length == 0) return;
505 
506  string clientName = string.Join(" ", args);
507  ShowQuestionPrompt("Reason for banning \"" + clientName + "\"? (Enter c to cancel)", (reason) =>
508  {
509  if (reason == "c" || reason == "C") { return; }
510  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) =>
511  {
512  if (duration == "c" || duration == "C") { return; }
513  TimeSpan? banDuration = null;
514  if (!string.IsNullOrWhiteSpace(duration))
515  {
516  if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration))
517  {
518  ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\".");
519  return;
520  }
521  banDuration = parsedBanDuration;
522  }
523 
524  GameMain.NetworkMember.BanPlayer(clientName, reason, banDuration);
525  });
526  });
527  },
528  () =>
529  {
530  if (GameMain.NetworkMember == null) return null;
531 
532  return new string[][]
533  {
534  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray()
535  };
536  }));
537 
538  commands.Add(new Command("banid", "banid [id]: Kick and ban the player with the specified client ID from the server. You can see the IDs of the clients using the command \"clientlist\".", (string[] args) =>
539  {
540  if (GameMain.NetworkMember == null || args.Length == 0) return;
541 
542  int.TryParse(args[0], out int id);
543  var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId == id);
544  if (client == null)
545  {
546  ThrowError("Client id \"" + id + "\" not found.");
547  return;
548  }
549 
550  ShowQuestionPrompt("Reason for banning \"" + client.Name + "\"? (Enter c to cancel)", (reason) =>
551  {
552  if (reason == "c" || reason == "C") { return; }
553  ShowQuestionPrompt("Enter the duration of the ban (leave empty to ban permanently, or use the format \"[days] d [hours] h\") (c to cancel)", (duration) =>
554  {
555  if (duration == "c" || duration == "C") { return; }
556  TimeSpan? banDuration = null;
557  if (!string.IsNullOrWhiteSpace(duration))
558  {
559  if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration))
560  {
561  ThrowError("\"" + duration + "\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\".");
562  return;
563  }
564  banDuration = parsedBanDuration;
565  }
566 
567  GameMain.NetworkMember.BanPlayer(client.Name, reason, banDuration);
568  });
569  });
570  }));
571 
572  commands.Add(new Command("banaddress|banip", "banaddress [endpoint]: Ban the IP address/SteamID from the server.", null));
573 
574  commands.Add(new Command("teleportcharacter|teleport", "teleport [character name] [location]: Teleport the specified character to a location , or the position of the cursor if location is omitted. If the name parameter is omitted, the controlled character will be teleported.",
575  onExecute: null,
576  getValidArgs:() =>
577  {
578  var characterList = Character.Controlled != null ? new[] { "Me" } : Array.Empty<string>();
579  var subList = Submarine.MainSub != null ? new[] { "mainsub" } : Array.Empty<string>();
580  return new string[][]
581  {
582  characterList.Concat(ListCharacterNames()).ToArray(),
583  subList.Concat(ListAvailableLocations()).ToArray()
584  };
585  }, isCheat: true));
586 
587  commands.Add(new Command("listlocations|locations", "listlocations: List all the locations in the level: subs, outposts, ruins, caves.",
588  onExecute:(string[] args) =>
589  {
590  var availableLocations = ListAvailableLocations();
591  NewMessage("***************", Color.Cyan);
592  foreach (var location in availableLocations)
593  {
594  NewMessage(location, Color.Cyan);
595  }
596  NewMessage("***************", Color.Cyan);
597  }));
598 
599  commands.Add(new Command("godmode", "godmode [character name]: Toggle character godmode. Makes the targeted character invulnerable to damage. If the name parameter is omitted, the controlled character will receive godmode.",
600  (string[] args) =>
601  {
602  Character targetCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, false);
603 
604  if (targetCharacter == null) { return; }
605 
606  targetCharacter.GodMode = !targetCharacter.GodMode;
607  NewMessage((targetCharacter.GodMode ? "Enabled godmode on " : "Disabled godmode on ") + targetCharacter.Name, Color.White);
608  },
609  () =>
610  {
611  return new string[][] { ListCharacterNames() };
612  }, isCheat: true));
613 
614  commands.Add(new Command("godmode_mainsub", "godmode_mainsub: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (string[] args) =>
615  {
616  if (Submarine.MainSub == null) return;
617 
618  Submarine.MainSub.GodMode = !Submarine.MainSub.GodMode;
619  NewMessage(Submarine.MainSub.GodMode ? "Godmode on" : "Godmode off", Color.White);
620  }, isCheat: true));
621 
622  commands.Add(new Command("growthdelay", "growthdelay: Sets how long it takes for planters to attempt to advance a plant's growth.", (string[] args) =>
623  {
624  if (args.Length > 0 && float.TryParse(args[0], out float value))
625  {
626  Planter.GrowthTickDelay = value;
627  NewMessage($"Growth delay set to {value}.", Color.Green);
628  return;
629  }
630  NewMessage("Invalid value.", Color.Red);
631  }, isCheat: true));
632 
633  commands.Add(new Command("lock", "lock: Lock movement of the main submarine.", (string[] args) =>
634  {
635  Submarine.LockX = !Submarine.LockX;
636  Submarine.LockY = Submarine.LockX;
637  NewMessage((Submarine.LockX ? "Submarine movement locked." : "Submarine movement unlocked."), Color.White);
638  }, null, true));
639 
640  commands.Add(new Command("lockx", "lockx: Lock horizontal movement of the main submarine.", (string[] args) =>
641  {
642  Submarine.LockX = !Submarine.LockX;
643  NewMessage((Submarine.LockX ? "Horizontal submarine movement locked." : "Horizontal submarine movement unlocked."), Color.White);
644  }, null, true));
645 
646  commands.Add(new Command("locky", "locky: Lock vertical movement of the main submarine.", (string[] args) =>
647  {
648  Submarine.LockY = !Submarine.LockY;
649  NewMessage((Submarine.LockY ? "Vertical submarine movement locked." : "Vertical submarine movement unlocked."), Color.White);
650  }, null, true));
651 
652  commands.Add(new Command("dumpids", "", (string[] args) =>
653  {
654  try
655  {
656  int count = args.Length == 0 ? 10 : int.Parse(args[0]);
657  Entity.DumpIds(count, args.Length >= 2 ? args[1] : null);
658  }
659  catch (Exception e)
660  {
661  ThrowError("Failed to dump ids", e);
662  }
663  }));
664 
665  commands.Add(new Command("dumptofile", "findentityids [filename]: Outputs the contents of the debug console into a text file in the game folder. If the filename argument is omitted, \"consoleOutput.txt\" is used as the filename.", (string[] args) =>
666  {
667  string filename = "consoleOutput.txt";
668  if (args.Length > 0) { filename = string.Join(" ", args); }
669 
670  File.WriteAllLines(filename, Messages.Select(m => m.Text).ToArray());
671  }));
672 
673  commands.Add(new Command("findentityids", "findentityids [entityname]", (string[] args) =>
674  {
675  if (args.Length == 0) { return; }
676  foreach (MapEntity mapEntity in MapEntity.MapEntityList)
677  {
678  if (mapEntity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase))
679  {
680  ThrowError(mapEntity.ID + ": " + mapEntity.Name.ToString());
681  }
682  }
683  foreach (Character character in Character.CharacterList)
684  {
685  if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName == args[0])
686  {
687  ThrowError(character.ID + ": " + character.Name.ToString());
688  }
689  }
690  }));
691 
692  commands.Add(new Command("giveaffliction", "giveaffliction [affliction name] [affliction strength] [character name] [limb type] [use relative strength]: Add an affliction to a character. If the name parameter is omitted, the affliction is added to the controlled character.", (string[] args) =>
693  {
694  if (args.Length < 2) { return; }
695  string affliction = args[0];
696  AfflictionPrefab afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Identifier == affliction);
697  if (afflictionPrefab == null)
698  {
699  afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(affliction, StringComparison.OrdinalIgnoreCase));
700  }
701  if (afflictionPrefab == null)
702  {
703  ThrowError("Affliction \"" + affliction + "\" not found.");
704  return;
705  }
706  if (!float.TryParse(args[1], out float afflictionStrength))
707  {
708  ThrowError("\"" + args[1] + "\" is not a valid affliction strength.");
709  return;
710  }
711  bool relativeStrength = false;
712  if (args.Length > 4)
713  {
714  bool.TryParse(args[4], out relativeStrength);
715  }
716  Character targetCharacter = args.Length <= 2 ? Character.Controlled : FindMatchingCharacter(new string[] { args[2] });
717  if (targetCharacter != null)
718  {
719  Limb targetLimb = targetCharacter.AnimController.MainLimb;
720  if (args.Length > 3)
721  {
722  targetLimb = targetCharacter.AnimController.Limbs.FirstOrDefault(l => l.type.ToString().Equals(args[3], StringComparison.OrdinalIgnoreCase));
723  }
724  if (relativeStrength)
725  {
726  afflictionStrength *= targetCharacter.MaxVitality / afflictionPrefab.MaxStrength;
727  }
728  targetCharacter.CharacterHealth.ApplyAffliction(targetLimb ?? targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength));
729  }
730  },
731  () =>
732  {
733  return new string[][]
734  {
735  AfflictionPrefab.Prefabs.Select(a => a.Name.Value).ToArray(),
736  new string[] { "1" },
737  Character.CharacterList.Select(c => c.Name).ToArray(),
738  Enum.GetNames(typeof(LimbType)).ToArray()
739  };
740  }, isCheat: true));
741 
742  commands.Add(new Command("heal", "heal [character name] [all]: Restore the specified character to full health. If the name parameter is omitted, the controlled character will be healed. By default only heals common afflictions such as physical damage and blood loss: use the \"all\" argument to heal everything, including poisonings/addictions/etc.", (string[] args) =>
743  {
744  bool healAll = args.Length > 1 && args[1].Equals("all", StringComparison.OrdinalIgnoreCase);
745  Character healedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(healAll ? args.Take(args.Length - 1).ToArray() : args);
746  if (healedCharacter != null)
747  {
748  healedCharacter.SetAllDamage(0.0f, 0.0f, 0.0f);
749  healedCharacter.Oxygen = 100.0f;
750  healedCharacter.Bloodloss = 0.0f;
751  healedCharacter.SetStun(0.0f, true);
752  if (healAll)
753  {
754  healedCharacter.CharacterHealth.RemoveAllAfflictions();
755  }
756  }
757  },
758  () =>
759  {
760  return new string[][]
761  {
762  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
763  };
764  }, isCheat: true));
765 
766 
767  commands.Add(new Command("listsuitabletreatments", "listsuitabletreatments [character name]: List which items are the most suitable for treating the specified character. Useful for debugging medic AI.", (string[] args) =>
768  {
769  Character character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
770  if (character != null)
771  {
772  Dictionary<Identifier, float> treatments = new Dictionary<Identifier, float>();
773  character.CharacterHealth.GetSuitableTreatments(treatments, user: null);
774  foreach (var treatment in treatments.OrderByDescending(t => t.Value))
775  {
776  Color color = Color.White;
777 #if CLIENT
778  color = ToolBox.GradientLerp(
779  MathUtils.InverseLerp(-1000, 1000, treatment.Value),
780  Color.Red, Color.Yellow, Color.White, Color.LightGreen);
781 #endif
782  NewMessage((int)treatment.Value + ": " + treatment.Key, color);
783 
784  }
785  }
786  },
787  () =>
788  {
789  return new string[][]
790  {
791  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
792  };
793  }, isCheat: true));
794 
795  commands.Add(new Command("revive", "revive [character name]: Bring the specified character back from the dead. If the name parameter is omitted, the controlled character will be revived.", (string[] args) =>
796  {
797  Character revivedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
798  if (revivedCharacter == null) { return; }
799 
800  revivedCharacter.Revive();
801 #if SERVER
802  if (GameMain.Server != null)
803  {
804  foreach (Client c in GameMain.Server.ConnectedClients)
805  {
806  if (c.Character != revivedCharacter) { continue; }
807 
808  // If killed in ironman mode, the character has been wiped from the save mid-round, so its
809  // original data needs to be restored to the save file (without making a backup of the dead character)
810  if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
811  {
812  if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore)
813  {
814  characterToRestore.CharacterInfo.PermanentlyDead = false;
815  mpCampaign.SaveSingleCharacter(characterToRestore, skipBackup: true);
816  }
817  }
818 
819  //clients stop controlling the character when it dies, force control back
820  GameMain.Server.SetClientCharacter(c, revivedCharacter);
821  break;
822  }
823  }
824 #endif
825  },
826  () =>
827  {
828  return new string[][]
829  {
830  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
831  };
832  }, isCheat: true));
833 
834  commands.Add(new Command("freeze", "", (string[] args) =>
835  {
836  if (Character.Controlled != null) Character.Controlled.AnimController.Frozen = !Character.Controlled.AnimController.Frozen;
837  }, isCheat: true));
838 
839  commands.Add(new Command("ragdoll", "ragdoll [character name]: Force-ragdoll the specified character. If the name parameter is omitted, the controlled character will be ragdolled.", (string[] args) =>
840  {
841  Character ragdolledCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
842  if (ragdolledCharacter != null)
843  {
844  ragdolledCharacter.IsForceRagdolled = !ragdolledCharacter.IsForceRagdolled;
845  }
846  },
847  () =>
848  {
849  return new string[][]
850  {
851  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
852  };
853  }, isCheat: true));
854 
855  commands.Add(new Command("freecamera|freecam", "freecam: Detach the camera from the controlled character.", (string[] args) =>
856  {
857 #if CLIENT
858  if (Screen.Selected == GameMain.SubEditorScreen) { return; }
859 
860  if (GameMain.Client == null)
861  {
862  Character.Controlled = null;
863  GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
864  }
865  else
866  {
867  GameMain.Client?.SendConsoleCommand("freecam");
868  }
869 #endif
870  }, isCheat: true));
871 
872  commands.Add(new Command("eventmanager", "eventmanager: Toggle event manager on/off. No new random events are created when the event manager is disabled.", (string[] args) =>
873  {
874  if (GameMain.GameSession?.EventManager != null)
875  {
876  GameMain.GameSession.EventManager.Enabled = !GameMain.GameSession.EventManager.Enabled;
877  NewMessage(GameMain.GameSession.EventManager.Enabled ? "Event manager on" : "Event manager off", Color.White);
878  }
879  }, isCheat: true));
880 
881  commands.Add(new Command("triggerevent", "triggerevent [identifier]: Created a new event.", (string[] args) =>
882  {
883  List<EventPrefab> eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList();
884  if (GameMain.GameSession?.EventManager != null && args.Length > 0)
885  {
886  EventPrefab eventPrefab = eventPrefabs.Find(prefab => prefab.Identifier == args[0]);
887  if (eventPrefab is TraitorEventPrefab)
888  {
889  ThrowError($"{eventPrefab.Identifier} is a traitor event. You need to use the 'triggertraitorevent' command to start it.");
890  return;
891  }
892  else if (eventPrefab != null)
893  {
894  var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
895  if (newEvent == null)
896  {
897  NewMessage($"Could not initialize event {args[0]} because level did not meet requirements");
898  return;
899  }
900  GameMain.GameSession.EventManager.ActivateEvent(newEvent);
901  NewMessage($"Initialized event {eventPrefab.Identifier}", Color.Aqua);
902  return;
903  }
904 
905  NewMessage($"Failed to trigger event because {args[0]} is not a valid event identifier.", Color.Red);
906  return;
907  }
908  NewMessage("Failed to trigger event", Color.Red);
909  }, isCheat: true, getValidArgs: () =>
910  {
911  List<EventPrefab> eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList();
912 
913  return new[]
914  {
915  eventPrefabs.Select(prefab => prefab.Identifier).Distinct().Select(id => id.Value).ToArray()
916  };
917  }));
918 
919  commands.Add(new Command("debugevent", "debugevent [identifier]: outputs debug info about a specific event that's currently active. Mainly intended for debugging events in multiplayer: in single player, the same information is available by enabling debugdraw.", (string[] args) =>
920  {
921  if (args.Length == 0)
922  {
923  ThrowError($"Please specify the identifier of the event you want to debug.");
924  return;
925  }
926 
927  if (GameMain.GameSession?.EventManager is EventManager eventManager)
928  {
929  var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]);
930  if (ev == null)
931  {
932  ThrowError($"Event \"{args[0]}\" not found.");
933  }
934  else
935  {
936  string info = ev.GetDebugInfo();
937 #if SERVER
938  //strip rich text tags
939  RichTextData.GetRichTextData(info, out info);
940 #endif
941  NewMessage(info);
942  }
943  }
944  }, isCheat: true, getValidArgs: () =>
945  {
946  IEnumerable<EventPrefab> eventPrefabs;
947  if (GameMain.GameSession?.EventManager == null || GameMain.GameSession.EventManager.ActiveEvents.None())
948  {
949  eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty);
950  }
951  else
952  {
953  eventPrefabs = GameMain.GameSession.EventManager.ActiveEvents.Select(e => e.Prefab);
954  }
955  return new[]
956  {
957  eventPrefabs.Select(ev => ev.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
958  };
959  }));
960 
961  commands.Add(new Command("unlockmission", "unlockmission [identifier/tag]: Unlocks a mission in a random adjacent level.", (string[] args) =>
962  {
963  if (GameMain.GameSession?.GameMode is not CampaignMode campaign)
964  {
965  ThrowError("The unlockmission command is only usable in the campaign mode.");
966  return;
967  }
968  if (args.Length == 0)
969  {
970  ThrowError("Please enter the identifier or a tag of the mission you want to unlock.");
971  return;
972  }
973  var currentLocation = campaign.Map.CurrentLocation;
974  if (MissionPrefab.Prefabs.Any(p => p.Identifier == args[0]))
975  {
976  currentLocation.UnlockMissionByIdentifier(args[0].ToIdentifier());
977  }
978  else
979  {
980  currentLocation.UnlockMissionByTag(args[0].ToIdentifier());
981  }
982  if (campaign is MultiPlayerCampaign mpCampaign)
983  {
984  mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
985  }
986  }, isCheat: true, getValidArgs: () =>
987  {
988  return new[]
989  {
990  MissionPrefab.Prefabs.Select(p => p.Identifier.ToString()).ToArray()
991  };
992  }));
993 
994  commands.Add(new Command("setcampaignmetadata", "setcampaignmetadata [identifier] [value]: Sets the specified campaign metadata value.", (string[] args) =>
995  {
996  if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
997  {
998  ThrowError("The setcampaignmetadata command is only usable in the campaign mode.");
999  return;
1000  }
1001  if (args.Length < 2)
1002  {
1003  ThrowError("Please specify an identifier and a value.");
1004  return;
1005  }
1006  if (float.TryParse(args[1], out float floatVal))
1007  {
1008  SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), floatVal, SetDataAction.OperationType.Set);
1009  }
1010  else
1011  {
1012  SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set);
1013  }
1014 
1015  }, isCheat: true));
1016 
1017  commands.Add(new Command("setskill", "setskill [all/identifier] [max/level] [character]: Set your skill level.", (string[] args) =>
1018  {
1019  if (args.Length < 2)
1020  {
1021  NewMessage($"Missing arguments. Expected at least 2 but got {args.Length} (skill, level, name)", Color.Red);
1022  return;
1023  }
1024 
1025  Identifier skillIdentifier = args[0].ToIdentifier();
1026  string levelString = args[1];
1027  Character character = args.Length >= 3 ? FindMatchingCharacter(args.Skip(2).ToArray(), false) : Character.Controlled;
1028 
1029  if (character?.Info?.Job == null)
1030  {
1031  NewMessage("Character is not valid.", Color.Red);
1032  return;
1033  }
1034 
1035  bool isMax = levelString.Equals("max", StringComparison.OrdinalIgnoreCase);
1036 
1037  if (float.TryParse(levelString, NumberStyles.Number, CultureInfo.InvariantCulture, out float level) || isMax)
1038  {
1039  if (isMax) { level = 100; }
1040  if (skillIdentifier == "all")
1041  {
1042  foreach (Skill skill in character.Info.Job.GetSkills())
1043  {
1044  character.Info.SetSkillLevel(skill.Identifier, level);
1045  }
1046  NewMessage($"Set all {character.Name}'s skills to {level}", Color.Green);
1047  }
1048  else
1049  {
1050  character.Info.SetSkillLevel(skillIdentifier, level);
1051  NewMessage($"Set {character.Name}'s {skillIdentifier} level to {level}", Color.Green);
1052  }
1053  }
1054  else
1055  {
1056  NewMessage($"{levelString} is not a valid level. Expected number or \"max\".", Color.Red);
1057  }
1058  }, isCheat: true, getValidArgs: () =>
1059  {
1060  return new[]
1061  {
1062  Character.Controlled?.Info?.Job?.GetSkills()?.Select(skill => skill.Identifier.Value).ToArray() ?? Array.Empty<string>(),
1063  new[]{ "max" },
1064  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(),
1065  };
1066  }));
1067 
1068  commands.Add(new Command("water|editwater", "water/editwater: Toggle water editing. Allows adding water into rooms by holding the left mouse button and removing it by holding the right mouse button.", (string[] args) =>
1069  {
1070  Hull.EditWater = !Hull.EditWater;
1071  NewMessage(Hull.EditWater ? "Water editing on" : "Water editing off", Color.White);
1072  }, isCheat: true));
1073 
1074  commands.Add(new Command("givetalent", "givetalent [talent] [player]: give the talent to the specified character. If the character argument is omitted, the talent is given to the controlled character.", (string[] args) =>
1075  {
1076  if (args.Length == 0) { return; }
1077  var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) : Character.Controlled;
1078  if (character != null)
1079  {
1080  TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c =>
1081  c.Identifier == args[0] ||
1082  c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase));
1083  if (talentPrefab == null)
1084  {
1085  ThrowError($"Couldn't find the talent \"{args[0]}\".");
1086  return;
1087  }
1088  character.GiveTalent(talentPrefab);
1089  NewMessage($"Gave talent \"{talentPrefab.DisplayName}\" to \"{character.Name}\".");
1090  }
1091  },
1092  () =>
1093  {
1094  List<string> talentNames = new List<string>();
1095  foreach (TalentPrefab talent in TalentPrefab.TalentPrefabs)
1096  {
1097  talentNames.Add(talent.DisplayName.Value);
1098  }
1099 
1100  return new string[][]
1101  {
1102  talentNames.Select(id => id).ToArray(),
1103  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1104  };
1105  }, isCheat: true));
1106 
1107  commands.Add(new Command("unlocktalents", "unlocktalents [all/[jobname]] [character]: give the specified character all the talents of the specified class", (string[] args) =>
1108  {
1109  var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) : Character.Controlled;
1110  if (character == null) { return; }
1111 
1112  List<TalentTree> talentTrees = new List<TalentTree>();
1113  if (args.Length == 0 || args[0].Equals("all", StringComparison.OrdinalIgnoreCase))
1114  {
1115  talentTrees.AddRange(TalentTree.JobTalentTrees);
1116  }
1117  else
1118  {
1119  var job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase));
1120  if (job == null)
1121  {
1122  ThrowError($"Failed to find the job \"{args[0]}\".");
1123  return;
1124  }
1125  if (!TalentTree.JobTalentTrees.TryGet(job.Identifier, out TalentTree talentTree))
1126  {
1127  ThrowError($"No talents configured for the job \"{args[0]}\".");
1128  return;
1129  }
1130  talentTrees.Add(talentTree);
1131  }
1132 
1133  foreach (var talentTree in talentTrees)
1134  {
1135  foreach (var talentId in talentTree.AllTalentIdentifiers)
1136  {
1137  character.GiveTalent(talentId);
1138  NewMessage($"Unlocked talent \"{talentId}\".");
1139  }
1140  }
1141  },
1142  () =>
1143  {
1144  List<string> availableArgs = new List<string>() { "All" };
1145  availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name.Value));
1146  return new string[][]
1147  {
1148  availableArgs.ToArray(),
1149  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1150  };
1151  }, isCheat: true));
1152 
1153  commands.Add(new Command("giveexperience", "giveexperience [amount] [character]: Give experience to character.", (string[] args) =>
1154  {
1155  if (args.Length < 1)
1156  {
1157  NewMessage($"Missing arguments. Expected at least 1 but got {args.Length} (experience, name)");
1158  return;
1159  }
1160 
1161  string experienceString = args[0];
1162  var character = FindMatchingCharacter(args.Skip(1).ToArray()) ?? Character.Controlled;
1163 
1164  if (character?.Info == null)
1165  {
1166  NewMessage("Character is not valid.");
1167  return;
1168  }
1169 
1170  if (int.TryParse(experienceString, NumberStyles.Number, CultureInfo.InvariantCulture, out int experience))
1171  {
1172  character.Info.GiveExperience(experience);
1173  NewMessage($"Gave {character.Name} {experience} experience");
1174  }
1175  else
1176  {
1177  NewMessage($"{experienceString} is not a valid value. Expected number.");
1178  }
1179  }, isCheat: true, getValidArgs: () =>
1180  {
1181  return new[]
1182  {
1183  new string[] { "100" },
1184  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray(),
1185  };
1186  }));
1187 
1188  commands.Add(new Command("fire|editfire", "fire/editfire: Allows putting up fires by left clicking.", (string[] args) =>
1189  {
1190  Hull.EditFire = !Hull.EditFire;
1191  NewMessage(Hull.EditFire ? "Fire spawning on" : "Fire spawning off", Color.White);
1192  }, isCheat: true));
1193 
1194  commands.Add(new Command("explosion", "explosion [range] [force] [damage] [structuredamage] [item damage] [emp strength] [ballast flora strength]: Creates an explosion at the position of the cursor.", null, isCheat: true));
1195 
1196  commands.Add(new Command("showseed|showlevelseed", "showseed: Show the seed of the current level.", (string[] args) =>
1197  {
1198  if (Level.Loaded == null)
1199  {
1200  ThrowError("No level loaded.");
1201  }
1202  else
1203  {
1204  NewMessage("Level seed: " + Level.Loaded.Seed);
1205  NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier);
1206  NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()));
1207  NewMessage("Mirrored: " + Level.Loaded.Mirrored);
1208  NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y);
1209  NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown"));
1210  }
1211  },null));
1212 
1213  commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
1214  onExecute:(string[] args) =>
1215  {
1216  if (Submarine.MainSub == null) { return; }
1217 
1218  if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
1219  {
1220 #if SERVER
1221  ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client.");
1222 #else
1223  Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
1224 #endif
1225  }
1226  else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
1227  {
1228  if (Level.Loaded == null)
1229  {
1230  NewMessage("Can't teleport the sub to the start of the level (no level loaded).", Color.Red);
1231  return;
1232  }
1233  Vector2 pos = Level.Loaded.StartPosition;
1234  if (Level.Loaded.StartOutpost != null)
1235  {
1236  pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
1237  }
1238  Submarine.MainSub.SetPosition(pos);
1239  }
1240  else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
1241  {
1242  if (Level.Loaded == null)
1243  {
1244  NewMessage("Can't teleport the sub to the end of the level (no level loaded).", Color.Red);
1245  return;
1246  }
1247  Vector2 pos = Level.Loaded.EndPosition;
1248  if (Level.Loaded.EndOutpost != null)
1249  {
1250  pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
1251  }
1252  Submarine.MainSub.SetPosition(pos);
1253  }
1254  else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
1255  {
1256  Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
1257 
1258  var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
1259  if (Level.Loaded?.EndOutpost == null)
1260  {
1261  NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
1262  return;
1263  }
1264  var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost);
1265  if (submarineDockingPort != null && outpostDockingPort != null)
1266  {
1267  submarineDockingPort.Dock(outpostDockingPort);
1268  }
1269  }
1270  },
1271  getValidArgs:() =>
1272  {
1273  return new string[][]
1274  {
1275  new string[] { "start", "end", "endoutpost", "cursor" }
1276  };
1277  }, isCheat: true));
1278 
1279 #if DEBUG
1280  commands.Add(new Command("crash", "crash: Crashes the game.", (string[] args) =>
1281  {
1282  throw new Exception("crash command issued");
1283  }));
1284 
1285  commands.Add(new Command("listeditableproperties", "", (string[] args) =>
1286  {
1287  StringBuilder sb = new StringBuilder();
1288  string filename;
1289 #if CLIENT
1290  filename = "ItemComponent properties (client).txt";
1291  sb.AppendLine("Client-side ItemComponent properties:");
1292 #else
1293  filename = "ItemComponent properties (server).txt";
1294  sb.AppendLine("Server-side ItemComponent properties:");
1295 #endif
1296  var itemComponents = typeof(ItemComponent).Assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(ItemComponent)));
1297  foreach (var ic in itemComponents.OrderBy(ic => ic.Name))
1298  {
1299  sb.AppendLine(ic.Name+":");
1300  foreach (var prop in ic.GetProperties())
1301  {
1302  if (prop.DeclaringType != ic) { continue; }
1303  if (prop.GetCustomAttributes(inherit: false).OfType<Editable>().Any())
1304  {
1305  sb.AppendLine(prop.Name);
1306  }
1307  }
1308  }
1309  File.WriteAllText(filename, sb.ToString());
1310  }));
1311 
1312  commands.Add(new Command("fastforward", "fastforward [seconds]: Fast forwards the game by x seconds. Note that large numbers may cause a long freeze.", (string[] args) =>
1313  {
1314  float seconds = 0;
1315  if (args.Length > 0) { float.TryParse(args[0], out seconds); }
1316  System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
1317  sw.Start();
1318  for (int i = 0; i < seconds * Timing.FixedUpdateRate; i++)
1319  {
1320  Screen.Selected?.Update(Timing.Step);
1321  }
1322  sw.Stop();
1323  NewMessage($"Fast-forwarded by {seconds} seconds (took {sw.ElapsedMilliseconds / 1000.0f} s).");
1324  }));
1325 
1326  commands.Add(new Command("removecharacter", "removecharacter [character name]: Immediately deletes the specified character.", (string[] args) =>
1327  {
1328  if (args.Length == 0) { return; }
1329  Character character = FindMatchingCharacter(args, false);
1330  if (character == null) { return; }
1331 
1332  Entity.Spawner?.AddEntityToRemoveQueue(character);
1333  },
1334  () =>
1335  {
1336  return new string[][]
1337  {
1338  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1339  };
1340  }, isCheat: true));
1341 
1342  commands.Add(new Command("waterphysicsparams", "waterphysicsparams [stiffness] [spread] [damping]: defaults 0.02, 0.05, 0.05", (string[] args) =>
1343  {
1344  float stiffness = 0.02f, spread = 0.05f, damp = 0.01f;
1345  if (args.Length > 0) float.TryParse(args[0], out stiffness);
1346  if (args.Length > 1) float.TryParse(args[1], out spread);
1347  if (args.Length > 2) float.TryParse(args[2], out damp);
1348  Hull.WaveStiffness = stiffness;
1349  Hull.WaveSpread = spread;
1350  Hull.WaveDampening = damp;
1351  }, null));
1352 
1353  commands.Add(new Command("testlevels", "testlevels", (string[] args) =>
1354  {
1355  CoroutineManager.StartCoroutine(TestLevels());
1356  },
1357  null));
1358 
1359  IEnumerable<CoroutineStatus> TestLevels()
1360  {
1361  SubmarineInfo selectedSub = null;
1362  Identifier subName = GameSettings.CurrentConfig.QuickStartSub;
1363  if (subName != Identifier.Empty)
1364  {
1365  selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName);
1366  }
1367 
1368  int count = 0;
1369  while (true)
1370  {
1371  var gamesession = new GameSession(
1372  SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.Type == SubmarineType.Player && !s.HasTag(SubmarineTag.HideInMenus)),
1373  GameModePreset.DevSandbox ?? GameModePreset.Sandbox);
1374  string seed = ToolBox.RandomSeed(16);
1375  gamesession.StartRound(seed);
1376 
1377  Rectangle subWorldRect = Submarine.MainSub.Borders;
1378  subWorldRect.Location += new Point((int)Submarine.MainSub.WorldPosition.X, (int)Submarine.MainSub.WorldPosition.Y);
1379  subWorldRect.Y -= subWorldRect.Height;
1380  foreach (var ruin in Level.Loaded.Ruins)
1381  {
1382  if (ruin.Area.Intersects(subWorldRect))
1383  {
1384  ThrowError("Ruins intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Info.Name);
1385  yield return CoroutineStatus.Success;
1386  }
1387  }
1388 
1389  var levelCells = Level.Loaded.GetCells(
1390  Submarine.MainSub.WorldPosition,
1391  Math.Max(Submarine.MainSub.Borders.Width / Level.GridCellSize, 2));
1392  foreach (var cell in levelCells)
1393  {
1394  Vector2 minExtents = new Vector2(
1395  cell.Edges.Min(e => Math.Min(e.Point1.X, e.Point2.X)),
1396  cell.Edges.Min(e => Math.Min(e.Point1.Y, e.Point2.Y)));
1397  Vector2 maxExtents = new Vector2(
1398  cell.Edges.Max(e => Math.Max(e.Point1.X, e.Point2.X)),
1399  cell.Edges.Max(e => Math.Max(e.Point1.Y, e.Point2.Y)));
1400  Rectangle cellRect = new Rectangle(
1401  (int)minExtents.X, (int)minExtents.Y,
1402  (int)(maxExtents.X - minExtents.X), (int)(maxExtents.Y - minExtents.Y));
1403  if (cellRect.Intersects(subWorldRect))
1404  {
1405  ThrowError("Level cells intersect with the sub. Seed: " + seed + ", Submarine: " + Submarine.MainSub.Info.Name);
1406  yield return CoroutineStatus.Success;
1407  }
1408  }
1409 
1410  GameMain.GameSession.EndRound("");
1411  Submarine.Unload();
1412 
1413  count++;
1414  NewMessage("Level seed " + seed + " ok (test #" + count + ")");
1415 #if CLIENT
1416  //dismiss round summary and any other message boxes
1417  GUIMessageBox.CloseAll();
1418 #endif
1419  yield return CoroutineStatus.Running;
1420  }
1421  }
1422 #endif
1423 
1424  commands.Add(new Command("showreputation", "showreputation: List the current reputation values.", (string[] args) =>
1425  {
1426  if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1427  {
1428  NewMessage("Reputation:");
1429  foreach (var faction in campaign.Factions)
1430  {
1431  NewMessage($" - {faction.Prefab.Name}: {faction.Reputation.Value}");
1432  }
1433  }
1434  else
1435  {
1436  ThrowError("Could not show reputation (no active campaign).");
1437  }
1438  }, null));
1439 
1440  commands.Add(new Command("setlocationreputation", "setlocationreputation [value]: Set the reputation in the current location to the specified value.", (string[] args) =>
1441  {
1442  if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1443  {
1444  if (args.Length == 0) { return; }
1445  if (float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation))
1446  {
1447  campaign.Map.CurrentLocation.Reputation?.SetReputation(reputation);
1448  }
1449  else
1450  {
1451  ThrowError($"Could not set location reputation ({args[0]} is not a valid reputation value).");
1452  }
1453  }
1454  else
1455  {
1456  ThrowError("Could not set location reputation (no active campaign).");
1457  }
1458  }, null, true));
1459 
1460  commands.Add(new Command("setreputation", "setreputation [faction] [value]: Set the reputation of a cation to the specified value.", (string[] args) =>
1461  {
1462  if (args.Length < 2)
1463  {
1464  ThrowError("Insufficient arguments (expected 2)");
1465  return;
1466  }
1467 
1468  if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1469  {
1470  if (campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == args[0]) is { } faction)
1471  {
1472  if (float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float reputation))
1473  {
1474  faction.Reputation.SetReputation(reputation);
1475  }
1476  else
1477  {
1478  ThrowError($"Could not set faction reputation ({args[1]} is not a valid reputation value).");
1479  }
1480  }
1481  else
1482  {
1483  ThrowError($"Could not set faction reputation (faction {args[0]} not found).");
1484  }
1485  }
1486  else
1487  {
1488  ThrowError("Could not set faction reputation (no active campaign).");
1489  }
1490  }, () =>
1491  {
1492  return new[]
1493  {
1494  FactionPrefab.Prefabs.Select(static f => f.Identifier.Value).ToArray(),
1495  GameMain.GameSession?.Campaign?.Factions.Select(static f => f.Prefab.Identifier.ToString()).ToArray() ?? Array.Empty<string>()
1496  };
1497  }, true));
1498 
1499  commands.Add(new Command("fixitems", "fixitems: Repairs all items and restores them to full condition.", (string[] args) =>
1500  {
1501  foreach (Item it in Item.ItemList)
1502  {
1503  if (it.GetComponent<GeneticMaterial>() != null) { continue; }
1504  it.Condition = it.MaxCondition;
1505  }
1506  }, null, true));
1507 
1508  commands.Add(new Command("fixhulls|fixwalls", "fixwalls/fixhulls: Fixes all walls.", (string[] args) =>
1509  {
1510  var walls = new List<Structure>(Structure.WallList);
1511  foreach (Structure w in walls)
1512  {
1513  try
1514  {
1515  for (int i = 0; i < w.SectionCount; i++)
1516  {
1517  w.AddDamage(i, -100000.0f);
1518  }
1519  }
1520  catch (InvalidOperationException e)
1521  {
1522  string errorMsg = "Error while executing the fixhulls command.\n" + e.StackTrace.CleanupStackTrace();
1523  GameAnalyticsManager.AddErrorEventOnce("DebugConsole.FixHulls", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1524  }
1525  }
1526  }, null, true));
1527 
1528  commands.Add(new Command("maxupgrades", "maxupgrades [category] [prefab]: Maxes out all upgrades or only specific one if given arguments.", args =>
1529  {
1530  UpgradeManager upgradeManager = GameMain.GameSession?.Campaign?.UpgradeManager;
1531  if (upgradeManager == null)
1532  {
1533  ThrowError("This command can only be used in campaign.");
1534  return;
1535  }
1536 
1537  string categoryIdentifier = null;
1538  string prefabIdentifier = null;
1539 
1540  switch (args.Length)
1541  {
1542  case 1:
1543  categoryIdentifier = args[0];
1544  break;
1545  case 2:
1546  categoryIdentifier = args[0];
1547  prefabIdentifier = args[1];
1548  break;
1549  }
1550 
1551  foreach (UpgradeCategory category in UpgradeCategory.Categories)
1552  {
1553  if (!string.IsNullOrWhiteSpace(categoryIdentifier) && category.Identifier != categoryIdentifier) { continue; }
1554  foreach (UpgradePrefab prefab in UpgradePrefab.Prefabs)
1555  {
1556  if (!prefab.UpgradeCategories.Contains(category)) { continue; }
1557  if (!string.IsNullOrWhiteSpace(prefabIdentifier) && prefab.Identifier != prefabIdentifier) { continue; }
1558 
1559  int targetLevel = prefab.GetMaxLevelForCurrentSub() - upgradeManager.GetRealUpgradeLevel(prefab, category);
1560  for (int i = 0; i < targetLevel; i++)
1561  {
1562  upgradeManager.PurchaseUpgrade(prefab, category, force: true);
1563  }
1564  NewMessage($"Upgraded {category.Identifier}.{prefab.Identifier} by {targetLevel} levels.", Color.DarkGreen);
1565  }
1566  }
1567 
1568  NewMessage($"Start a new round to apply the upgrades.", Color.Lime);
1569  }, () =>
1570  {
1571  return new[]
1572  {
1573  UpgradeCategory.Categories.Select(c => c.Identifier).Distinct().Select(i => i.Value).ToArray(),
1574  UpgradePrefab.Prefabs.Select(c => c.Identifier).Distinct().Select(i => i.Value).ToArray()
1575  };
1576  }, true));
1577 
1578  commands.Add(new Command("power", "power: Immediately powers up the submarine's nuclear reactor.", (string[] args) =>
1579  {
1580  Item reactorItem = Item.ItemList.Find(i => i.GetComponent<Reactor>() != null);
1581  if (reactorItem == null) { return; }
1582 
1583  var reactor = reactorItem.GetComponent<Reactor>();
1584  reactor.PowerUpImmediately();
1585 #if SERVER
1586  if (GameMain.Server != null)
1587  {
1588  reactorItem.CreateServerEvent(reactor);
1589  }
1590 #endif
1591  }, null, true));
1592 
1593  commands.Add(new Command("oxygen|air", "oxygen/air: Replenishes the oxygen levels in every room to 100%.", (string[] args) =>
1594  {
1595  foreach (Hull hull in Hull.HullList)
1596  {
1597  hull.OxygenPercentage = 100.0f;
1598  }
1599  }, null, true));
1600 
1601  commands.Add(new Command("kill", "kill [character]: Immediately kills the specified character.", (string[] args) =>
1602  {
1603  Character killedCharacter = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args);
1604  killedCharacter?.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null);
1605  },
1606  () =>
1607  {
1608  return new string[][]
1609  {
1610  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1611  };
1612  }, isCheat: true));
1613 
1614  commands.Add(new Command("killmonsters", "killmonsters: Immediately kills all AI-controlled enemies in the level.", (string[] args) =>
1615  {
1616  foreach (Character c in Character.CharacterList)
1617  {
1618  if (c.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior == null)
1619  {
1620  c.SetAllDamage(200.0f, 0.0f, 0.0f);
1621  }
1622  }
1623  foreach (Hull hull in Hull.HullList)
1624  {
1625  hull.BallastFlora?.Kill();
1626  }
1627  foreach (Submarine sub in Submarine.Loaded)
1628  {
1629  sub.WreckAI?.Kill();
1630  }
1631  }, null, isCheat: true));
1632 
1633  commands.Add(new Command("despawnnow", "despawnnow [character]: Immediately despawns the specified dead character. If the character argument is omitted, all dead characters are despawned.", (string[] args) =>
1634  {
1635  if (args.Length == 0)
1636  {
1637  foreach (Character c in Character.CharacterList.Where(c => c.IsDead).ToList())
1638  {
1639  c.DespawnNow();
1640  }
1641  }
1642  else
1643  {
1644  Character character = FindMatchingCharacter(args);
1645  character?.DespawnNow();
1646  }
1647  },
1648  () =>
1649  {
1650  return new string[][]
1651  {
1652  Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1653  };
1654  }, isCheat: true));
1655 
1656  commands.Add(new Command("setclientcharacter", "setclientcharacter [client name] [character name]: Gives the client control of the specified character.", null,
1657  () =>
1658  {
1659  if (GameMain.NetworkMember == null) return null;
1660 
1661  return new string[][]
1662  {
1663  GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
1664  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1665  };
1666  }));
1667 
1668  commands.Add(new Command("campaigninfo|campaignstatus", "campaigninfo: Display information about the state of the currently active campaign.", (string[] args) =>
1669  {
1670  if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
1671  {
1672  ThrowError("No campaign active!");
1673  return;
1674  }
1675 
1676  campaign.LogState();
1677  }));
1678 
1679  commands.Add(new Command("campaigndestination|setcampaigndestination", "campaigndestination [index]: Set the location to head towards in the currently active campaign.", (string[] args) =>
1680  {
1681  if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
1682  {
1683  ThrowError("No campaign active!");
1684  return;
1685  }
1686 
1687  if (args.Length == 0)
1688  {
1689  int i = 0;
1690  foreach (LocationConnection connection in campaign.Map.CurrentLocation.Connections)
1691  {
1692  NewMessage(" " + i + ". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White);
1693  i++;
1694  }
1695  ShowQuestionPrompt("Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) + "):", (string selectedDestination) =>
1696  {
1697  int destinationIndex = -1;
1698  if (!int.TryParse(selectedDestination, out destinationIndex)) return;
1699  if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count)
1700  {
1701  NewMessage("Index out of bounds!", Color.Red);
1702  return;
1703  }
1704  Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
1705  campaign.Map.SelectLocation(location);
1706  NewMessage(location.DisplayName + " selected.", Color.White);
1707  });
1708  }
1709  else
1710  {
1711  int destinationIndex = -1;
1712  if (!int.TryParse(args[0], out destinationIndex)) return;
1713  if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count)
1714  {
1715  NewMessage("Index out of bounds!", Color.Red);
1716  return;
1717  }
1718  Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
1719  campaign.Map.SelectLocation(location);
1720  NewMessage(location.DisplayName + " selected.", Color.White);
1721  }
1722  }));
1723 
1724  commands.Add(new Command("togglecampaignteleport", "Toggle on/off teleportation between campaign locations by double clicking on the campaign map.", args =>
1725  {
1726  if (GameMain.GameSession?.Campaign == null)
1727  {
1728  ThrowError("No campaign active.");
1729  return;
1730  }
1731  GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport;
1732  NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ? "Enabled" : "Disabled") + " teleportation on the campaign map.", Color.White);
1733  }, isCheat: true));
1734 
1735  commands.Add(new Command("money", "money [amount] [character]: Gives the specified amount of money to the crew when a campaign is active.", args =>
1736  {
1737  if (args.Length == 0) { return; }
1738 
1739  if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) { return; }
1740  Character targetCharacter = null;
1741 
1742  if (args.Length >= 2)
1743  {
1744  targetCharacter = FindMatchingCharacter(args.Skip(1).ToArray());
1745  }
1746 
1747  if (int.TryParse(args[0], out int money))
1748  {
1749  Wallet wallet = targetCharacter is null || GameMain.IsSingleplayer ? campaign.Bank : targetCharacter.Wallet;
1750  wallet.Give(money);
1751  GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat, "console");
1752  }
1753  else
1754  {
1755  ThrowError($"\"{args[0]}\" is not a valid numeric value.");
1756  }
1757  }, isCheat: true, getValidArgs: () => new []
1758  {
1759  new []{ string.Empty },
1760  Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
1761  }));
1762 
1763  commands.Add(new Command("showmoney", "showmoney: Shows the amount of money in everyones wallet.", args =>
1764  {
1765  if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
1766  {
1767  ThrowError("No campaign active!");
1768  return;
1769  }
1770 
1771  NewMessage($"Bank: {campaign.Bank.Balance}");
1772  }, isCheat: true));
1773 
1774  commands.Add(new Command("skipeventcooldown", "skipeventcooldown: Skips the currently active event cooldown and triggers pending monster spawns immediately.", args =>
1775  {
1776  GameMain.GameSession?.EventManager?.SkipEventCooldown();
1777  }, isCheat: true));
1778 
1779  commands.Add(new Command("ballastflora", "infectballast [options]: Infect ballasts and control its growth.", args =>
1780  {
1781  if (args.Length == 0)
1782  {
1783  ThrowError("No action specified.");
1784  return;
1785  }
1786 
1787  string primaryAction = args.Length > 0 ? args[0] : "";
1788  string secondaryArgument = args.Length > 1 ? args[1] : "";
1789 
1790  if (Submarine.MainSub == null)
1791  {
1792  ThrowError("No submarine loaded.");
1793  return;
1794  }
1795 
1796  if (primaryAction.Equals("infect", StringComparison.OrdinalIgnoreCase))
1797  {
1798  List<Pump> pumps = new List<Pump>();
1799  foreach (Item item in Submarine.MainSub.GetItems(true))
1800  {
1801  if (item.CurrentHull != null && item.HasTag(Tags.Ballast) && item.GetComponent<Pump>() is { } pump)
1802  {
1803  if (item.CurrentHull.BallastFlora != null) { continue; }
1804  pumps.Add(pump);
1805  }
1806  }
1807 
1808  if (pumps.Any())
1809  {
1810  BallastFloraPrefab prefab = string.IsNullOrWhiteSpace(secondaryArgument) ? BallastFloraPrefab.Prefabs.First() : BallastFloraPrefab.Find(secondaryArgument.ToIdentifier());
1811  if (prefab == null)
1812  {
1813  ThrowError($"No such behavior: {secondaryArgument}");
1814  return;
1815  }
1816 
1817  Pump random = pumps.GetRandomUnsynced();
1818  random.InfectBallast(prefab.Identifier, allowMultiplePerShip: true);
1819  NewMessage($"Infected {random.Name} with {prefab.Identifier} in {random.Item.CurrentHull.DisplayName}.", Color.Green);
1820  return;
1821  }
1822 
1823  ThrowError("No available pumps to infect on this submarine.");
1824  }
1825 
1826  if (primaryAction.Equals("growthwarp", StringComparison.OrdinalIgnoreCase))
1827  {
1828  if (int.TryParse(secondaryArgument, out int value))
1829  {
1830  foreach (Hull hull in Hull.HullList.Where(h => h.BallastFlora != null))
1831  {
1832  BallastFloraBehavior bs = hull.BallastFlora;
1833  bs.GrowthWarps = value;
1834  }
1835 
1836  NewMessage("Accelerating growth...", Color.Green);
1837  return;
1838  }
1839 
1840  ThrowError($"Invalid integer \"{secondaryArgument}\".");
1841  }
1842  }, isCheat: true, getValidArgs: () =>
1843  {
1844  string[] primaries = { "infect", "growthwarp" };
1845  string[] identifiers = BallastFloraPrefab.Prefabs.Select(bfp => bfp.Identifier).Distinct().Select(i => i.Value).ToArray();
1846  return new[] { primaries, identifiers };
1847  }));
1848 
1849  commands.Add(new Command("setdifficulty|forcedifficulty", "difficulty [0-100]. Leave the parameter empty to disable.", (string[] args) =>
1850  {
1851  if (args.Length == 0)
1852  {
1853  Level.ForcedDifficulty = null;
1854  NewMessage($"Forced difficulty level disabled.", Color.Green);
1855  }
1856  else if (float.TryParse(args[0], out float difficulty))
1857  {
1858  Level.ForcedDifficulty = difficulty;
1859  NewMessage($"Set the difficulty level to { Level.ForcedDifficulty }.", Color.Yellow);
1860  }
1861  }, isCheat: true));
1862 
1863  commands.Add(new Command("difficulty|leveldifficulty", "difficulty [0-100]: Change the level difficulty setting in the server lobby.", null));
1864 
1865  commands.Add(new Command("autoitemplacerdebug|outfitdebug", "autoitemplacerdebug: Toggle automatic item placer debug info on/off. The automatically placed items are listed in the debug console at the start of a round.", (string[] args) =>
1866  {
1867  AutoItemPlacer.OutputDebugInfo = !AutoItemPlacer.OutputDebugInfo;
1868  NewMessage((AutoItemPlacer.OutputDebugInfo ? "Enabled" : "Disabled") + " automatic item placer logging.", Color.White);
1869  }, isCheat: false));
1870 
1871  commands.Add(new Command("verboselogging", "verboselogging: Toggle verbose console logging on/off. When on, additional debug information is written to the debug console.", (string[] args) =>
1872  {
1873  var config = GameSettings.CurrentConfig;
1874  config.VerboseLogging = !GameSettings.CurrentConfig.VerboseLogging;
1875  GameSettings.SetCurrentConfig(config);
1876  NewMessage((GameSettings.CurrentConfig.VerboseLogging ? "Enabled" : "Disabled") + " verbose logging.", Color.White);
1877  }, isCheat: false));
1878 
1879  commands.Add(new Command("listtasks", "listtasks: Lists all asynchronous tasks currently in the task pool.", (string[] args) => { TaskPool.ListTasks(line => DebugConsole.NewMessage(line)); }));
1880 
1881  commands.Add(new Command("listcoroutines", "listcoroutines: Lists all coroutines currently running.", (string[] args) => { CoroutineManager.ListCoroutines(); }));
1882 
1883  commands.Add(new Command("calculatehashes", "calculatehashes [content package name]: Show the MD5 hashes of the files in the selected content package. If the name parameter is omitted, the first content package is selected.", (string[] args) =>
1884  {
1885  if (args.Length > 0)
1886  {
1887  string packageName = string.Join(" ", args);
1888  var package = ContentPackageManager.EnabledPackages.All.FirstOrDefault(p => p.Name.Equals(packageName, StringComparison.OrdinalIgnoreCase));
1889  if (package == null)
1890  {
1891  ThrowError("Content package \"" + packageName + "\" not found.");
1892  }
1893  else
1894  {
1895  package.CalculateHash(logging: true);
1896  }
1897  }
1898  else
1899  {
1900  ContentPackageManager.EnabledPackages.Core.CalculateHash(logging: true);
1901  }
1902  },
1903  () =>
1904  {
1905  return new string[][]
1906  {
1907  ContentPackageManager.EnabledPackages.All.Select(cp => cp.Name).ToArray()
1908  };
1909  }));
1910 
1911  commands.Add(new Command("simulatedlatency", "simulatedlatency [minimumlatencyseconds] [randomlatencyseconds]: applies a simulated latency to network messages. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) =>
1912  {
1913  if (args.Count() < 2 || (GameMain.NetworkMember == null)) return;
1914  if (!float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float minimumLatency))
1915  {
1916  ThrowError(args[0] + " is not a valid latency value.");
1917  return;
1918  }
1919  if (!float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float randomLatency))
1920  {
1921  ThrowError(args[1] + " is not a valid latency value.");
1922  return;
1923  }
1924  if (GameMain.NetworkMember != null)
1925  {
1926  GameMain.NetworkMember.SimulatedMinimumLatency = minimumLatency;
1927  GameMain.NetworkMember.SimulatedRandomLatency = randomLatency;
1928  }
1929  NewMessage("Set simulated minimum latency to " + minimumLatency.ToString(CultureInfo.InvariantCulture) + " and random latency to " + randomLatency.ToString(CultureInfo.InvariantCulture) + ".", Color.White);
1930  }));
1931 
1932  commands.Add(new Command("simulatedloss", "simulatedloss [lossratio]: applies simulated packet loss to network messages. For example, a value of 0.1 would mean 10% of the packets are dropped. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) =>
1933  {
1934  if (args.Count() < 1 || (GameMain.NetworkMember == null)) return;
1935  if (!float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float loss))
1936  {
1937  ThrowError(args[0] + " is not a valid loss ratio.");
1938  return;
1939  }
1940  if (GameMain.NetworkMember != null)
1941  {
1942  GameMain.NetworkMember.SimulatedLoss = loss;
1943  }
1944  NewMessage("Set simulated packet loss to " + (int)(loss * 100) + "%.", Color.White);
1945  }));
1946  commands.Add(new Command("simulatedduplicateschance", "simulatedduplicateschance [duplicateratio]: simulates packet duplication in network messages. For example, a value of 0.1 would mean there's a 10% chance a packet gets sent twice. Useful for simulating real network conditions when testing the multiplayer locally.", (string[] args) =>
1947  {
1948  if (args.Count() < 1 || (GameMain.NetworkMember == null)) return;
1949  if (!float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float duplicates))
1950  {
1951  ThrowError(args[0] + " is not a valid duplicate ratio.");
1952  return;
1953  }
1954  if (GameMain.NetworkMember != null)
1955  {
1956  GameMain.NetworkMember.SimulatedDuplicatesChance = duplicates;
1957  }
1958  NewMessage("Set packet duplication to " + (int)(duplicates * 100) + "%.", Color.White);
1959  }));
1960 
1961 #if DEBUG
1962  commands.Add(new Command("debugvoip", "Toggle the server writing VOIP into audio files.", null, isCheat: false));
1963 
1964  commands.Add(new Command("simulatedlongloadingtime", "simulatedlongloadingtime [minimum loading time]: forces loading a round to take at least the specified amount of seconds.", (string[] args) =>
1965  {
1966  if (args.Count() < 1 || (GameMain.NetworkMember == null)) return;
1967  if (!float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float time))
1968  {
1969  ThrowError(args[0] + " is not a valid duration ratio.");
1970  return;
1971  }
1972  GameSession.MinimumLoadingTime = time;
1973  NewMessage("Set minimum loading time to " + time + " seconds.", Color.White);
1974  }));
1975 
1976 
1977  commands.Add(new Command("resetcharacternetstate", "resetcharacternetstate [character name]: A debug-only command that resets a character's network state, intended for diagnosing character syncing issues.", null,
1978  () =>
1979  {
1980  if (GameMain.NetworkMember == null) { return null; }
1981  return new string[][]
1982  {
1983  Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1984  };
1985  }));
1986 
1987  commands.Add(new Command("storeinfo", "", (string[] args) =>
1988  {
1989  if (GameMain.GameSession?.Map?.CurrentLocation is Location location)
1990  {
1991  if (location.Stores != null)
1992  {
1993  var msg = "--- Location: " + location.DisplayName + " ---";
1994  foreach (var store in location.Stores)
1995  {
1996  msg += $"\nStore identifier: {store.Value.Identifier}";
1997  msg += $"\nBalance: {store.Value.Balance}";
1998  msg += $"\nPrice modifier: {store.Value.PriceModifier}%";
1999  msg += "\nDaily specials:";
2000  store.Value.DailySpecials.ForEach(i => msg += $"\n - {i.Name}");
2001  msg += "\nRequested goods:";
2002  store.Value.RequestedGoods.ForEach(i => msg += $"\n - {i.Name}");
2003 
2004  }
2005  NewMessage(msg);
2006  }
2007  else
2008  {
2009  NewMessage($"No stores at {location}, can't show store info.");
2010  }
2011  }
2012  else
2013  {
2014  NewMessage("No current location set, can't show store info.");
2015  }
2016  }));
2017 #endif
2018 
2019  commands.Add(new Command("startitems|startitemset", "start item set identifier", (string[] args) =>
2020  {
2021  if (args.Length == 0)
2022  {
2023  ThrowError($"No start item set identifier defined!");
2024  return;
2025  }
2026  AutoItemPlacer.DefaultStartItemSet = args[0].ToIdentifier();
2027  NewMessage($"Start item set changed to \"{AutoItemPlacer.DefaultStartItemSet}\"");
2028  }, isCheat: false));
2029 
2030  //"dummy commands" that only exist so that the server can give clients permissions to use them
2031  //TODO: alphabetical order?
2032  commands.Add(new Command("control", "control [character name]: Start controlling the specified character (client-only).", null, () =>
2033  {
2034  return new string[][] { ListCharacterNames() };
2035  }, isCheat: true));
2036  commands.Add(new Command("los", "Toggle the line of sight effect on/off (client-only).", null, isCheat: true));
2037  commands.Add(new Command("lighting|lights", "Toggle lighting on/off (client-only).", null, isCheat: true));
2038  commands.Add(new Command("ambientlight", "ambientlight [color]: Change the color of the ambient light in the level.", null, isCheat: true));
2039  commands.Add(new Command("debugdraw", "Toggle the debug drawing mode on/off (client-only).", null, isCheat: true));
2040  commands.Add(new Command("debugwiring", "Toggle the wiring debug mode on/off (client-only).", null, isCheat: true));
2041  commands.Add(new Command("debugdrawlocalization", "Toggle the localization debug drawing mode on/off (client-only). Colors all text that hasn't been fetched from a localization file magenta, making it easier to spot hard-coded or missing texts.", null, isCheat: false));
2042  commands.Add(new Command("debugdrawlos", "Toggle the los debug drawing mode on/off (client-only).", null, isCheat: true));
2043  commands.Add(new Command("togglevoicechatfilters", "Toggle the radio/muffle filters in the voice chat (client-only).", null, isCheat: false));
2044  commands.Add(new Command("togglehud|hud", "Toggle the character HUD (inventories, icons, buttons, etc) on/off (client-only).", null));
2045  commands.Add(new Command("toggleupperhud", "Toggle the upper part of the ingame HUD (chatbox, crewmanager) on/off (client-only).", null));
2046  commands.Add(new Command("toggleitemhighlights", "Toggle the item highlight effect on/off (client-only).", null));
2047  commands.Add(new Command("togglecharacternames", "Toggle the names hovering above characters on/off (client-only).", null));
2048  commands.Add(new Command("followsub", "Toggle whether the camera should follow the nearest submarine (client-only).", null));
2049  commands.Add(new Command("toggleaitargets|aitargets", "Toggle the visibility of AI targets (= targets that enemies can detect and attack/escape from) (client-only).", null, isCheat: true));
2050  commands.Add(new Command("debugai", "Toggle the ai debug mode on/off (works properly only in single player).", null, isCheat: true));
2051  commands.Add(new Command("devmode", "Toggle the dev mode on/off (client-only).", null, isCheat: true));
2052  commands.Add(new Command("showmonsters", "Permanently unlocks all the monsters in the character editor. Use \"hidemonsters\" to undo.", null, isCheat: true));
2053  commands.Add(new Command("hidemonsters", "Permanently hides in the character editor all the monsters that haven't been encountered in the game. Use \"showmonsters\" to undo.", null, isCheat: true));
2054 
2055  InitProjectSpecific();
2056 
2057  commands.Sort((c1, c2) => c1.Names.First().CompareTo(c2.Names.First()));
2058  }
2059 
2060  public static string AutoComplete(string command, int increment = 1)
2061  {
2062  string[] splitCommand = ToolBox.SplitCommand(command);
2063  string[] args = splitCommand.Skip(1).ToArray();
2064 
2065  //if an argument is given or the last character is a space, attempt to autocomplete the argument
2066  if (args.Length > 0 || (splitCommand.Length > 0 && command.Last() == ' '))
2067  {
2068  Command matchingCommand = commands.Find(c => c.Names.Contains(splitCommand[0].ToIdentifier()));
2069  if (matchingCommand?.GetValidArgs == null) { return command; }
2070 
2071  int autoCompletedArgIndex = args.Length > 0 && command.Last() != ' ' ? args.Length - 1 : args.Length;
2072 
2073  //get all valid arguments for the given command
2074  string[][] allArgs = matchingCommand.GetValidArgs();
2075  if (allArgs == null || allArgs.GetLength(0) < autoCompletedArgIndex + 1) { return command; }
2076 
2077  if (string.IsNullOrEmpty(currentAutoCompletedCommand))
2078  {
2079  currentAutoCompletedCommand = autoCompletedArgIndex > args.Length - 1 ? " " : args.Last();
2080  }
2081 
2082  //find all valid autocompletions for the given argument
2083  string[] validArgs = allArgs[autoCompletedArgIndex].Where(arg =>
2084  currentAutoCompletedCommand.Trim().Length <= arg.Length &&
2085  arg.Substring(0, currentAutoCompletedCommand.Trim().Length).ToLower() == currentAutoCompletedCommand.Trim().ToLower()).ToArray();
2086 
2087  // add all completions that contain the current argument, to the end of the list
2088  validArgs = validArgs.Concat(allArgs[autoCompletedArgIndex].Where(arg =>
2089  arg.ToLower().Contains(currentAutoCompletedCommand.Trim().ToLower()) &&
2090  !validArgs.Contains(arg))).ToArray();
2091 
2092  if (validArgs.Length == 0) { return command; }
2093 
2094  currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, validArgs.Length);
2095  string autoCompletedArg = validArgs[currentAutoCompletedIndex];
2096 
2097  //add quotation marks to args that contain spaces
2098  if (autoCompletedArg.Contains(' ')) autoCompletedArg = '"' + autoCompletedArg + '"';
2099  for (int i = 0; i < splitCommand.Length; i++)
2100  {
2101  if (splitCommand[i].Contains(' ')) splitCommand[i] = '"' + splitCommand[i] + '"';
2102  }
2103 
2104  return string.Join(" ", autoCompletedArgIndex >= args.Length ? splitCommand : splitCommand.Take(splitCommand.Length - 1)) + " " + autoCompletedArg;
2105  }
2106  else
2107  {
2108  if (string.IsNullOrWhiteSpace(currentAutoCompletedCommand))
2109  {
2110  currentAutoCompletedCommand = command;
2111  }
2112 
2113  List<Identifier> matchingCommands = new List<Identifier>();
2114  foreach (Command c in commands)
2115  {
2116  foreach (var name in c.Names)
2117  {
2118  if (currentAutoCompletedCommand.Length > name.Value.Length) { continue; }
2119  if (name.StartsWith(currentAutoCompletedCommand))
2120  {
2121  matchingCommands.Add(name);
2122  }
2123  }
2124  }
2125 
2126  if (matchingCommands.Count == 0) return command;
2127 
2128  currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, matchingCommands.Count);
2129  return matchingCommands[currentAutoCompletedIndex].Value;
2130  }
2131  }
2132 
2133  public static void ResetAutoComplete()
2134  {
2135  currentAutoCompletedCommand = "";
2136  currentAutoCompletedIndex = 0;
2137  }
2138 
2143  public static void ExecuteCommand(string inputtedCommands)
2144  {
2145  if (string.IsNullOrWhiteSpace(inputtedCommands) || inputtedCommands == "\\" || inputtedCommands == "\n") { return; }
2146 
2147  string[] commandsToExecute = inputtedCommands.Split("\n");
2148  foreach (string command in commandsToExecute)
2149  {
2150  if (activeQuestionCallback != null)
2151  {
2152 #if CLIENT
2153  activeQuestionText = null;
2154 #endif
2155  NewCommand(command);
2156  //reset the variable before invoking the delegate because the method may need to activate another question
2157  var temp = activeQuestionCallback;
2158  activeQuestionCallback = null;
2159  temp(command);
2160  return;
2161  }
2162 
2163  if (string.IsNullOrWhiteSpace(command) || command == "\\") { return; }
2164 
2165  string[] splitCommand = ToolBox.SplitCommand(command);
2166  if (splitCommand.Length == 0)
2167  {
2168  ThrowError("Failed to execute command \"" + command + "\"!");
2169  GameAnalyticsManager.AddErrorEventOnce(
2170  "DebugConsole.ExecuteCommand:LengthZero",
2171  GameAnalyticsManager.ErrorSeverity.Error,
2172  "Failed to execute command \"" + command + "\"!");
2173  return;
2174  }
2175 
2176  Identifier firstCommand = splitCommand[0].ToIdentifier();
2177 
2178  if (firstCommand != "admin")
2179  {
2180  NewCommand(command);
2181  }
2182 
2183 #if CLIENT
2184  if (GameMain.Client != null)
2185  {
2186  Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand));
2187  if (matchingCommand == null)
2188  {
2189  //if the command is not defined client-side, we'll relay it anyway because it may be a custom command at the server's side
2190  GameMain.Client.SendConsoleCommand(command);
2191  NewMessage("Server command: " + command, Color.Cyan);
2192  return;
2193  }
2194  else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
2195  {
2196  if (matchingCommand.RelayToServer)
2197  {
2198  GameMain.Client.SendConsoleCommand(command);
2199  NewMessage("Server command: " + command, Color.Cyan);
2200  }
2201  else
2202  {
2203  matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray());
2204  }
2205  return;
2206  }
2207  if (!IsCommandPermitted(firstCommand, GameMain.Client))
2208  {
2209 #if DEBUG
2210  AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build.");
2211 #else
2212  ThrowError($"You're not permitted to use the command \"{firstCommand}\"!");
2213  return;
2214 #endif
2215  }
2216  }
2217 #endif
2218 
2219  bool commandFound = false;
2220  foreach (Command c in commands)
2221  {
2222  if (!c.Names.Contains(firstCommand)) { continue; }
2223  c.Execute(splitCommand.Skip(1).ToArray());
2224  commandFound = true;
2225  break;
2226  }
2227 
2228  if (!commandFound)
2229  {
2230  ThrowError("Command \"" + splitCommand[0] + "\" not found.");
2231  }
2232  }
2233  }
2234 
2235  private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name).Select(c => c.Name).Distinct().ToArray();
2236 
2237  private static string[] ListAvailableLocations()
2238  {
2239  List<string> locationNames = new();
2240  foreach(var submarine in Submarine.Loaded)
2241  {
2242  locationNames.Add(submarine.Info.Name);
2243  }
2244 
2245  if (Level.Loaded != null)
2246  {
2247  foreach (var cave in Level.Loaded.Caves)
2248  {
2249  string caveName = cave.CaveGenerationParams.Name;
2250  // add index in case there are duplicate names
2251  int index = 1;
2252  while (locationNames.Contains($"{caveName}_{index}"))
2253  {
2254  index++;
2255  }
2256  locationNames.Add($"{caveName}_{index}");
2257  }
2258  }
2259 
2260  return locationNames.ToArray();
2261  }
2262 
2263  private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition)
2264  {
2265  if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase))
2266  {
2267  var randomWaypoint = GetRandomWaypoint(mainSub.GetWaypoints(alsoFromConnectedSubs:false));
2268  if (randomWaypoint != null)
2269  {
2270  teleportPosition = randomWaypoint.WorldPosition;
2271  return true;
2272  }
2273  LogError("No waypoints found in the main sub!");
2274  }
2275 
2276  foreach (var submarine in Submarine.Loaded)
2277  {
2278  if (string.Equals(submarine.Info.Name, locationName, StringComparison.InvariantCultureIgnoreCase))
2279  {
2280  var randomWaypoint = GetRandomWaypoint(submarine.GetWaypoints(alsoFromConnectedSubs:false));
2281  if (randomWaypoint != null)
2282  {
2283  teleportPosition = randomWaypoint.WorldPosition;
2284  return true;
2285  }
2286  LogError($"No waypoints found in sub {submarine.Info.Name}!");
2287  }
2288  }
2289 
2290  if (Level.Loaded is Level loadedLevel)
2291  {
2292  (string locationNameNoIndex, int locationIndex) = SplitIndex(locationName);
2293  int caveIndex = 1;
2294  foreach (var cave in loadedLevel.Caves)
2295  {
2296  if (string.Equals(cave.CaveGenerationParams.Name, locationNameNoIndex, StringComparison.InvariantCultureIgnoreCase))
2297  {
2298  if (caveIndex != locationIndex)
2299  {
2300  caveIndex++;
2301  continue;
2302  }
2303 
2304  var randomWaypoint = GetRandomWaypoint(cave.Tunnels.GetRandom(Rand.RandSync.Unsynced).WayPoints);
2305  if (randomWaypoint != null)
2306  {
2307  teleportPosition = randomWaypoint.WorldPosition;
2308  return true;
2309  }
2310  LogError($"No waypoints found in cave {cave.CaveGenerationParams.Name}!");
2311  }
2312  }
2313  }
2314  teleportPosition = Vector2.Zero;
2315  return false;
2316 
2317  WayPoint GetRandomWaypoint(IReadOnlyList<WayPoint> waypoints)
2318  {
2319  if (waypoints.None())
2320  {
2321  return null;
2322  }
2323 
2324  if (waypoints.Any(point => point.SpawnType == SpawnType.Human))
2325  {
2326  return waypoints.GetRandom(point => point.SpawnType == SpawnType.Human, Rand.RandSync.Unsynced);
2327  }
2328 
2329  if (waypoints.Any(point => point.SpawnType == SpawnType.Path))
2330  {
2331  return waypoints.GetRandom(point => point.SpawnType == SpawnType.Path, Rand.RandSync.Unsynced);
2332  }
2333 
2334  return waypoints.GetRandom(Rand.RandSync.Unsynced);
2335  }
2336 
2337  (string, int) SplitIndex(string caveName)
2338  {
2339  string[] splitName = caveName.Split('_');
2340  if (splitName.Length == 1)
2341  {
2342  return (splitName[0], -1);
2343  }
2344  else
2345  {
2346  return (splitName[0], int.Parse(splitName[1]));
2347  }
2348  }
2349  }
2350 
2351  private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null, bool botsOnly = false)
2352  {
2353  if (args.Length == 0) return null;
2354 
2355  string characterName;
2356  if (int.TryParse(args.Last(), out int characterIndex) && args.Length > 1)
2357  {
2358  characterName = string.Join(" ", args.Take(args.Length - 1)).ToLowerInvariant();
2359  }
2360  else
2361  {
2362  characterName = string.Join(" ", args).ToLowerInvariant();
2363  characterIndex = -1;
2364  }
2365 
2366  var matchingCharacters = Character.CharacterList.FindAll(c =>
2367  c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) &&
2368  (!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c));
2369 
2370  if (botsOnly)
2371  {
2372  matchingCharacters = matchingCharacters.FindAll(c => c is AICharacter);
2373  }
2374 
2375  if (!matchingCharacters.Any())
2376  {
2377  NewMessage("Character \""+ characterName + "\" not found", Color.Red);
2378  return null;
2379  }
2380 
2381  // Use same sorting as DebugConsole.ListCharacterNames() above
2382  matchingCharacters = matchingCharacters.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ToList();
2383  if (characterIndex == -1)
2384  {
2385  if (matchingCharacters.Count > 1)
2386  {
2387  NewMessage(
2388  "Found multiple matching characters. " +
2389  "Use \"[charactername] [0-" + (matchingCharacters.Count - 1) + "]\" to choose a specific character.",
2390  Color.LightGray);
2391  }
2392  return matchingCharacters[0];
2393  }
2394  else if (characterIndex < 0 || characterIndex >= matchingCharacters.Count)
2395  {
2396  ThrowError("Character index out of range. Select an index between 0 and " + (matchingCharacters.Count - 1));
2397  }
2398  else
2399  {
2400  return matchingCharacters[characterIndex];
2401  }
2402 
2403  return null;
2404  }
2405 
2406  private static void TeleportCharacter(Vector2 cursorWorldPos, Character controlledCharacter, string[] args)
2407  {
2408  if (Screen.Selected != GameMain.GameScreen)
2409  {
2410  NewMessage("Cannot teleport a character in the menu or the editor screens.", color: Color.Yellow);
2411  return;
2412  }
2413 
2414  Character targetCharacter = controlledCharacter;
2415  Vector2 worldPosition = cursorWorldPos;
2416  string locationNameArgument = "";
2417 
2418  var availableLocations = ListAvailableLocations();
2419  if (args.Length > 0)
2420  {
2421  if (args.Length > 1)
2422  {
2423  // remove location name from args
2424  if (availableLocations.Contains(args.Last())
2425  || string.Equals(args.Last(), "mainsub", StringComparison.InvariantCultureIgnoreCase))
2426  {
2427  locationNameArgument = args.Last();
2428  args = args.Take(args.Length - 1).ToArray();
2429  }
2430  else
2431  {
2432  NewMessage("Invalid arguments", color: Color.Yellow);
2433  return;
2434  }
2435  }
2436 
2437  // the remaining args should be the character name and a possible index
2438  if (args[0].ToLowerInvariant() != "me")
2439  {
2440  Character match = FindMatchingCharacter(args, ignoreRemotePlayers:false);
2441  targetCharacter = match;
2442  }
2443  }
2444 
2445  if (!string.IsNullOrWhiteSpace(locationNameArgument))
2446  {
2447  if (TryFindTeleportPosition(locationNameArgument, out Vector2 teleportPosition))
2448  {
2449  worldPosition = teleportPosition;
2450  }
2451  else
2452  {
2453  ThrowError($"No teleport position for location \"{locationNameArgument}\" was found.");
2454  return;
2455  }
2456  }
2457 
2458  if (targetCharacter != null)
2459  {
2460  targetCharacter.TeleportTo(worldPosition);
2461  }
2462  else
2463  {
2464  NewMessage("Invalid arguments", color: Color.Yellow);
2465  }
2466  }
2467 
2468  public static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, out string errorMsg)
2469  {
2470  errorMsg = "";
2471  if (args.Length == 0) { return; }
2472 
2473  Character spawnedCharacter = null;
2474 
2475  Vector2 spawnPosition = Vector2.Zero;
2476  WayPoint spawnPoint = null;
2477 
2478  string characterLowerCase = args[0].ToLowerInvariant();
2479  JobPrefab job = null;
2480  if (!JobPrefab.Prefabs.ContainsKey(characterLowerCase))
2481  {
2482  job = JobPrefab.Prefabs.Find(jp => jp.Name != null && jp.Name.Equals(characterLowerCase, StringComparison.OrdinalIgnoreCase));
2483  }
2484  else
2485  {
2486  job = JobPrefab.Prefabs[characterLowerCase];
2487  }
2488  bool isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
2489  bool addToCrew = false;
2490  if (args.Length > 1)
2491  {
2492  switch (args[1].ToLowerInvariant())
2493  {
2494  case "inside":
2495  spawnPoint = WayPoint.GetRandom(SpawnType.Human, job, Submarine.MainSub);
2496  break;
2497  case "outside":
2498  spawnPoint = WayPoint.GetRandom(SpawnType.Enemy);
2499  break;
2500  case "near":
2501  case "close":
2502  float closestDist = -1.0f;
2503  foreach (WayPoint wp in WayPoint.WayPointList)
2504  {
2505  if (wp.Submarine != null) continue;
2506 
2507  //don't spawn inside hulls
2508  if (Hull.FindHull(wp.WorldPosition, null) != null) continue;
2509 
2510  float dist = Vector2.Distance(wp.WorldPosition, GameMain.GameScreen.Cam.WorldViewCenter);
2511 
2512  if (closestDist < 0.0f || dist < closestDist)
2513  {
2514  spawnPoint = wp;
2515  closestDist = dist;
2516  }
2517  }
2518  break;
2519  case "cursor":
2520  spawnPosition = cursorWorldPos;
2521  break;
2522  default:
2523  spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
2524  break;
2525  }
2526  addToCrew =
2527  args.Length > 3 ?
2528  args[3].Equals("true", StringComparison.OrdinalIgnoreCase) :
2529  isHuman;
2530  }
2531  else
2532  {
2533  spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
2534  }
2535 
2536  if (string.IsNullOrWhiteSpace(args[0])) { return; }
2537  CharacterTeamType teamType = Character.Controlled != null ? Character.Controlled.TeamID : CharacterTeamType.Team1;
2538  if (args.Length > 2)
2539  {
2540  try
2541  {
2542  teamType = (CharacterTeamType)int.Parse(args[2]);
2543  }
2544  catch
2545  {
2546  ThrowError($"\"{args[2]}\" is not a valid team id.");
2547  }
2548  }
2549 
2550  if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; }
2551 
2552  if (isHuman)
2553  {
2554  var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0;
2555  CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
2556  spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8));
2557 
2558  spawnedCharacter.GiveJobItems(spawnPoint);
2559  spawnedCharacter.GiveIdCardTags(spawnPoint);
2560  spawnedCharacter.Info.StartItemsGiven = true;
2561  }
2562  else
2563  {
2564  CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier());
2565  if (prefab != null)
2566  {
2567  CharacterInfo characterInfo = null;
2568  if (prefab.HasCharacterInfo)
2569  {
2570  characterInfo = new CharacterInfo(prefab.Identifier);
2571  }
2572  spawnedCharacter = Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8), characterInfo);
2573  }
2574  }
2575  if (addToCrew && GameMain.GameSession != null)
2576  {
2577  spawnedCharacter.TeamID = teamType;
2578 #if CLIENT
2579  GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
2580 #endif
2581  }
2582  }
2583 
2584  public static void SpawnItem(string[] args, Vector2 cursorPos, Character controlledCharacter, out string errorMsg)
2585  {
2586  errorMsg = "";
2587  if (args.Length < 1) return;
2588 
2589  Vector2? spawnPos = null;
2590  Inventory spawnInventory = null;
2591 
2592  string itemNameOrId = args[0].ToLowerInvariant();
2593  ItemPrefab itemPrefab =
2594  (MapEntityPrefab.FindByName(itemNameOrId) ??
2595  MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab;
2596  if (itemPrefab == null)
2597  {
2598  errorMsg = "Item \"" + itemNameOrId + "\" not found!";
2599  var matching = ItemPrefab.Prefabs.Find(me => me.Name.StartsWith(itemNameOrId, StringComparison.OrdinalIgnoreCase) && me is ItemPrefab);
2600  if (matching != null)
2601  {
2602  errorMsg += $" Did you mean \"{matching.Name}\"?";
2603  if (matching.Name.Contains(" "))
2604  {
2605  errorMsg += $" Please note that you should surround multi-word names with quotation marks (e.q. spawnitem \"{matching.Name}\")";
2606  }
2607  }
2608  return;
2609  }
2610 
2611  int amount = 1;
2612  if (args.Length > 1)
2613  {
2614  string spawnLocation = args.Last();
2615  if (args.Length > 2)
2616  {
2617  spawnLocation = args[^2];
2618  if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
2619  amount = Math.Min(amount, 100);
2620  }
2621 
2622  switch (spawnLocation)
2623  {
2624  case "cursor":
2625  spawnPos = cursorPos;
2626  break;
2627  case "inventory":
2628  spawnInventory = controlledCharacter?.Inventory;
2629  break;
2630  case "cargo":
2631  var wp = WayPoint.GetRandom(SpawnType.Cargo, null, Submarine.MainSub);
2632  spawnPos = wp == null ? Vector2.Zero : wp.WorldPosition;
2633  break;
2634  case "random":
2635  //Dont do a thing, random is basically Human points anyways - its in the help description.
2636  break;
2637  default:
2638  var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray());
2639  if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; }
2640  break;
2641  }
2642  }
2643 
2644  if ((spawnPos == null || spawnPos == Vector2.Zero) && spawnInventory == null)
2645  {
2646  var wp = WayPoint.GetRandom(SpawnType.Human, null, Submarine.MainSub);
2647  spawnPos = wp == null ? Vector2.Zero : wp.WorldPosition;
2648  }
2649 
2650  for (int i = 0; i < amount; i++)
2651  {
2652  if (spawnPos != null)
2653  {
2654  if (Entity.Spawner == null || Entity.Spawner.Removed)
2655  {
2656  new Item(itemPrefab, spawnPos.Value, null);
2657  }
2658  else
2659  {
2660  Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value);
2661  }
2662  }
2663  else if (spawnInventory != null)
2664  {
2665  if (Entity.Spawner == null)
2666  {
2667  var spawnedItem = new Item(itemPrefab, Vector2.Zero, null);
2668  spawnInventory.TryPutItem(spawnedItem, null, spawnedItem.AllowedSlots);
2669  onItemSpawned(spawnedItem);
2670  }
2671  else
2672  {
2673  Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned);
2674  }
2675 
2676  static void onItemSpawned(Item item)
2677  {
2678  if (item.ParentInventory?.Owner is Character character)
2679  {
2680  foreach (WifiComponent wifiComponent in item.GetComponents<WifiComponent>())
2681  {
2682  wifiComponent.TeamID = character.TeamID;
2683  }
2684  }
2685  }
2686  }
2687  }
2688  }
2689 
2694  public static void AddSafeError(string error)
2695  {
2696 #if DEBUG
2697  DebugConsole.ThrowError(error);
2698 #else
2699  DebugConsole.LogError(error);
2700 #endif
2701  }
2702 
2703  public static void LogError(string msg, Color? color = null, ContentPackage contentPackage = null)
2704  {
2705  msg = AddContentPackageInfoToMessage(msg, contentPackage);
2706  color ??= Color.Red;
2707  NewMessage(msg, color.Value, isCommand: false, isError: true);
2708  }
2709 
2710  public static void NewCommand(string command, Color? color = null)
2711  {
2712  color ??= Color.White;
2713  NewMessage(command, color.Value, isCommand: true, isError: false);
2714  }
2715 
2716  public static void NewMessage(LocalizedString msg, Color? color = null, bool debugOnly = false)
2717  => NewMessage(msg.Value, color, debugOnly);
2718 
2719  public static void NewMessage(string msg, Color? color = null, bool debugOnly = false)
2720  {
2721  color ??= Color.White;
2722  if (debugOnly)
2723  {
2724 #if DEBUG
2725  NewMessage(msg, color.Value, isCommand: false, isError: false);
2726 #endif
2727  }
2728  else
2729  {
2730  NewMessage(msg, color.Value, isCommand: false, isError: false);
2731  }
2732 #if DEBUG && CLIENT
2733  Console.WriteLine(msg);
2734 #endif
2735  }
2736 
2737  private static void NewMessage(string msg, Color color, bool isCommand, bool isError)
2738  {
2739  if (string.IsNullOrEmpty(msg)) { return; }
2740 
2741  var newMsg = new ColoredText(msg, color, isCommand, isError);
2742  queuedMessages.Enqueue(newMsg);
2743  MessageHandler.Invoke(newMsg);
2744  }
2745 
2746  public static void ShowQuestionPrompt(string question, QuestionCallback onAnswered, string[] args = null, int argCount = -1)
2747  {
2748  if (args != null && args.Length > argCount)
2749  {
2750  onAnswered(args[argCount]);
2751  return;
2752  }
2753 
2754 #if CLIENT
2755  activeQuestionText = new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, 0), listBox.Content.RectTransform),
2756  " >>" + question, font: GUIStyle.SmallFont, wrap: true)
2757  {
2758  CanBeFocused = false,
2759  TextColor = Color.Cyan
2760  };
2761 #else
2762  NewMessage(" >>" + question, Color.Cyan);
2763 #endif
2764  activeQuestionCallback += onAnswered;
2765  }
2766 
2767  private static bool TryParseTimeSpan(string s, out TimeSpan timeSpan)
2768  {
2769  timeSpan = new TimeSpan();
2770  if (string.IsNullOrWhiteSpace(s)) return false;
2771 
2772  string currNum = "";
2773  foreach (char c in s)
2774  {
2775  if (char.IsDigit(c))
2776  {
2777  currNum += c;
2778  }
2779  else if (char.IsWhiteSpace(c))
2780  {
2781  continue;
2782  }
2783  else
2784  {
2785  if (!int.TryParse(currNum, out int parsedNum) || parsedNum < 0)
2786  {
2787  return false;
2788  }
2789  try
2790  {
2791  switch (c)
2792  {
2793  case 'd':
2794  timeSpan += new TimeSpan(parsedNum, 0, 0, 0, 0);
2795  break;
2796  case 'h':
2797  timeSpan += new TimeSpan(0, parsedNum, 0, 0, 0);
2798  break;
2799  case 'm':
2800  timeSpan += new TimeSpan(0, 0, parsedNum, 0, 0);
2801  break;
2802  case 's':
2803  timeSpan += new TimeSpan(0, 0, 0, parsedNum, 0);
2804  break;
2805  default:
2806  return false;
2807  }
2808  }
2809  catch (ArgumentOutOfRangeException)
2810  {
2811  ThrowError($"{parsedNum} {c} exceeds the maximum supported time span. Using the maximum time span {TimeSpan.MaxValue} instead.");
2812  timeSpan = TimeSpan.MaxValue;
2813  return true;
2814  }
2815  currNum = "";
2816  }
2817  }
2818 
2819  return true;
2820  }
2821 
2822  public static Command FindCommand(string commandName) => commands.Find(c => c.Names.Contains(commandName.ToIdentifier()));
2823 
2824  public static void Log(LocalizedString message) => Log(message?.Value);
2825 
2826  public static void Log(string message)
2827  {
2828  if (GameSettings.CurrentConfig.VerboseLogging)
2829  {
2830  NewMessage(message, Color.Gray);
2831  }
2832  }
2833 
2834  public static void ThrowErrorLocalized(LocalizedString error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false)
2835  {
2836  ThrowError(error.Value, e, contentPackage, createMessageBox, appendStackTrace);
2837  }
2838 
2839  public static void ThrowError(string error, Exception e = null, ContentPackage contentPackage = null, bool createMessageBox = false, bool appendStackTrace = false)
2840  {
2841  error = AddContentPackageInfoToMessage(error, contentPackage);
2842  if (e != null)
2843  {
2844  error += " {" + e.Message + "}\n";
2845  if (e.StackTrace != null)
2846  {
2847  error += e.StackTrace.CleanupStackTrace();
2848  }
2849  if (e.InnerException != null)
2850  {
2851  var innermost = e.GetInnermost();
2852  error += "\n\nInner exception: " + innermost.Message + "\n";
2853  if (innermost.StackTrace != null)
2854  {
2855  error += innermost.StackTrace.CleanupStackTrace();
2856  }
2857  }
2858  }
2859  else if (appendStackTrace && Environment.StackTrace != null)
2860  {
2861  error += "\n" + Environment.StackTrace.CleanupStackTrace();
2862  }
2863  System.Diagnostics.Debug.WriteLine($"ThrowError: {error}");
2864 
2865 #if CLIENT
2866  if (createMessageBox)
2867  {
2868  CoroutineManager.StartCoroutine(CreateMessageBox(error));
2869  }
2870  else
2871  {
2872  isOpen = true;
2873  }
2874 #endif
2875 
2876  LogError(error);
2877  }
2878 
2879  public static void ThrowErrorAndLogToGA(string gaIdentifier, string errorMsg)
2880  {
2881  ThrowError(errorMsg);
2882  GameAnalyticsManager.AddErrorEventOnce(
2883  gaIdentifier,
2884  GameAnalyticsManager.ErrorSeverity.Error,
2885  errorMsg);
2886  }
2887 
2888  private static readonly HashSet<string> loggedErrorIdentifiers = new HashSet<string>();
2892  public static void ThrowErrorOnce(string identifier, string errorMsg, Exception e = null)
2893  {
2894  if (loggedErrorIdentifiers.Contains(identifier)) { return; }
2895  ThrowError(errorMsg, e);
2896  loggedErrorIdentifiers.Add(identifier);
2897  }
2898 
2899  public static void AddWarning(string warning, ContentPackage contentPackage = null)
2900  {
2901  warning = AddContentPackageInfoToMessage($"WARNING: {warning}", contentPackage);
2902  System.Diagnostics.Debug.WriteLine(warning);
2903  NewMessage(warning, Color.Yellow);
2904  }
2905 
2906  private static string AddContentPackageInfoToMessage(string message, ContentPackage contentPackage)
2907  {
2908  if (contentPackage == null) { return message; }
2909 #if CLIENT
2910  string color = XMLExtensions.ToStringHex(Color.MediumPurple);
2911  return $"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {message}";
2912 #else
2913  return $"[{contentPackage.Name}] {message}";
2914 #endif
2915  }
2916 
2917 #if CLIENT
2918  private static IEnumerable<CoroutineStatus> CreateMessageBox(string errorMsg)
2919  {
2920  new GUIMessageBox(TextManager.Get("Error"), errorMsg);
2921  yield return CoroutineStatus.Success;
2922  }
2923 #endif
2924 
2925  public static void SaveLogs()
2926  {
2927  if (unsavedMessages.Count == 0) return;
2928  if (!Directory.Exists(SavePath))
2929  {
2930  try
2931  {
2932  Directory.CreateDirectory(SavePath);
2933  }
2934  catch (Exception e)
2935  {
2936  ThrowError("Failed to create a folder for debug console logs", e);
2937  return;
2938  }
2939  }
2940 
2941  string fileName = "DebugConsoleLog_";
2942 #if SERVER
2943  fileName += "Server_";
2944 #else
2945  fileName += "Client_";
2946 #endif
2947 
2948  fileName += DateTime.Now.ToShortDateString() + "_" + DateTime.Now.ToShortTimeString();
2949  var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform();
2950  foreach (char invalidChar in invalidChars)
2951  {
2952  fileName = fileName.Replace(invalidChar.ToString(), "");
2953  }
2954 
2955  string filePath = Path.Combine(SavePath, fileName);
2956  if (File.Exists(filePath + ".txt"))
2957  {
2958  int fileNum = 2;
2959  while (File.Exists(filePath + " (" + fileNum + ")"))
2960  {
2961  fileNum++;
2962  }
2963  filePath = filePath + " (" + fileNum + ")";
2964  }
2965 
2966  try
2967  {
2968  File.WriteAllLines(filePath + ".txt", unsavedMessages.Select(l => "[" + l.Time + "] " + l.Text));
2969  }
2970  catch (Exception e)
2971  {
2972  unsavedMessages.Clear();
2973  ThrowError("Saving debug console log to " + filePath + " failed", e);
2974  }
2975  }
2976 
2977  public static void DeactivateCheats()
2978  {
2979 #if CLIENT
2980  GameMain.DebugDraw = false;
2981  GameMain.LightManager.LightingEnabled = true;
2982  Character.DebugDrawInteract = false;
2983 #endif
2984  Hull.EditWater = false;
2985  Hull.EditFire = false;
2986  EnemyAIController.DisableEnemyAI = false;
2987  HumanAIController.DisableCrewAI = false;
2988  }
2989  }
2990 }
readonly bool IsCheat
Using a command that's considered a cheat disables achievements
Command(string name, string help, Action< string[]> onExecute, Func< string[][]> getValidArgs=null, bool isCheat=false)
Use this constructor to create a command that executes the same action regardless of whether it's exe...
static GameSession?? GameSession
Definition: GameMain.cs:88
static NetworkMember NetworkMember
Definition: GameMain.cs:190
The base class for components holding the different functionalities of the item
void InfectBallast(Identifier identifier, bool allowMultiplePerShip=false)
static readonly List< PermissionPreset > List
ColoredText(string text, Color color, bool isCommand, bool isError)