4 using Microsoft.Xna.Framework;
6 using System.Collections.Concurrent;
7 using System.Collections.Generic;
8 using System.Collections.Immutable;
9 using System.ComponentModel;
10 using System.Globalization;
13 using System.Threading.Tasks;
22 public readonly
string Text;
27 public readonly
string Time;
29 public ColoredText(
string text, Color color,
bool isCommand,
bool isError)
33 this.IsCommand = isCommand;
34 this.IsError = isError;
36 Time = DateTime.Now.ToString(CultureInfo.InvariantCulture);
40 static partial class DebugConsole
45 public readonly ImmutableArray<Identifier>
Names;
46 public readonly
string Help;
60 public Command(
string name,
string help, Action<
string[]> onExecute, Func<
string[][]> getValidArgs =
null,
bool isCheat =
false)
62 Names = name.Split(
'|').ToIdentifiers().ToImmutableArray();
65 this.OnExecute = onExecute;
67 this.GetValidArgs = getValidArgs;
68 this.IsCheat = isCheat;
75 bool allowCheats =
false;
79 if (!allowCheats && !CheatsEnabled &&
IsCheat)
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);
92 return Names.First().GetHashCode();
96 private static readonly ConcurrentQueue<ColoredText> queuedMessages
97 =
new ConcurrentQueue<ColoredText>();
99 public static readonly NamedEvent<ColoredText> MessageHandler =
new NamedEvent<ColoredText>();
103 private readonly List<ColoredText> errors;
104 private readonly
bool wasConsoleOpen;
105 private Identifier handlerId;
106 public IReadOnlyList<ColoredText>
Errors => errors;
110 this.handlerId = handlerId;
112 this.wasConsoleOpen = IsOpen;
114 this.wasConsoleOpen =
false;
116 this.errors =
new List<ColoredText>();
119 var errs = this.errors;
121 MessageHandler.Register(handlerId, msg =>
123 if (!msg.IsError) { return; }
129 =>
new ErrorCatcher(ToolBox.RandomSeed(25).ToIdentifier());
133 if (handlerId.IsEmpty) {
return; }
134 MessageHandler.Deregister(handlerId);
135 handlerId = Identifier.Empty;
137 DebugConsole.IsOpen = wasConsoleOpen;
142 static partial
void ShowHelpMessage(Command command);
144 const int MaxMessages = 300;
146 public static readonly List<ColoredText> Messages =
new List<ColoredText>();
148 public delegate
void QuestionCallback(
string answer);
149 private static QuestionCallback activeQuestionCallback;
151 private static readonly List<Command> commands =
new List<Command>();
152 public static List<Command> Commands
154 get {
return commands; }
157 private static string currentAutoCompletedCommand;
158 private static int currentAutoCompletedIndex;
160 public static bool CheatsEnabled;
162 private static readonly List<ColoredText> unsavedMessages =
new List<ColoredText>();
163 private static readonly
int messagesPerFile = 800;
164 public const string SavePath =
"ConsoleLogs";
166 public static void AssignOnExecute(
string names, Action<
string[]> onExecute)
168 var matchingCommand = commands.Find(c => c.Names.Intersect(names.Split(
'|').ToIdentifiers()).Any());
169 if (matchingCommand ==
null)
171 throw new Exception(
"AssignOnExecute failed. Command matching the name(s) \"" + names +
"\" not found.");
175 matchingCommand.OnExecute = onExecute;
179 static DebugConsole()
182 CheatsEnabled =
true;
184 commands.Add(
new Command(
"help",
"", (
string[] args) =>
186 if (args.Length == 0)
188 foreach (Command c in commands)
190 if (string.IsNullOrEmpty(c.Help)) continue;
196 var matchingCommand = commands.Find(c => c.Names.Any(name => name == args[0]));
197 if (matchingCommand ==
null)
199 NewMessage(
"Command " + args[0] +
" not found.", Color.Red);
203 ShowHelpMessage(matchingCommand);
209 return new string[][]
211 commands.SelectMany(c => c.Names).Select(n => n.Value).ToArray(),
212 Array.Empty<
string>()
216 void printMapEntityPrefabs<T>(IEnumerable<T> prefabs) where T : MapEntityPrefab
218 NewMessage(
"***************", Color.Cyan);
219 foreach (T prefab
in prefabs)
221 if (prefab.Name.IsNullOrEmpty()) {
continue; }
222 string text = $
"- {prefab.Name}";
223 if (prefab.Tags.Any())
225 text += $
" ({string.Join(",
", prefab.Tags)})";
227 if (prefab.AllowedLinks?.Any() ??
false)
229 text += $
", Links: {string.Join(",
", prefab.AllowedLinks)}";
231 NewMessage(text, prefab.ContentPackage == ContentPackageManager.VanillaCorePackage ? Color.Cyan : Color.Purple);
233 NewMessage(
"***************", Color.Cyan);
236 commands.Add(
new Command(
"items|itemlist",
"itemlist: List all the item prefabs available for spawning.", (
string[] args) =>
238 printMapEntityPrefabs(ItemPrefab.Prefabs);
241 commands.Add(
new Command(
"itemassemblies",
"itemassemblies: List all the item assemblies available for spawning.", (
string[] args) =>
243 printMapEntityPrefabs(ItemAssemblyPrefab.Prefabs);
247 commands.Add(
new Command(
"netstats",
"netstats: Toggles the visibility of the network statistics UI.", (
string[] args) =>
249 if (GameMain.NetworkMember ==
null)
return;
250 GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats;
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,
256 string[] creatureAndJobNames =
257 CharacterPrefab.Prefabs.Select(p => p.Identifier.Value)
258 .Concat(JobPrefab.Prefabs.Select(p => p.Identifier.Value))
262 return new string[][]
264 creatureAndJobNames.ToArray(),
265 new string[] {
"near",
"inside",
"outside",
"cursor" },
266 new string[] {
"0",
"1",
"2",
"3" },
267 new string[] {
"true",
"false" },
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\".",
278 SpawnItem(args, Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? PlayerInput.MousePosition,
Character.Controlled, out
string errorMsg);
280 SpawnItem(args, Vector2.Zero,
null, out
string errorMsg);
282 if (!
string.IsNullOrWhiteSpace(errorMsg))
284 ThrowError(errorMsg);
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());
296 List<string> itemNames =
new List<string>();
297 foreach (ItemPrefab itemPrefab
in ItemPrefab.Prefabs)
299 if (!itemNames.Contains(itemPrefab.Name.Value))
301 itemNames.Add(itemPrefab.Name.Value);
305 List<string> spawnPosParams =
new List<string>() {
"cursor",
"inventory" };
307 if (GameMain.Server !=
null) spawnPosParams.AddRange(GameMain.Server.ConnectedClients.Select(c => c.Name));
309 spawnPosParams.AddRange(
Character.CharacterList.Where(c => c.Inventory !=
null).Select(c => c.Name).Distinct());
311 return new string[][]
314 spawnPosParams.ToArray()
318 commands.Add(
new Command(
"disablecrewai",
"disablecrewai: Disable the AI of the NPCs in the crew.", (
string[] args) =>
320 HumanAIController.DisableCrewAI =
true;
321 NewMessage(
"Crew AI disabled", Color.Red);
324 commands.Add(
new Command(
"enablecrewai",
"enablecrewai: Enable the AI of the NPCs in the crew.", (
string[] args) =>
326 HumanAIController.DisableCrewAI =
false;
327 NewMessage(
"Crew AI enabled", Color.Green);
330 commands.Add(
new Command(
"disableenemyai",
"disableenemyai: Disable the AI of the Enemy characters (monsters).", (
string[] args) =>
332 EnemyAIController.DisableEnemyAI =
true;
333 NewMessage(
"Enemy AI disabled", Color.Red);
336 commands.Add(
new Command(
"enableenemyai",
"enableenemyai: Enable the AI of the Enemy characters (monsters).", (
string[] args) =>
338 EnemyAIController.DisableEnemyAI =
false;
339 NewMessage(
"Enemy AI enabled", Color.Green);
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,
345 return new string[][]
347 EventPrefab.Prefabs.Where(p => p is TraitorEventPrefab).Select(p => p.Identifier.ToString()).ToArray()
351 commands.Add(
new Command(
"botcount",
"botcount [x]: Set the number of bots in the crew in multiplayer.",
null));
353 commands.Add(
new Command(
"botspawnmode",
"botspawnmode [fill/normal]: Set how bots are spawned in the multiplayer.",
null));
355 commands.Add(
new Command(
"killdisconnectedtimer",
"killdisconnectedtimer [seconds]: Set the time after which disconnect players' characters get automatically killed.",
null));
357 commands.Add(
new Command(
"autorestart",
"autorestart [true/false]: Enable or disable round auto-restart.",
null));
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));
361 commands.Add(
new Command(
"autorestarttimer",
"autorestarttimer [seconds]: Set the current autorestart countdown to the specified value.",
null));
363 commands.Add(
new Command(
"startwhenclientsready",
"startwhenclientsready [true/false]: Enable or disable automatically starting the round when clients are ready to start.",
null));
365 commands.Add(
new Command(
"giveperm",
"giveperm [id/steamid/endpoint/name]: Grants administrative permissions to the specified client.",
null,
368 if (GameMain.NetworkMember ==
null)
return null;
370 return new string[][]
372 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
377 commands.Add(
new Command(
"revokeperm",
"revokeperm [id/steamid/endpoint/name]: Revokes administrative permissions from the specified client.",
null,
380 if (GameMain.NetworkMember ==
null)
return null;
382 return new string[][]
384 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
385 Enum.GetValues(typeof(
ClientPermissions)).Cast<ClientPermissions>().Select(v => v.ToString()).ToArray()
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,
392 if (GameMain.NetworkMember ==
null)
return null;
394 return new string[][]
396 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
401 commands.Add(
new Command(
"givecommandperm",
"givecommandperm [id/steamid/endpoint/name]: Gives the specified client the permission to use the specified console commands.",
null,
404 if (GameMain.NetworkMember ==
null)
return null;
406 return new string[][]
408 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
409 commands.Select(c => c.Names.First().Value).Union(
new []{
"All" }).ToArray()
413 commands.Add(
new Command(
"revokecommandperm",
"revokecommandperm [id/steamid/endpoint/name]: Revokes permission to use the specified console commands from the specified client.",
null,
416 if (GameMain.NetworkMember ==
null)
return null;
418 return new string[][]
420 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
421 commands.Select(c => c.Names.First().Value).Union(
new []{
"All" }).ToArray()
425 commands.Add(
new Command(
"showperm",
"showperm [id/steamid/endpoint/name]: Shows the current administrative permissions of the specified client.",
null,
428 if (GameMain.NetworkMember ==
null)
return null;
430 return new string[][]
432 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray()
436 commands.Add(
new Command(
"respawnnow",
"respawnnow: Trigger a respawn immediately if there are any clients waiting to respawn.",
null));
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,
443 if (GameMain.NetworkMember?.ConnectedClients ==
null) {
return null; }
444 return new string[][]
446 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray()
449 commands.Add(
new Command(
"setkarma",
"setkarma [client] [0-100]: Sets the karma of the specified client to the specified value.",
null,
452 if (GameMain.NetworkMember?.ConnectedClients ==
null) {
return null; }
453 return new string[][]
455 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
456 new string[] {
"50" }
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));
461 commands.Add(
new Command(
"kick",
"kick [name]: Kick a player out of the server.", (
string[] args) =>
463 if (GameMain.NetworkMember ==
null || args.Length == 0) {
return; }
465 string playerName =
string.Join(
" ", args);
467 ShowQuestionPrompt(
"Reason for kicking \"" + playerName +
"\"? (Enter c to cancel)", (reason) =>
469 if (reason ==
"c" || reason ==
"C") {
return; }
470 GameMain.NetworkMember.KickPlayer(playerName, reason);
475 if (GameMain.NetworkMember ==
null) {
return null; }
477 return new string[][]
479 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray()
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) =>
485 if (GameMain.NetworkMember ==
null || args.Length == 0)
return;
487 int.TryParse(args[0], out
int id);
488 var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId ==
id);
491 ThrowError(
"Client id \"" +
id +
"\" not found.");
495 ShowQuestionPrompt(
"Reason for kicking \"" + client.Name +
"\"? (Enter c to cancel)", (reason) =>
497 if (reason ==
"c" || reason ==
"C") { return; }
498 GameMain.NetworkMember.KickPlayer(client.Name, reason);
502 commands.Add(
new Command(
"ban",
"ban [name]: Kick and ban the player from the server.", (
string[] args) =>
504 if (GameMain.NetworkMember ==
null || args.Length == 0)
return;
506 string clientName =
string.Join(
" ", args);
507 ShowQuestionPrompt(
"Reason for banning \"" + clientName +
"\"? (Enter c to cancel)", (reason) =>
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) =>
512 if (duration ==
"c" || duration ==
"C") {
return; }
513 TimeSpan? banDuration =
null;
514 if (!
string.IsNullOrWhiteSpace(duration))
516 if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration))
518 ThrowError(
"\"" + duration +
"\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\".");
521 banDuration = parsedBanDuration;
524 GameMain.NetworkMember.BanPlayer(clientName, reason, banDuration);
530 if (GameMain.NetworkMember ==
null)
return null;
532 return new string[][]
534 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray()
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) =>
540 if (GameMain.NetworkMember ==
null || args.Length == 0)
return;
542 int.TryParse(args[0], out
int id);
543 var client = GameMain.NetworkMember.ConnectedClients.Find(c => c.SessionId ==
id);
546 ThrowError(
"Client id \"" +
id +
"\" not found.");
550 ShowQuestionPrompt(
"Reason for banning \"" + client.Name +
"\"? (Enter c to cancel)", (reason) =>
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) =>
555 if (duration ==
"c" || duration ==
"C") {
return; }
556 TimeSpan? banDuration =
null;
557 if (!
string.IsNullOrWhiteSpace(duration))
559 if (!TryParseTimeSpan(duration, out TimeSpan parsedBanDuration))
561 ThrowError(
"\"" + duration +
"\" is not a valid ban duration. Use the format \"[days] d [hours] h\", \"[days] d\" or \"[hours] h\".");
564 banDuration = parsedBanDuration;
567 GameMain.NetworkMember.BanPlayer(client.Name, reason, banDuration);
572 commands.Add(
new Command(
"banaddress|banip",
"banaddress [endpoint]: Ban the IP address/SteamID from the server.",
null));
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.",
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[][]
582 characterList.Concat(ListCharacterNames()).ToArray(),
583 subList.Concat(ListAvailableLocations()).ToArray()
587 commands.Add(
new Command(
"listlocations|locations",
"listlocations: List all the locations in the level: subs, outposts, ruins, caves.",
588 onExecute:(
string[] args) =>
590 var availableLocations = ListAvailableLocations();
591 NewMessage(
"***************", Color.Cyan);
592 foreach (var location
in availableLocations)
594 NewMessage(location, Color.Cyan);
596 NewMessage(
"***************", Color.Cyan);
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.",
602 Character targetCharacter = (args.Length == 0) ?
Character.Controlled : FindMatchingCharacter(args,
false);
604 if (targetCharacter ==
null) {
return; }
606 targetCharacter.GodMode = !targetCharacter.GodMode;
607 NewMessage((targetCharacter.GodMode ?
"Enabled godmode on " :
"Disabled godmode on ") + targetCharacter.Name, Color.White);
611 return new string[][] { ListCharacterNames() };
614 commands.Add(
new Command(
"godmode_mainsub",
"godmode_mainsub: Toggle submarine godmode. Makes the main submarine invulnerable to damage.", (
string[] args) =>
619 NewMessage(
Submarine.MainSub.GodMode ?
"Godmode on" :
"Godmode off", Color.White);
622 commands.Add(
new Command(
"growthdelay",
"growthdelay: Sets how long it takes for planters to attempt to advance a plant's growth.", (
string[] args) =>
624 if (args.Length > 0 &&
float.TryParse(args[0], out
float value))
626 Planter.GrowthTickDelay = value;
627 NewMessage($
"Growth delay set to {value}.", Color.Green);
630 NewMessage(
"Invalid value.", Color.Red);
633 commands.Add(
new Command(
"lock",
"lock: Lock movement of the main submarine.", (
string[] args) =>
637 NewMessage((
Submarine.LockX ?
"Submarine movement locked." :
"Submarine movement unlocked."), Color.White);
640 commands.Add(
new Command(
"lockx",
"lockx: Lock horizontal movement of the main submarine.", (
string[] args) =>
643 NewMessage((
Submarine.LockX ?
"Horizontal submarine movement locked." :
"Horizontal submarine movement unlocked."), Color.White);
646 commands.Add(
new Command(
"locky",
"locky: Lock vertical movement of the main submarine.", (
string[] args) =>
649 NewMessage((
Submarine.LockY ?
"Vertical submarine movement locked." :
"Vertical submarine movement unlocked."), Color.White);
652 commands.Add(
new Command(
"dumpids",
"", (
string[] args) =>
656 int count = args.Length == 0 ? 10 :
int.Parse(args[0]);
657 Entity.DumpIds(count, args.Length >= 2 ? args[1] :
null);
661 ThrowError(
"Failed to dump ids", e);
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) =>
667 string filename =
"consoleOutput.txt";
668 if (args.Length > 0) { filename =
string.Join(
" ", args); }
670 File.WriteAllLines(filename, Messages.Select(m => m.Text).ToArray());
673 commands.Add(
new Command(
"findentityids",
"findentityids [entityname]", (
string[] args) =>
675 if (args.Length == 0) {
return; }
676 foreach (MapEntity mapEntity
in MapEntity.MapEntityList)
678 if (mapEntity.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase))
680 ThrowError(mapEntity.ID +
": " + mapEntity.Name.ToString());
683 foreach (Character character
in Character.CharacterList)
685 if (character.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase) || character.SpeciesName == args[0])
687 ThrowError(character.ID +
": " + character.Name.ToString());
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) =>
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)
699 afflictionPrefab = AfflictionPrefab.List.FirstOrDefault(a => a.Name.Equals(affliction, StringComparison.OrdinalIgnoreCase));
701 if (afflictionPrefab ==
null)
703 ThrowError(
"Affliction \"" + affliction +
"\" not found.");
706 if (!
float.TryParse(args[1], out
float afflictionStrength))
708 ThrowError(
"\"" + args[1] +
"\" is not a valid affliction strength.");
711 bool relativeStrength =
false;
714 bool.TryParse(args[4], out relativeStrength);
716 Character targetCharacter = args.Length <= 2 ?
Character.Controlled : FindMatchingCharacter(
new string[] { args[2] });
717 if (targetCharacter !=
null)
719 Limb targetLimb = targetCharacter.AnimController.MainLimb;
722 targetLimb = targetCharacter.AnimController.Limbs.FirstOrDefault(l => l.type.ToString().Equals(args[3], StringComparison.OrdinalIgnoreCase));
724 if (relativeStrength)
726 afflictionStrength *= targetCharacter.MaxVitality / afflictionPrefab.MaxStrength;
728 targetCharacter.CharacterHealth.ApplyAffliction(targetLimb ?? targetCharacter.AnimController.MainLimb, afflictionPrefab.Instantiate(afflictionStrength));
733 return new string[][]
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()
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) =>
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)
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);
754 healedCharacter.CharacterHealth.RemoveAllAfflictions();
760 return new string[][]
762 Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
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) =>
769 Character character = (args.Length == 0) ?
Character.Controlled : FindMatchingCharacter(args);
770 if (character !=
null)
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))
776 Color color = Color.White;
778 color = ToolBox.GradientLerp(
779 MathUtils.InverseLerp(-1000, 1000, treatment.Value),
780 Color.Red, Color.Yellow, Color.White, Color.LightGreen);
782 NewMessage((
int)treatment.Value +
": " + treatment.Key, color);
789 return new string[][]
791 Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
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) =>
797 Character revivedCharacter = (args.Length == 0) ?
Character.Controlled : FindMatchingCharacter(args);
798 if (revivedCharacter ==
null) {
return; }
800 revivedCharacter.Revive();
802 if (GameMain.Server !=
null)
804 foreach (
Client c
in GameMain.Server.ConnectedClients)
806 if (c.
Character != revivedCharacter) {
continue; }
810 if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
812 if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore)
814 characterToRestore.CharacterInfo.PermanentlyDead =
false;
815 mpCampaign.SaveSingleCharacter(characterToRestore, skipBackup:
true);
820 GameMain.Server.SetClientCharacter(c, revivedCharacter);
828 return new string[][]
830 Character.CharacterList.Select(c => c.
Name).Distinct().OrderBy(n => n).ToArray()
834 commands.Add(
new Command(
"freeze",
"", (
string[] args) =>
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) =>
841 Character ragdolledCharacter = (args.Length == 0) ?
Character.Controlled : FindMatchingCharacter(args);
842 if (ragdolledCharacter !=
null)
844 ragdolledCharacter.IsForceRagdolled = !ragdolledCharacter.IsForceRagdolled;
849 return new string[][]
851 Character.CharacterList.Select(c => c.
Name).Distinct().OrderBy(n => n).ToArray()
855 commands.Add(
new Command(
"freecamera|freecam",
"freecam: Detach the camera from the controlled character.", (
string[] args) =>
858 if (Screen.Selected == GameMain.SubEditorScreen) {
return; }
860 if (GameMain.Client ==
null)
863 GameMain.GameScreen.Cam.TargetPos = Vector2.Zero;
867 GameMain.Client?.SendConsoleCommand(
"freecam");
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) =>
874 if (GameMain.GameSession?.EventManager !=
null)
876 GameMain.GameSession.EventManager.Enabled = !GameMain.GameSession.EventManager.Enabled;
877 NewMessage(GameMain.GameSession.EventManager.Enabled ?
"Event manager on" :
"Event manager off", Color.White);
881 commands.Add(
new Command(
"triggerevent",
"triggerevent [identifier]: Created a new event.", (
string[] args) =>
883 List<EventPrefab> eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList();
884 if (GameMain.GameSession?.EventManager !=
null && args.Length > 0)
886 EventPrefab eventPrefab = eventPrefabs.Find(prefab => prefab.Identifier == args[0]);
887 if (eventPrefab is TraitorEventPrefab)
889 ThrowError($
"{eventPrefab.Identifier} is a traitor event. You need to use the 'triggertraitorevent' command to start it.");
892 else if (eventPrefab !=
null)
894 var newEvent = eventPrefab.CreateInstance(GameMain.GameSession.EventManager.RandomSeed);
895 if (newEvent ==
null)
897 NewMessage($
"Could not initialize event {args[0]} because level did not meet requirements");
900 GameMain.GameSession.EventManager.ActivateEvent(newEvent);
901 NewMessage($
"Initialized event {eventPrefab.Identifier}", Color.Aqua);
905 NewMessage($
"Failed to trigger event because {args[0]} is not a valid event identifier.", Color.Red);
908 NewMessage(
"Failed to trigger event", Color.Red);
909 }, isCheat:
true, getValidArgs: () =>
911 List<EventPrefab> eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty).ToList();
915 eventPrefabs.Select(prefab => prefab.Identifier).Distinct().Select(
id =>
id.Value).ToArray()
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) =>
921 if (args.Length == 0)
923 ThrowError($
"Please specify the identifier of the event you want to debug.");
927 if (GameMain.GameSession?.EventManager is EventManager eventManager)
929 var ev = eventManager.ActiveEvents.FirstOrDefault(ev => ev.Prefab?.Identifier == args[0]);
932 ThrowError($
"Event \"{args[0]}\" not found.");
936 string info = ev.GetDebugInfo();
939 RichTextData.GetRichTextData(info, out info);
944 }, isCheat:
true, getValidArgs: () =>
946 IEnumerable<EventPrefab> eventPrefabs;
947 if (GameMain.GameSession?.EventManager ==
null || GameMain.GameSession.EventManager.ActiveEvents.None())
949 eventPrefabs = EventSet.GetAllEventPrefabs().Where(prefab => prefab.Identifier != Identifier.Empty);
953 eventPrefabs = GameMain.GameSession.EventManager.ActiveEvents.Select(e => e.Prefab);
957 eventPrefabs.Select(ev => ev.Identifier.ToString()).ToArray() ?? Array.Empty<
string>()
961 commands.Add(
new Command(
"unlockmission",
"unlockmission [identifier/tag]: Unlocks a mission in a random adjacent level.", (
string[] args) =>
963 if (GameMain.GameSession?.GameMode is not CampaignMode campaign)
965 ThrowError(
"The unlockmission command is only usable in the campaign mode.");
968 if (args.Length == 0)
970 ThrowError(
"Please enter the identifier or a tag of the mission you want to unlock.");
973 var currentLocation = campaign.Map.CurrentLocation;
974 if (MissionPrefab.Prefabs.Any(p => p.Identifier == args[0]))
976 currentLocation.UnlockMissionByIdentifier(args[0].ToIdentifier());
980 currentLocation.UnlockMissionByTag(args[0].ToIdentifier());
982 if (campaign is MultiPlayerCampaign mpCampaign)
984 mpCampaign.IncrementLastUpdateIdForFlag(MultiPlayerCampaign.NetFlags.MapAndMissions);
986 }, isCheat:
true, getValidArgs: () =>
990 MissionPrefab.Prefabs.Select(p => p.Identifier.ToString()).ToArray()
994 commands.Add(
new Command(
"setcampaignmetadata",
"setcampaignmetadata [identifier] [value]: Sets the specified campaign metadata value.", (
string[] args) =>
996 if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
998 ThrowError(
"The setcampaignmetadata command is only usable in the campaign mode.");
1001 if (args.Length < 2)
1003 ThrowError(
"Please specify an identifier and a value.");
1006 if (
float.TryParse(args[1], out
float floatVal))
1008 SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), floatVal, SetDataAction.OperationType.Set);
1012 SetDataAction.PerformOperation(campaign.CampaignMetadata, args[0].ToIdentifier(), args[1], SetDataAction.OperationType.Set);
1017 commands.Add(
new Command(
"setskill",
"setskill [all/identifier] [max/level] [character]: Set your skill level.", (
string[] args) =>
1019 if (args.Length < 2)
1021 NewMessage($
"Missing arguments. Expected at least 2 but got {args.Length} (skill, level, name)", Color.Red);
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;
1029 if (character?.Info?.Job ==
null)
1031 NewMessage(
"Character is not valid.", Color.Red);
1035 bool isMax = levelString.Equals(
"max", StringComparison.OrdinalIgnoreCase);
1037 if (
float.TryParse(levelString, NumberStyles.Number, CultureInfo.InvariantCulture, out
float level) || isMax)
1039 if (isMax) { level = 100; }
1040 if (skillIdentifier ==
"all")
1042 foreach (Skill skill
in character.Info.Job.GetSkills())
1044 character.Info.SetSkillLevel(skill.Identifier, level);
1046 NewMessage($
"Set all {character.Name}'s skills to {level}", Color.Green);
1050 character.Info.SetSkillLevel(skillIdentifier, level);
1051 NewMessage($
"Set {character.Name}'s {skillIdentifier} level to {level}", Color.Green);
1056 NewMessage($
"{levelString} is not a valid level. Expected number or \"max\".", Color.Red);
1058 }, isCheat:
true, getValidArgs: () =>
1062 Character.Controlled?.Info?.Job?.GetSkills()?.Select(skill => skill.Identifier.Value).ToArray() ?? Array.Empty<
string>(),
1064 Character.CharacterList.Select(c => c.
Name).Distinct().OrderBy(n => n).ToArray(),
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) =>
1070 Hull.EditWater = !Hull.EditWater;
1071 NewMessage(Hull.EditWater ?
"Water editing on" :
"Water editing off", Color.White);
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) =>
1076 if (args.Length == 0) {
return; }
1077 var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) :
Character.Controlled;
1078 if (character !=
null)
1080 TalentPrefab talentPrefab = TalentPrefab.TalentPrefabs.Find(c =>
1081 c.Identifier == args[0] ||
1082 c.DisplayName.Equals(args[0], StringComparison.OrdinalIgnoreCase));
1083 if (talentPrefab ==
null)
1085 ThrowError($
"Couldn't find the talent \"{args[0]}\".");
1088 character.GiveTalent(talentPrefab);
1089 NewMessage($
"Gave talent \"{talentPrefab.DisplayName}\" to \"{character.Name}\".");
1094 List<string> talentNames =
new List<string>();
1095 foreach (TalentPrefab talent
in TalentPrefab.TalentPrefabs)
1097 talentNames.Add(talent.DisplayName.Value);
1100 return new string[][]
1102 talentNames.Select(
id =>
id).ToArray(),
1103 Character.CharacterList.Select(c => c.
Name).Distinct().OrderBy(n => n).ToArray()
1107 commands.Add(
new Command(
"unlocktalents",
"unlocktalents [all/[jobname]] [character]: give the specified character all the talents of the specified class", (
string[] args) =>
1109 var character = args.Length >= 2 ? FindMatchingCharacter(args.Skip(1).ToArray()) :
Character.Controlled;
1110 if (character ==
null) {
return; }
1112 List<TalentTree> talentTrees =
new List<TalentTree>();
1113 if (args.Length == 0 || args[0].Equals(
"all", StringComparison.OrdinalIgnoreCase))
1115 talentTrees.AddRange(TalentTree.JobTalentTrees);
1119 var job = JobPrefab.Prefabs.Find(jp => jp.Name !=
null && jp.Name.Equals(args[0], StringComparison.OrdinalIgnoreCase));
1122 ThrowError($
"Failed to find the job \"{args[0]}\".");
1125 if (!TalentTree.JobTalentTrees.TryGet(job.Identifier, out TalentTree talentTree))
1127 ThrowError($
"No talents configured for the job \"{args[0]}\".");
1130 talentTrees.Add(talentTree);
1133 foreach (var talentTree
in talentTrees)
1135 foreach (var talentId
in talentTree.AllTalentIdentifiers)
1137 character.GiveTalent(talentId);
1138 NewMessage($
"Unlocked talent \"{talentId}\".");
1144 List<string> availableArgs =
new List<string>() {
"All" };
1145 availableArgs.AddRange(JobPrefab.Prefabs.Select(j => j.Name.Value));
1146 return new string[][]
1148 availableArgs.ToArray(),
1149 Character.CharacterList.Select(c => c.
Name).Distinct().OrderBy(n => n).ToArray()
1153 commands.Add(
new Command(
"giveexperience",
"giveexperience [amount] [character]: Give experience to character.", (
string[] args) =>
1155 if (args.Length < 1)
1157 NewMessage($
"Missing arguments. Expected at least 1 but got {args.Length} (experience, name)");
1161 string experienceString = args[0];
1162 var character = FindMatchingCharacter(args.Skip(1).ToArray()) ??
Character.Controlled;
1164 if (character?.Info ==
null)
1166 NewMessage(
"Character is not valid.");
1170 if (
int.TryParse(experienceString, NumberStyles.Number, CultureInfo.InvariantCulture, out
int experience))
1172 character.Info.GiveExperience(experience);
1173 NewMessage($
"Gave {character.Name} {experience} experience");
1177 NewMessage($
"{experienceString} is not a valid value. Expected number.");
1179 }, isCheat:
true, getValidArgs: () =>
1183 new string[] {
"100" },
1184 Character.CharacterList.Select(c => c.
Name).Distinct().OrderBy(n => n).ToArray(),
1188 commands.Add(
new Command(
"fire|editfire",
"fire/editfire: Allows putting up fires by left clicking.", (
string[] args) =>
1190 Hull.EditFire = !Hull.EditFire;
1191 NewMessage(Hull.EditFire ?
"Fire spawning on" :
"Fire spawning off", Color.White);
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));
1196 commands.Add(
new Command(
"showseed|showlevelseed",
"showseed: Show the seed of the current level.", (
string[] args) =>
1198 if (Level.Loaded ==
null)
1200 ThrowError(
"No level loaded.");
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"));
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) =>
1216 if (
Submarine.MainSub ==
null) {
return; }
1218 if (args.Length == 0 || args[0].Equals(
"cursor", StringComparison.OrdinalIgnoreCase))
1221 ThrowError(
"Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client.");
1223 Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
1226 else if (args[0].Equals(
"start", StringComparison.OrdinalIgnoreCase))
1228 if (Level.Loaded ==
null)
1230 NewMessage(
"Can't teleport the sub to the start of the level (no level loaded).", Color.Red);
1233 Vector2 pos = Level.Loaded.StartPosition;
1234 if (Level.Loaded.StartOutpost !=
null)
1236 pos -= Vector2.UnitY * (
Submarine.MainSub.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
1240 else if (args[0].Equals(
"end", StringComparison.OrdinalIgnoreCase))
1242 if (Level.Loaded ==
null)
1244 NewMessage(
"Can't teleport the sub to the end of the level (no level loaded).", Color.Red);
1247 Vector2 pos = Level.Loaded.EndPosition;
1248 if (Level.Loaded.EndOutpost !=
null)
1250 pos -= Vector2.UnitY * (
Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
1254 else if (args[0].Equals(
"endoutpost", StringComparison.OrdinalIgnoreCase))
1256 Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY *
Submarine.MainSub.Borders.Height);
1259 if (Level.Loaded?.EndOutpost ==
null)
1261 NewMessage(
"Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
1264 var outpostDockingPort =
DockingPort.
List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost);
1265 if (submarineDockingPort !=
null && outpostDockingPort !=
null)
1267 submarineDockingPort.Dock(outpostDockingPort);
1273 return new string[][]
1275 new string[] {
"start",
"end",
"endoutpost",
"cursor" }
1280 commands.Add(
new Command(
"crash",
"crash: Crashes the game.", (
string[] args) =>
1282 throw new Exception(
"crash command issued");
1285 commands.Add(
new Command(
"listeditableproperties",
"", (
string[] args) =>
1287 StringBuilder sb =
new StringBuilder();
1290 filename =
"ItemComponent properties (client).txt";
1291 sb.AppendLine(
"Client-side ItemComponent properties:");
1293 filename =
"ItemComponent properties (server).txt";
1294 sb.AppendLine(
"Server-side ItemComponent properties:");
1297 foreach (var ic
in itemComponents.OrderBy(ic => ic.Name))
1299 sb.AppendLine(ic.Name+
":");
1300 foreach (var prop
in ic.GetProperties())
1302 if (prop.DeclaringType != ic) {
continue; }
1303 if (prop.GetCustomAttributes(inherit:
false).OfType<
Editable>().Any())
1305 sb.AppendLine(prop.Name);
1309 File.WriteAllText(filename, sb.ToString());
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) =>
1315 if (args.Length > 0) {
float.TryParse(args[0], out seconds); }
1316 System.Diagnostics.Stopwatch sw =
new System.Diagnostics.Stopwatch();
1318 for (
int i = 0; i < seconds * Timing.FixedUpdateRate; i++)
1320 Screen.Selected?.Update(Timing.Step);
1323 NewMessage($
"Fast-forwarded by {seconds} seconds (took {sw.ElapsedMilliseconds / 1000.0f} s).");
1326 commands.Add(
new Command(
"removecharacter",
"removecharacter [character name]: Immediately deletes the specified character.", (
string[] args) =>
1328 if (args.Length == 0) {
return; }
1329 Character character = FindMatchingCharacter(args,
false);
1330 if (character ==
null) {
return; }
1332 Entity.Spawner?.AddEntityToRemoveQueue(character);
1336 return new string[][]
1338 Character.CharacterList.Select(c => c.
Name).Distinct().OrderBy(n => n).ToArray()
1342 commands.Add(
new Command(
"waterphysicsparams",
"waterphysicsparams [stiffness] [spread] [damping]: defaults 0.02, 0.05, 0.05", (
string[] args) =>
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;
1353 commands.Add(
new Command(
"testlevels",
"testlevels", (
string[] args) =>
1355 CoroutineManager.StartCoroutine(TestLevels());
1359 IEnumerable<CoroutineStatus> TestLevels()
1361 SubmarineInfo selectedSub =
null;
1362 Identifier subName = GameSettings.CurrentConfig.QuickStartSub;
1363 if (subName != Identifier.Empty)
1365 selectedSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName);
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);
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)
1382 if (ruin.Area.Intersects(subWorldRect))
1384 ThrowError(
"Ruins intersect with the sub. Seed: " + seed +
", Submarine: " +
Submarine.MainSub.Info.Name);
1385 yield
return CoroutineStatus.Success;
1389 var levelCells = Level.Loaded.GetCells(
1391 Math.Max(
Submarine.MainSub.Borders.Width / Level.GridCellSize, 2));
1392 foreach (var cell
in levelCells)
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)));
1401 (
int)minExtents.X, (
int)minExtents.Y,
1402 (
int)(maxExtents.X - minExtents.X), (
int)(maxExtents.Y - minExtents.Y));
1403 if (cellRect.Intersects(subWorldRect))
1405 ThrowError(
"Level cells intersect with the sub. Seed: " + seed +
", Submarine: " +
Submarine.MainSub.Info.Name);
1406 yield
return CoroutineStatus.Success;
1410 GameMain.GameSession.EndRound(
"");
1414 NewMessage(
"Level seed " + seed +
" ok (test #" + count +
")");
1417 GUIMessageBox.CloseAll();
1419 yield
return CoroutineStatus.Running;
1424 commands.Add(
new Command(
"showreputation",
"showreputation: List the current reputation values.", (
string[] args) =>
1426 if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1428 NewMessage(
"Reputation:");
1429 foreach (var faction in campaign.Factions)
1431 NewMessage($
" - {faction.Prefab.Name}: {faction.Reputation.Value}");
1436 ThrowError(
"Could not show reputation (no active campaign).");
1440 commands.Add(
new Command(
"setlocationreputation",
"setlocationreputation [value]: Set the reputation in the current location to the specified value.", (
string[] args) =>
1442 if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1444 if (args.Length == 0) {
return; }
1445 if (
float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out
float reputation))
1447 campaign.Map.CurrentLocation.Reputation?.SetReputation(reputation);
1451 ThrowError($
"Could not set location reputation ({args[0]} is not a valid reputation value).");
1456 ThrowError(
"Could not set location reputation (no active campaign).");
1460 commands.Add(
new Command(
"setreputation",
"setreputation [faction] [value]: Set the reputation of a cation to the specified value.", (
string[] args) =>
1462 if (args.Length < 2)
1464 ThrowError(
"Insufficient arguments (expected 2)");
1468 if (GameMain.GameSession?.GameMode is CampaignMode campaign)
1470 if (campaign.Factions.FirstOrDefault(f => f.Prefab.Identifier == args[0]) is { } faction)
1472 if (
float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out
float reputation))
1474 faction.Reputation.SetReputation(reputation);
1478 ThrowError($
"Could not set faction reputation ({args[1]} is not a valid reputation value).");
1483 ThrowError($
"Could not set faction reputation (faction {args[0]} not found).");
1488 ThrowError(
"Could not set faction reputation (no active campaign).");
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>()
1499 commands.Add(
new Command(
"fixitems",
"fixitems: Repairs all items and restores them to full condition.", (
string[] args) =>
1501 foreach (Item it
in Item.ItemList)
1504 it.Condition = it.MaxCondition;
1508 commands.Add(
new Command(
"fixhulls|fixwalls",
"fixwalls/fixhulls: Fixes all walls.", (
string[] args) =>
1510 var walls =
new List<Structure>(
Structure.WallList);
1511 foreach (Structure w
in walls)
1515 for (
int i = 0; i < w.SectionCount; i++)
1517 w.AddDamage(i, -100000.0f);
1520 catch (InvalidOperationException e)
1522 string errorMsg =
"Error while executing the fixhulls command.\n" + e.StackTrace.CleanupStackTrace();
1523 GameAnalyticsManager.AddErrorEventOnce(
"DebugConsole.FixHulls", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
1528 commands.Add(
new Command(
"maxupgrades",
"maxupgrades [category] [prefab]: Maxes out all upgrades or only specific one if given arguments.", args =>
1530 UpgradeManager upgradeManager = GameMain.GameSession?.Campaign?.UpgradeManager;
1531 if (upgradeManager ==
null)
1533 ThrowError(
"This command can only be used in campaign.");
1537 string categoryIdentifier =
null;
1538 string prefabIdentifier =
null;
1540 switch (args.Length)
1543 categoryIdentifier = args[0];
1546 categoryIdentifier = args[0];
1547 prefabIdentifier = args[1];
1551 foreach (UpgradeCategory category
in UpgradeCategory.Categories)
1553 if (!
string.IsNullOrWhiteSpace(categoryIdentifier) && category.Identifier != categoryIdentifier) {
continue; }
1554 foreach (UpgradePrefab prefab
in UpgradePrefab.Prefabs)
1556 if (!prefab.UpgradeCategories.Contains(category)) {
continue; }
1557 if (!
string.IsNullOrWhiteSpace(prefabIdentifier) && prefab.Identifier != prefabIdentifier) {
continue; }
1559 int targetLevel = prefab.GetMaxLevelForCurrentSub() - upgradeManager.GetRealUpgradeLevel(prefab, category);
1560 for (
int i = 0; i < targetLevel; i++)
1562 upgradeManager.PurchaseUpgrade(prefab, category, force:
true);
1564 NewMessage($
"Upgraded {category.Identifier}.{prefab.Identifier} by {targetLevel} levels.", Color.DarkGreen);
1568 NewMessage($
"Start a new round to apply the upgrades.", Color.Lime);
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()
1578 commands.Add(
new Command(
"power",
"power: Immediately powers up the submarine's nuclear reactor.", (
string[] args) =>
1580 Item reactorItem =
Item.ItemList.Find(i => i.GetComponent<
Reactor>() !=
null);
1581 if (reactorItem ==
null) {
return; }
1583 var reactor = reactorItem.GetComponent<
Reactor>();
1586 if (GameMain.Server !=
null)
1588 reactorItem.CreateServerEvent(reactor);
1593 commands.Add(
new Command(
"oxygen|air",
"oxygen/air: Replenishes the oxygen levels in every room to 100%.", (
string[] args) =>
1595 foreach (Hull hull
in Hull.HullList)
1597 hull.OxygenPercentage = 100.0f;
1601 commands.Add(
new Command(
"kill",
"kill [character]: Immediately kills the specified character.", (
string[] args) =>
1603 Character killedCharacter = (args.Length == 0) ?
Character.Controlled : FindMatchingCharacter(args);
1604 killedCharacter?.Kill(
CauseOfDeathType.Unknown, causeOfDeathAffliction:
null);
1608 return new string[][]
1610 Character.CharacterList.Select(c => c.
Name).Distinct().OrderBy(n => n).ToArray()
1614 commands.Add(
new Command(
"killmonsters",
"killmonsters: Immediately kills all AI-controlled enemies in the level.", (
string[] args) =>
1616 foreach (Character c
in Character.CharacterList)
1618 if (c.AIController is EnemyAIController enemyAI && enemyAI.PetBehavior ==
null)
1620 c.SetAllDamage(200.0f, 0.0f, 0.0f);
1623 foreach (Hull hull
in Hull.HullList)
1625 hull.BallastFlora?.Kill();
1627 foreach (Submarine sub
in Submarine.Loaded)
1629 sub.WreckAI?.Kill();
1631 },
null, isCheat:
true));
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) =>
1635 if (args.Length == 0)
1637 foreach (Character c
in Character.CharacterList.Where(c => c.IsDead).ToList())
1644 Character character = FindMatchingCharacter(args);
1645 character?.DespawnNow();
1650 return new string[][]
1652 Character.CharacterList.Where(c => c.IsDead).Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1656 commands.Add(
new Command(
"setclientcharacter",
"setclientcharacter [client name] [character name]: Gives the client control of the specified character.",
null,
1659 if (GameMain.NetworkMember ==
null)
return null;
1661 return new string[][]
1663 GameMain.NetworkMember.ConnectedClients.Select(c => c.Name).ToArray(),
1664 Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1668 commands.Add(
new Command(
"campaigninfo|campaignstatus",
"campaigninfo: Display information about the state of the currently active campaign.", (
string[] args) =>
1670 if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
1672 ThrowError(
"No campaign active!");
1676 campaign.LogState();
1679 commands.Add(
new Command(
"campaigndestination|setcampaigndestination",
"campaigndestination [index]: Set the location to head towards in the currently active campaign.", (
string[] args) =>
1681 if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
1683 ThrowError(
"No campaign active!");
1687 if (args.Length == 0)
1690 foreach (LocationConnection connection
in campaign.Map.CurrentLocation.Connections)
1692 NewMessage(
" " + i +
". " + connection.OtherLocation(campaign.Map.CurrentLocation).DisplayName, Color.White);
1695 ShowQuestionPrompt(
"Select a destination (0 - " + (campaign.Map.CurrentLocation.Connections.Count - 1) +
"):", (
string selectedDestination) =>
1697 int destinationIndex = -1;
1698 if (!int.TryParse(selectedDestination, out destinationIndex)) return;
1699 if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count)
1701 NewMessage(
"Index out of bounds!", Color.Red);
1704 Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
1705 campaign.Map.SelectLocation(location);
1706 NewMessage(location.DisplayName +
" selected.", Color.White);
1711 int destinationIndex = -1;
1712 if (!
int.TryParse(args[0], out destinationIndex))
return;
1713 if (destinationIndex < 0 || destinationIndex >= campaign.Map.CurrentLocation.Connections.Count)
1715 NewMessage(
"Index out of bounds!", Color.Red);
1718 Location location = campaign.Map.CurrentLocation.Connections[destinationIndex].OtherLocation(campaign.Map.CurrentLocation);
1719 campaign.Map.SelectLocation(location);
1720 NewMessage(location.DisplayName +
" selected.", Color.White);
1724 commands.Add(
new Command(
"togglecampaignteleport",
"Toggle on/off teleportation between campaign locations by double clicking on the campaign map.", args =>
1726 if (GameMain.GameSession?.Campaign ==
null)
1728 ThrowError(
"No campaign active.");
1731 GameMain.GameSession.Map.AllowDebugTeleport = !GameMain.GameSession.Map.AllowDebugTeleport;
1732 NewMessage((GameMain.GameSession.Map.AllowDebugTeleport ?
"Enabled" :
"Disabled") +
" teleportation on the campaign map.", Color.White);
1735 commands.Add(
new Command(
"money",
"money [amount] [character]: Gives the specified amount of money to the crew when a campaign is active.", args =>
1737 if (args.Length == 0) {
return; }
1739 if (!(GameMain.GameSession?.GameMode is CampaignMode campaign)) {
return; }
1742 if (args.Length >= 2)
1744 targetCharacter = FindMatchingCharacter(args.Skip(1).ToArray());
1747 if (
int.TryParse(args[0], out
int money))
1749 Wallet wallet = targetCharacter is
null || GameMain.IsSingleplayer ? campaign.Bank : targetCharacter.Wallet;
1751 GameAnalyticsManager.AddMoneyGainedEvent(money, GameAnalyticsManager.MoneySource.Cheat,
"console");
1755 ThrowError($
"\"{args[0]}\" is not a valid numeric value.");
1757 }, isCheat:
true, getValidArgs: () =>
new []
1759 new []{
string.Empty },
1760 Character.CharacterList.Select(c => c.Name).Distinct().ToArray()
1763 commands.Add(
new Command(
"showmoney",
"showmoney: Shows the amount of money in everyones wallet.", args =>
1765 if (!(GameMain.GameSession?.GameMode is CampaignMode campaign))
1767 ThrowError(
"No campaign active!");
1771 NewMessage($
"Bank: {campaign.Bank.Balance}");
1774 commands.Add(
new Command(
"skipeventcooldown",
"skipeventcooldown: Skips the currently active event cooldown and triggers pending monster spawns immediately.", args =>
1776 GameMain.GameSession?.EventManager?.SkipEventCooldown();
1779 commands.Add(
new Command(
"ballastflora",
"infectballast [options]: Infect ballasts and control its growth.", args =>
1781 if (args.Length == 0)
1783 ThrowError(
"No action specified.");
1787 string primaryAction = args.Length > 0 ? args[0] :
"";
1788 string secondaryArgument = args.Length > 1 ? args[1] :
"";
1792 ThrowError(
"No submarine loaded.");
1796 if (primaryAction.Equals(
"infect", StringComparison.OrdinalIgnoreCase))
1798 List<Pump> pumps =
new List<Pump>();
1799 foreach (Item item
in Submarine.MainSub.GetItems(
true))
1801 if (item.CurrentHull !=
null && item.HasTag(Tags.Ballast) && item.GetComponent<
Pump>() is { } pump)
1803 if (item.CurrentHull.BallastFlora !=
null) {
continue; }
1810 BallastFloraPrefab prefab =
string.IsNullOrWhiteSpace(secondaryArgument) ? BallastFloraPrefab.Prefabs.First() : BallastFloraPrefab.Find(secondaryArgument.ToIdentifier());
1813 ThrowError($
"No such behavior: {secondaryArgument}");
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);
1823 ThrowError(
"No available pumps to infect on this submarine.");
1826 if (primaryAction.Equals(
"growthwarp", StringComparison.OrdinalIgnoreCase))
1828 if (
int.TryParse(secondaryArgument, out
int value))
1830 foreach (Hull hull
in Hull.HullList.Where(h => h.BallastFlora !=
null))
1832 BallastFloraBehavior bs = hull.BallastFlora;
1833 bs.GrowthWarps = value;
1836 NewMessage(
"Accelerating growth...", Color.Green);
1840 ThrowError($
"Invalid integer \"{secondaryArgument}\".");
1842 }, isCheat:
true, getValidArgs: () =>
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 };
1849 commands.Add(
new Command(
"setdifficulty|forcedifficulty",
"difficulty [0-100]. Leave the parameter empty to disable.", (
string[] args) =>
1851 if (args.Length == 0)
1853 Level.ForcedDifficulty =
null;
1854 NewMessage($
"Forced difficulty level disabled.", Color.Green);
1856 else if (
float.TryParse(args[0], out
float difficulty))
1858 Level.ForcedDifficulty = difficulty;
1859 NewMessage($
"Set the difficulty level to { Level.ForcedDifficulty }.", Color.Yellow);
1863 commands.Add(
new Command(
"difficulty|leveldifficulty",
"difficulty [0-100]: Change the level difficulty setting in the server lobby.",
null));
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) =>
1867 AutoItemPlacer.OutputDebugInfo = !AutoItemPlacer.OutputDebugInfo;
1868 NewMessage((AutoItemPlacer.OutputDebugInfo ?
"Enabled" :
"Disabled") +
" automatic item placer logging.", Color.White);
1869 }, isCheat:
false));
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) =>
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));
1879 commands.Add(
new Command(
"listtasks",
"listtasks: Lists all asynchronous tasks currently in the task pool.", (
string[] args) => { TaskPool.ListTasks(line => DebugConsole.NewMessage(line)); }));
1881 commands.Add(
new Command(
"listcoroutines",
"listcoroutines: Lists all coroutines currently running.", (
string[] args) => { CoroutineManager.ListCoroutines(); }));
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) =>
1885 if (args.Length > 0)
1887 string packageName =
string.Join(
" ", args);
1888 var
package = ContentPackageManager.EnabledPackages.All.FirstOrDefault(p => p.Name.Equals(packageName, StringComparison.OrdinalIgnoreCase));
1889 if (package ==
null)
1891 ThrowError(
"Content package \"" + packageName +
"\" not found.");
1895 package.CalculateHash(logging:
true);
1900 ContentPackageManager.EnabledPackages.Core.CalculateHash(logging:
true);
1905 return new string[][]
1907 ContentPackageManager.EnabledPackages.All.Select(cp => cp.Name).ToArray()
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) =>
1913 if (args.Count() < 2 || (GameMain.NetworkMember ==
null))
return;
1914 if (!
float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out
float minimumLatency))
1916 ThrowError(args[0] +
" is not a valid latency value.");
1919 if (!
float.TryParse(args[1], NumberStyles.Any, CultureInfo.InvariantCulture, out
float randomLatency))
1921 ThrowError(args[1] +
" is not a valid latency value.");
1924 if (GameMain.NetworkMember !=
null)
1926 GameMain.NetworkMember.SimulatedMinimumLatency = minimumLatency;
1927 GameMain.NetworkMember.SimulatedRandomLatency = randomLatency;
1929 NewMessage(
"Set simulated minimum latency to " + minimumLatency.ToString(CultureInfo.InvariantCulture) +
" and random latency to " + randomLatency.ToString(CultureInfo.InvariantCulture) +
".", Color.White);
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) =>
1934 if (args.Count() < 1 || (GameMain.NetworkMember ==
null))
return;
1935 if (!
float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out
float loss))
1937 ThrowError(args[0] +
" is not a valid loss ratio.");
1940 if (GameMain.NetworkMember !=
null)
1942 GameMain.NetworkMember.SimulatedLoss = loss;
1944 NewMessage(
"Set simulated packet loss to " + (
int)(loss * 100) +
"%.", Color.White);
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) =>
1948 if (args.Count() < 1 || (GameMain.NetworkMember ==
null))
return;
1949 if (!
float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out
float duplicates))
1951 ThrowError(args[0] +
" is not a valid duplicate ratio.");
1954 if (GameMain.NetworkMember !=
null)
1956 GameMain.NetworkMember.SimulatedDuplicatesChance = duplicates;
1958 NewMessage(
"Set packet duplication to " + (
int)(duplicates * 100) +
"%.", Color.White);
1962 commands.Add(
new Command(
"debugvoip",
"Toggle the server writing VOIP into audio files.",
null, isCheat:
false));
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) =>
1966 if (args.Count() < 1 || (GameMain.NetworkMember ==
null))
return;
1967 if (!
float.TryParse(args[0], NumberStyles.Any, CultureInfo.InvariantCulture, out
float time))
1969 ThrowError(args[0] +
" is not a valid duration ratio.");
1972 GameSession.MinimumLoadingTime = time;
1973 NewMessage(
"Set minimum loading time to " + time +
" seconds.", Color.White);
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,
1980 if (GameMain.NetworkMember ==
null) {
return null; }
1981 return new string[][]
1983 Character.CharacterList.Select(c => c.Name).Distinct().OrderBy(n => n).ToArray()
1987 commands.Add(
new Command(
"storeinfo",
"", (
string[] args) =>
1989 if (GameMain.GameSession?.Map?.CurrentLocation is Location location)
1991 if (location.Stores !=
null)
1993 var msg =
"--- Location: " + location.DisplayName +
" ---";
1994 foreach (var store
in location.Stores)
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}");
2009 NewMessage($
"No stores at {location}, can't show store info.");
2014 NewMessage(
"No current location set, can't show store info.");
2019 commands.Add(
new Command(
"startitems|startitemset",
"start item set identifier", (
string[] args) =>
2021 if (args.Length == 0)
2023 ThrowError($
"No start item set identifier defined!");
2026 AutoItemPlacer.DefaultStartItemSet = args[0].ToIdentifier();
2027 NewMessage($
"Start item set changed to \"{AutoItemPlacer.DefaultStartItemSet}\"");
2028 }, isCheat:
false));
2032 commands.Add(
new Command(
"control",
"control [character name]: Start controlling the specified character (client-only).",
null, () =>
2034 return new string[][] { ListCharacterNames() };
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));
2055 InitProjectSpecific();
2057 commands.Sort((c1, c2) => c1.Names.First().CompareTo(c2.Names.First()));
2060 public static string AutoComplete(
string command,
int increment = 1)
2062 string[] splitCommand = ToolBox.SplitCommand(command);
2063 string[] args = splitCommand.Skip(1).ToArray();
2066 if (args.Length > 0 || (splitCommand.Length > 0 && command.Last() ==
' '))
2068 Command matchingCommand = commands.Find(c => c.Names.Contains(splitCommand[0].ToIdentifier()));
2069 if (matchingCommand?.GetValidArgs ==
null) {
return command; }
2071 int autoCompletedArgIndex = args.Length > 0 && command.Last() !=
' ' ? args.Length - 1 : args.Length;
2074 string[][] allArgs = matchingCommand.GetValidArgs();
2075 if (allArgs ==
null || allArgs.GetLength(0) < autoCompletedArgIndex + 1) {
return command; }
2077 if (
string.IsNullOrEmpty(currentAutoCompletedCommand))
2079 currentAutoCompletedCommand = autoCompletedArgIndex > args.Length - 1 ?
" " : args.Last();
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();
2088 validArgs = validArgs.Concat(allArgs[autoCompletedArgIndex].Where(arg =>
2089 arg.ToLower().Contains(currentAutoCompletedCommand.Trim().ToLower()) &&
2090 !validArgs.Contains(arg))).ToArray();
2092 if (validArgs.Length == 0) {
return command; }
2094 currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, validArgs.Length);
2095 string autoCompletedArg = validArgs[currentAutoCompletedIndex];
2098 if (autoCompletedArg.Contains(
' ')) autoCompletedArg =
'"' + autoCompletedArg +
'"';
2099 for (
int i = 0; i < splitCommand.Length; i++)
2101 if (splitCommand[i].Contains(
' ')) splitCommand[i] =
'"' + splitCommand[i] +
'"';
2104 return string.Join(
" ", autoCompletedArgIndex >= args.Length ? splitCommand : splitCommand.Take(splitCommand.Length - 1)) +
" " + autoCompletedArg;
2108 if (
string.IsNullOrWhiteSpace(currentAutoCompletedCommand))
2110 currentAutoCompletedCommand = command;
2113 List<Identifier> matchingCommands =
new List<Identifier>();
2114 foreach (Command c
in commands)
2116 foreach (var name
in c.Names)
2118 if (currentAutoCompletedCommand.Length > name.Value.Length) {
continue; }
2119 if (name.StartsWith(currentAutoCompletedCommand))
2121 matchingCommands.Add(name);
2126 if (matchingCommands.Count == 0)
return command;
2128 currentAutoCompletedIndex = MathUtils.PositiveModulo(currentAutoCompletedIndex + increment, matchingCommands.Count);
2129 return matchingCommands[currentAutoCompletedIndex].Value;
2133 public static void ResetAutoComplete()
2135 currentAutoCompletedCommand =
"";
2136 currentAutoCompletedIndex = 0;
2143 public static void ExecuteCommand(
string inputtedCommands)
2145 if (
string.IsNullOrWhiteSpace(inputtedCommands) || inputtedCommands ==
"\\" || inputtedCommands ==
"\n") {
return; }
2147 string[] commandsToExecute = inputtedCommands.Split(
"\n");
2148 foreach (
string command
in commandsToExecute)
2150 if (activeQuestionCallback !=
null)
2153 activeQuestionText =
null;
2155 NewCommand(command);
2157 var temp = activeQuestionCallback;
2158 activeQuestionCallback =
null;
2163 if (
string.IsNullOrWhiteSpace(command) || command ==
"\\") {
return; }
2165 string[] splitCommand = ToolBox.SplitCommand(command);
2166 if (splitCommand.Length == 0)
2168 ThrowError(
"Failed to execute command \"" + command +
"\"!");
2169 GameAnalyticsManager.AddErrorEventOnce(
2170 "DebugConsole.ExecuteCommand:LengthZero",
2171 GameAnalyticsManager.ErrorSeverity.Error,
2172 "Failed to execute command \"" + command +
"\"!");
2176 Identifier firstCommand = splitCommand[0].ToIdentifier();
2178 if (firstCommand !=
"admin")
2180 NewCommand(command);
2184 if (GameMain.Client !=
null)
2186 Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand));
2187 if (matchingCommand ==
null)
2190 GameMain.Client.SendConsoleCommand(command);
2191 NewMessage(
"Server command: " + command, Color.Cyan);
2194 else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
2196 if (matchingCommand.RelayToServer)
2198 GameMain.Client.SendConsoleCommand(command);
2199 NewMessage(
"Server command: " + command, Color.Cyan);
2203 matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray());
2207 if (!IsCommandPermitted(firstCommand, GameMain.Client))
2210 AddWarning($
"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build.");
2212 ThrowError($
"You're not permitted to use the command \"{firstCommand}\"!");
2219 bool commandFound =
false;
2220 foreach (Command c
in commands)
2222 if (!c.Names.Contains(firstCommand)) {
continue; }
2223 c.Execute(splitCommand.Skip(1).ToArray());
2224 commandFound =
true;
2230 ThrowError(
"Command \"" + splitCommand[0] +
"\" not found.");
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();
2237 private static string[] ListAvailableLocations()
2239 List<string> locationNames =
new();
2240 foreach(var submarine
in Submarine.Loaded)
2242 locationNames.Add(submarine.Info.Name);
2245 if (Level.Loaded !=
null)
2247 foreach (var cave
in Level.Loaded.Caves)
2249 string caveName = cave.CaveGenerationParams.Name;
2252 while (locationNames.Contains($
"{caveName}_{index}"))
2256 locationNames.Add($
"{caveName}_{index}");
2260 return locationNames.ToArray();
2263 private static bool TryFindTeleportPosition(
string locationName, out Vector2 teleportPosition)
2265 if (
Submarine.MainSub is Submarine mainSub &&
string.Equals(locationName,
"mainsub", StringComparison.InvariantCultureIgnoreCase))
2267 var randomWaypoint = GetRandomWaypoint(mainSub.GetWaypoints(alsoFromConnectedSubs:
false));
2268 if (randomWaypoint !=
null)
2270 teleportPosition = randomWaypoint.WorldPosition;
2273 LogError(
"No waypoints found in the main sub!");
2276 foreach (var submarine
in Submarine.Loaded)
2278 if (
string.Equals(submarine.Info.Name, locationName, StringComparison.InvariantCultureIgnoreCase))
2280 var randomWaypoint = GetRandomWaypoint(submarine.GetWaypoints(alsoFromConnectedSubs:
false));
2281 if (randomWaypoint !=
null)
2283 teleportPosition = randomWaypoint.WorldPosition;
2286 LogError($
"No waypoints found in sub {submarine.Info.Name}!");
2290 if (Level.Loaded is Level loadedLevel)
2292 (
string locationNameNoIndex,
int locationIndex) = SplitIndex(locationName);
2294 foreach (var cave
in loadedLevel.Caves)
2296 if (
string.Equals(cave.CaveGenerationParams.Name, locationNameNoIndex, StringComparison.InvariantCultureIgnoreCase))
2298 if (caveIndex != locationIndex)
2304 var randomWaypoint = GetRandomWaypoint(cave.Tunnels.GetRandom(Rand.RandSync.Unsynced).WayPoints);
2305 if (randomWaypoint !=
null)
2307 teleportPosition = randomWaypoint.WorldPosition;
2310 LogError($
"No waypoints found in cave {cave.CaveGenerationParams.Name}!");
2314 teleportPosition = Vector2.Zero;
2317 WayPoint GetRandomWaypoint(IReadOnlyList<WayPoint> waypoints)
2319 if (waypoints.None())
2324 if (waypoints.Any(point => point.SpawnType ==
SpawnType.Human))
2326 return waypoints.GetRandom(point => point.SpawnType ==
SpawnType.Human, Rand.RandSync.Unsynced);
2329 if (waypoints.Any(point => point.SpawnType ==
SpawnType.Path))
2331 return waypoints.GetRandom(point => point.SpawnType ==
SpawnType.Path, Rand.RandSync.Unsynced);
2334 return waypoints.GetRandom(Rand.RandSync.Unsynced);
2337 (string, int) SplitIndex(
string caveName)
2339 string[] splitName = caveName.Split(
'_');
2340 if (splitName.Length == 1)
2342 return (splitName[0], -1);
2346 return (splitName[0],
int.Parse(splitName[1]));
2351 private static Character FindMatchingCharacter(
string[] args,
bool ignoreRemotePlayers =
false,
Client allowedRemotePlayer =
null,
bool botsOnly =
false)
2353 if (args.Length == 0)
return null;
2355 string characterName;
2356 if (
int.TryParse(args.Last(), out
int characterIndex) && args.Length > 1)
2358 characterName =
string.Join(
" ", args.Take(args.Length - 1)).ToLowerInvariant();
2362 characterName =
string.Join(
" ", args).ToLowerInvariant();
2363 characterIndex = -1;
2366 var matchingCharacters =
Character.CharacterList.FindAll(c =>
2367 c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) &&
2368 (!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c));
2372 matchingCharacters = matchingCharacters.FindAll(c => c is AICharacter);
2375 if (!matchingCharacters.Any())
2377 NewMessage(
"Character \""+ characterName +
"\" not found", Color.Red);
2382 matchingCharacters = matchingCharacters.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ToList();
2383 if (characterIndex == -1)
2385 if (matchingCharacters.Count > 1)
2388 "Found multiple matching characters. " +
2389 "Use \"[charactername] [0-" + (matchingCharacters.Count - 1) +
"]\" to choose a specific character.",
2392 return matchingCharacters[0];
2394 else if (characterIndex < 0 || characterIndex >= matchingCharacters.Count)
2396 ThrowError(
"Character index out of range. Select an index between 0 and " + (matchingCharacters.Count - 1));
2400 return matchingCharacters[characterIndex];
2406 private static void TeleportCharacter(Vector2 cursorWorldPos, Character controlledCharacter,
string[] args)
2408 if (Screen.Selected != GameMain.GameScreen)
2410 NewMessage(
"Cannot teleport a character in the menu or the editor screens.", color: Color.Yellow);
2414 Character targetCharacter = controlledCharacter;
2415 Vector2 worldPosition = cursorWorldPos;
2416 string locationNameArgument =
"";
2418 var availableLocations = ListAvailableLocations();
2419 if (args.Length > 0)
2421 if (args.Length > 1)
2424 if (availableLocations.Contains(args.Last())
2425 ||
string.Equals(args.Last(),
"mainsub", StringComparison.InvariantCultureIgnoreCase))
2427 locationNameArgument = args.Last();
2428 args = args.Take(args.Length - 1).ToArray();
2432 NewMessage(
"Invalid arguments", color: Color.Yellow);
2438 if (args[0].ToLowerInvariant() !=
"me")
2440 Character match = FindMatchingCharacter(args, ignoreRemotePlayers:
false);
2441 targetCharacter = match;
2445 if (!
string.IsNullOrWhiteSpace(locationNameArgument))
2447 if (TryFindTeleportPosition(locationNameArgument, out Vector2 teleportPosition))
2449 worldPosition = teleportPosition;
2453 ThrowError($
"No teleport position for location \"{locationNameArgument}\" was found.");
2458 if (targetCharacter !=
null)
2460 targetCharacter.TeleportTo(worldPosition);
2464 NewMessage(
"Invalid arguments", color: Color.Yellow);
2468 public static void SpawnCharacter(
string[] args, Vector2 cursorWorldPos, out
string errorMsg)
2471 if (args.Length == 0) {
return; }
2475 Vector2 spawnPosition = Vector2.Zero;
2476 WayPoint spawnPoint =
null;
2478 string characterLowerCase = args[0].ToLowerInvariant();
2479 JobPrefab job =
null;
2480 if (!JobPrefab.Prefabs.ContainsKey(characterLowerCase))
2482 job = JobPrefab.Prefabs.Find(jp => jp.Name !=
null && jp.Name.Equals(characterLowerCase, StringComparison.OrdinalIgnoreCase));
2486 job = JobPrefab.Prefabs[characterLowerCase];
2488 bool isHuman = job !=
null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
2489 bool addToCrew =
false;
2490 if (args.Length > 1)
2492 switch (args[1].ToLowerInvariant())
2498 spawnPoint = WayPoint.GetRandom(
SpawnType.Enemy);
2502 float closestDist = -1.0f;
2503 foreach (WayPoint wp
in WayPoint.WayPointList)
2505 if (wp.Submarine !=
null)
continue;
2508 if (Hull.FindHull(wp.WorldPosition,
null) !=
null)
continue;
2510 float dist = Vector2.Distance(wp.WorldPosition, GameMain.GameScreen.Cam.WorldViewCenter);
2512 if (closestDist < 0.0f || dist < closestDist)
2520 spawnPosition = cursorWorldPos;
2528 args[3].Equals(
"true", StringComparison.OrdinalIgnoreCase) :
2536 if (
string.IsNullOrWhiteSpace(args[0])) {
return; }
2538 if (args.Length > 2)
2546 ThrowError($
"\"{args[2]}\" is not a valid team id.");
2550 if (spawnPoint !=
null) { spawnPosition = spawnPoint.WorldPosition; }
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));
2558 spawnedCharacter.GiveJobItems(spawnPoint);
2559 spawnedCharacter.GiveIdCardTags(spawnPoint);
2560 spawnedCharacter.Info.StartItemsGiven =
true;
2564 CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier());
2567 CharacterInfo characterInfo =
null;
2568 if (prefab.HasCharacterInfo)
2570 characterInfo =
new CharacterInfo(prefab.Identifier);
2572 spawnedCharacter =
Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8), characterInfo);
2575 if (addToCrew && GameMain.GameSession !=
null)
2577 spawnedCharacter.TeamID = teamType;
2579 GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
2584 public static void SpawnItem(
string[] args, Vector2 cursorPos, Character controlledCharacter, out
string errorMsg)
2587 if (args.Length < 1)
return;
2589 Vector2? spawnPos =
null;
2590 Inventory spawnInventory =
null;
2592 string itemNameOrId = args[0].ToLowerInvariant();
2593 ItemPrefab itemPrefab =
2594 (MapEntityPrefab.FindByName(itemNameOrId) ??
2595 MapEntityPrefab.FindByIdentifier(itemNameOrId.ToIdentifier())) as ItemPrefab;
2596 if (itemPrefab ==
null)
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)
2602 errorMsg += $
" Did you mean \"{matching.Name}\"?";
2603 if (matching.Name.Contains(
" "))
2605 errorMsg += $
" Please note that you should surround multi-word names with quotation marks (e.q. spawnitem \"{matching.Name}\")";
2612 if (args.Length > 1)
2614 string spawnLocation = args.Last();
2615 if (args.Length > 2)
2617 spawnLocation = args[^2];
2618 if (!
int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
2619 amount = Math.Min(amount, 100);
2622 switch (spawnLocation)
2625 spawnPos = cursorPos;
2628 spawnInventory = controlledCharacter?.Inventory;
2632 spawnPos = wp ==
null ? Vector2.Zero : wp.WorldPosition;
2638 var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray());
2639 if (matchingCharacter !=
null){ spawnInventory = matchingCharacter.Inventory; }
2644 if ((spawnPos ==
null || spawnPos == Vector2.Zero) && spawnInventory ==
null)
2647 spawnPos = wp ==
null ? Vector2.Zero : wp.WorldPosition;
2650 for (
int i = 0; i < amount; i++)
2652 if (spawnPos !=
null)
2654 if (Entity.Spawner ==
null || Entity.Spawner.Removed)
2656 new Item(itemPrefab, spawnPos.Value,
null);
2660 Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value);
2663 else if (spawnInventory !=
null)
2665 if (Entity.Spawner ==
null)
2667 var spawnedItem =
new Item(itemPrefab, Vector2.Zero,
null);
2668 spawnInventory.TryPutItem(spawnedItem,
null, spawnedItem.AllowedSlots);
2669 onItemSpawned(spawnedItem);
2673 Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnInventory, onSpawned: onItemSpawned);
2676 static void onItemSpawned(Item item)
2678 if (item.ParentInventory?.Owner is Character character)
2682 wifiComponent.
TeamID = character.TeamID;
2694 public static void AddSafeError(
string error)
2697 DebugConsole.ThrowError(error);
2699 DebugConsole.LogError(error);
2703 public static void LogError(
string msg, Color? color =
null, ContentPackage contentPackage =
null)
2705 msg = AddContentPackageInfoToMessage(msg, contentPackage);
2706 color ??= Color.Red;
2707 NewMessage(msg, color.Value, isCommand:
false, isError:
true);
2710 public static void NewCommand(
string command, Color? color =
null)
2712 color ??= Color.White;
2713 NewMessage(command, color.Value, isCommand:
true, isError:
false);
2716 public static void NewMessage(LocalizedString msg, Color? color =
null,
bool debugOnly =
false)
2717 => NewMessage(msg.Value, color, debugOnly);
2719 public static void NewMessage(
string msg, Color? color =
null,
bool debugOnly =
false)
2721 color ??= Color.White;
2725 NewMessage(msg, color.Value, isCommand:
false, isError:
false);
2730 NewMessage(msg, color.Value, isCommand:
false, isError:
false);
2737 private static void NewMessage(
string msg, Color color,
bool isCommand,
bool isError)
2739 if (
string.IsNullOrEmpty(msg)) {
return; }
2741 var newMsg =
new ColoredText(msg, color, isCommand, isError);
2742 queuedMessages.Enqueue(newMsg);
2743 MessageHandler.Invoke(newMsg);
2746 public static void ShowQuestionPrompt(
string question, QuestionCallback onAnswered,
string[] args =
null,
int argCount = -1)
2748 if (args !=
null && args.Length > argCount)
2750 onAnswered(args[argCount]);
2755 activeQuestionText =
new GUITextBlock(
new RectTransform(
new Point(listBox.Content.Rect.Width, 0), listBox.Content.RectTransform),
2756 " >>" + question, font: GUIStyle.SmallFont, wrap:
true)
2758 CanBeFocused =
false,
2759 TextColor = Color.Cyan
2762 NewMessage(
" >>" + question, Color.Cyan);
2764 activeQuestionCallback += onAnswered;
2767 private static bool TryParseTimeSpan(
string s, out TimeSpan timeSpan)
2769 timeSpan =
new TimeSpan();
2770 if (
string.IsNullOrWhiteSpace(s))
return false;
2772 string currNum =
"";
2773 foreach (
char c
in s)
2775 if (
char.IsDigit(c))
2779 else if (
char.IsWhiteSpace(c))
2785 if (!
int.TryParse(currNum, out
int parsedNum) || parsedNum < 0)
2794 timeSpan +=
new TimeSpan(parsedNum, 0, 0, 0, 0);
2797 timeSpan +=
new TimeSpan(0, parsedNum, 0, 0, 0);
2800 timeSpan +=
new TimeSpan(0, 0, parsedNum, 0, 0);
2803 timeSpan +=
new TimeSpan(0, 0, 0, parsedNum, 0);
2809 catch (ArgumentOutOfRangeException)
2811 ThrowError($
"{parsedNum} {c} exceeds the maximum supported time span. Using the maximum time span {TimeSpan.MaxValue} instead.");
2812 timeSpan = TimeSpan.MaxValue;
2822 public static Command FindCommand(
string commandName) => commands.Find(c => c.Names.Contains(commandName.ToIdentifier()));
2824 public static void Log(LocalizedString message) => Log(message?.Value);
2826 public static void Log(
string message)
2828 if (GameSettings.CurrentConfig.VerboseLogging)
2830 NewMessage(message, Color.Gray);
2834 public static void ThrowErrorLocalized(LocalizedString error, Exception e =
null, ContentPackage contentPackage =
null,
bool createMessageBox =
false,
bool appendStackTrace =
false)
2836 ThrowError(error.Value, e, contentPackage, createMessageBox, appendStackTrace);
2839 public static void ThrowError(
string error, Exception e =
null, ContentPackage contentPackage =
null,
bool createMessageBox =
false,
bool appendStackTrace =
false)
2841 error = AddContentPackageInfoToMessage(error, contentPackage);
2844 error +=
" {" + e.Message +
"}\n";
2845 if (e.StackTrace !=
null)
2847 error += e.StackTrace.CleanupStackTrace();
2849 if (e.InnerException !=
null)
2851 var innermost = e.GetInnermost();
2852 error +=
"\n\nInner exception: " + innermost.Message +
"\n";
2853 if (innermost.StackTrace !=
null)
2855 error += innermost.StackTrace.CleanupStackTrace();
2859 else if (appendStackTrace && Environment.StackTrace !=
null)
2861 error +=
"\n" + Environment.StackTrace.CleanupStackTrace();
2863 System.Diagnostics.Debug.WriteLine($
"ThrowError: {error}");
2866 if (createMessageBox)
2868 CoroutineManager.StartCoroutine(CreateMessageBox(error));
2879 public static void ThrowErrorAndLogToGA(
string gaIdentifier,
string errorMsg)
2881 ThrowError(errorMsg);
2882 GameAnalyticsManager.AddErrorEventOnce(
2884 GameAnalyticsManager.ErrorSeverity.Error,
2888 private static readonly HashSet<string> loggedErrorIdentifiers =
new HashSet<string>();
2892 public static void ThrowErrorOnce(
string identifier,
string errorMsg, Exception e =
null)
2894 if (loggedErrorIdentifiers.Contains(identifier)) {
return; }
2895 ThrowError(errorMsg, e);
2896 loggedErrorIdentifiers.Add(identifier);
2899 public static void AddWarning(
string warning, ContentPackage contentPackage =
null)
2901 warning = AddContentPackageInfoToMessage($
"WARNING: {warning}", contentPackage);
2902 System.Diagnostics.Debug.WriteLine(warning);
2903 NewMessage(warning, Color.Yellow);
2906 private static string AddContentPackageInfoToMessage(
string message, ContentPackage contentPackage)
2908 if (contentPackage ==
null) {
return message; }
2910 string color = XMLExtensions.ToStringHex(Color.MediumPurple);
2911 return $
"‖color:{color}‖[{contentPackage.Name}]‖color:end‖ {message}";
2913 return $
"[{contentPackage.Name}] {message}";
2918 private static IEnumerable<CoroutineStatus> CreateMessageBox(
string errorMsg)
2920 new GUIMessageBox(TextManager.Get(
"Error"), errorMsg);
2921 yield
return CoroutineStatus.Success;
2925 public static void SaveLogs()
2927 if (unsavedMessages.Count == 0)
return;
2928 if (!Directory.Exists(SavePath))
2932 Directory.CreateDirectory(SavePath);
2936 ThrowError(
"Failed to create a folder for debug console logs", e);
2941 string fileName =
"DebugConsoleLog_";
2943 fileName +=
"Server_";
2945 fileName +=
"Client_";
2948 fileName += DateTime.Now.ToShortDateString() +
"_" + DateTime.Now.ToShortTimeString();
2949 var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform();
2950 foreach (
char invalidChar
in invalidChars)
2952 fileName = fileName.Replace(invalidChar.ToString(),
"");
2955 string filePath = Path.Combine(SavePath, fileName);
2956 if (File.Exists(filePath +
".txt"))
2959 while (File.Exists(filePath +
" (" + fileNum +
")"))
2963 filePath = filePath +
" (" + fileNum +
")";
2968 File.WriteAllLines(filePath +
".txt", unsavedMessages.Select(l =>
"[" + l.Time +
"] " + l.Text));
2972 unsavedMessages.Clear();
2973 ThrowError(
"Saving debug console log to " + filePath +
" failed", e);
2977 public static void DeactivateCheats()
2980 GameMain.DebugDraw =
false;
2981 GameMain.LightManager.LightingEnabled =
true;
2984 Hull.EditWater =
false;
2985 Hull.EditFire =
false;
2986 EnemyAIController.DisableEnemyAI =
false;
2987 HumanAIController.DisableCrewAI =
false;
Func< string[][]> GetValidArgs
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...
Action< string[]> OnExecute
readonly ImmutableArray< Identifier > Names
void Execute(string[] args)
override int GetHashCode()
static GameSession?? GameSession
static NetworkMember NetworkMember
static IEnumerable< DockingPort > List
The base class for components holding the different functionalities of the item
void InfectBallast(Identifier identifier, bool allowMultiplePerShip=false)
void PowerUpImmediately()
static readonly List< PermissionPreset > List
ColoredText(string text, Color color, bool isCommand, bool isError)
IReadOnlyList< ColoredText > Errors
static ErrorCatcher Create()