Merge remote-tracking branch 'upstream/master' into develop

This commit is contained in:
EvilFactory
2024-06-18 12:19:13 -03:00
263 changed files with 7788 additions and 2849 deletions
@@ -14,6 +14,7 @@ using System.Threading.Tasks;
using Barotrauma.MapCreatures.Behavior;
using System.Text;
namespace Barotrauma
{
readonly struct ColoredText
@@ -249,7 +250,7 @@ namespace Barotrauma
GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats;
}));
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team (0-3)]: 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,
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,
() =>
{
string[] creatureAndJobNames =
@@ -261,7 +262,10 @@ namespace Barotrauma
return new string[][]
{
creatureAndJobNames.ToArray(),
new string[] { "near", "inside", "outside", "cursor" }
new string[] { "near", "inside", "outside", "cursor" },
new string[] { "0", "1", "2", "3" },
new string[] { "true", "false" },
};
}, isCheat: true));
@@ -567,11 +571,30 @@ namespace Barotrauma
commands.Add(new Command("banaddress|banip", "banaddress [endpoint]: Ban the IP address/SteamID from the server.", null));
commands.Add(new Command("teleportcharacter|teleport", "teleport [character name]: Teleport the specified character to the position of the cursor. If the name parameter is omitted, the controlled character will be teleported.", null,
() =>
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.",
onExecute: null,
getValidArgs:() =>
{
return new string[][] { ListCharacterNames() };
var characterList = Character.Controlled != null ? new[] { "Me" } : Array.Empty<string>();
var subList = Submarine.MainSub != null ? new[] { "mainsub" } : Array.Empty<string>();
return new string[][]
{
characterList.Concat(ListCharacterNames()).ToArray(),
subList.Concat(ListAvailableLocations()).ToArray()
};
}, isCheat: true));
commands.Add(new Command("listlocations|locations", "listlocations: List all the locations in the level: subs, outposts, ruins, caves.",
onExecute:(string[] args) =>
{
var availableLocations = ListAvailableLocations();
NewMessage("***************", Color.Cyan);
foreach (var location in availableLocations)
{
NewMessage(location, Color.Cyan);
}
NewMessage("***************", Color.Cyan);
}));
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.",
(string[] args) =>
@@ -782,6 +805,17 @@ namespace Barotrauma
{
if (c.Character != revivedCharacter) { continue; }
// If killed in ironman mode, the character has been wiped from the save mid-round, so its
// original data needs to be restored to the save file (without making a backup of the dead character)
if (GameMain.Server.ServerSettings.IronmanMode && GameMain.GameSession?.Campaign is MultiPlayerCampaign mpCampaign)
{
if (mpCampaign.RestoreSingleCharacterFromBackup(c) is CharacterCampaignData characterToRestore)
{
characterToRestore.CharacterInfo.PermanentlyDead = false;
mpCampaign.SaveSingleCharacter(characterToRestore, skipBackup: true);
}
}
//clients stop controlling the character when it dies, force control back
GameMain.Server.SetClientCharacter(c, revivedCharacter);
break;
@@ -1176,7 +1210,8 @@ namespace Barotrauma
}
},null));
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.", (string[] args) =>
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.",
onExecute:(string[] args) =>
{
if (Submarine.MainSub == null) { return; }
@@ -1233,7 +1268,7 @@ namespace Barotrauma
}
}
},
() =>
getValidArgs:() =>
{
return new string[][]
{
@@ -1924,6 +1959,7 @@ namespace Barotrauma
}));
#if DEBUG
commands.Add(new Command("debugvoip", "Toggle the server writing VOIP into audio files.", null, isCheat: false));
commands.Add(new Command("simulatedlongloadingtime", "simulatedlongloadingtime [minimum loading time]: forces loading a round to take at least the specified amount of seconds.", (string[] args) =>
{
@@ -2047,6 +2083,11 @@ namespace Barotrauma
string[] validArgs = allArgs[autoCompletedArgIndex].Where(arg =>
currentAutoCompletedCommand.Trim().Length <= arg.Length &&
arg.Substring(0, currentAutoCompletedCommand.Trim().Length).ToLower() == currentAutoCompletedCommand.Trim().ToLower()).ToArray();
// add all completions that contain the current argument, to the end of the list
validArgs = validArgs.Concat(allArgs[autoCompletedArgIndex].Where(arg =>
arg.ToLower().Contains(currentAutoCompletedCommand.Trim().ToLower()) &&
!validArgs.Contains(arg))).ToArray();
if (validArgs.Length == 0) { return command; }
@@ -2095,95 +2136,219 @@ namespace Barotrauma
currentAutoCompletedIndex = 0;
}
public static void ExecuteCommand(string command)
/// <summary>
/// Executes the specific command or commands
/// </summary>
/// <param name="inputtedCommands">Command, or multiple commands separated by newlines.</param>
public static void ExecuteCommand(string inputtedCommands)
{
if (activeQuestionCallback != null)
if (string.IsNullOrWhiteSpace(inputtedCommands) || inputtedCommands == "\\" || inputtedCommands == "\n") { return; }
string[] commandsToExecute = inputtedCommands.Split("\n");
foreach (string command in commandsToExecute)
{
#if CLIENT
activeQuestionText = null;
#endif
NewCommand(command);
//reset the variable before invoking the delegate because the method may need to activate another question
var temp = activeQuestionCallback;
activeQuestionCallback = null;
temp(command);
return;
}
if (string.IsNullOrWhiteSpace(command) || command == "\\" || command == "\n") { return; }
string[] splitCommand = ToolBox.SplitCommand(command);
if (splitCommand.Length == 0)
{
ThrowError("Failed to execute command \"" + command + "\"!");
GameAnalyticsManager.AddErrorEventOnce(
"DebugConsole.ExecuteCommand:LengthZero",
GameAnalyticsManager.ErrorSeverity.Error,
"Failed to execute command \"" + command + "\"!");
return;
}
Identifier firstCommand = splitCommand[0].ToIdentifier();
if (firstCommand != "admin")
{
NewCommand(command);
}
#if CLIENT
if (GameMain.Client != null)
{
Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand));
if (matchingCommand == null)
if (activeQuestionCallback != null)
{
//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
GameMain.Client.SendConsoleCommand(command);
NewMessage("Server command: " + command, Color.Cyan);
#if CLIENT
activeQuestionText = null;
#endif
NewCommand(command);
//reset the variable before invoking the delegate because the method may need to activate another question
var temp = activeQuestionCallback;
activeQuestionCallback = null;
temp(command);
return;
}
else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
if (string.IsNullOrWhiteSpace(command) || command == "\\") { return; }
string[] splitCommand = ToolBox.SplitCommand(command);
if (splitCommand.Length == 0)
{
if (matchingCommand.RelayToServer)
ThrowError("Failed to execute command \"" + command + "\"!");
GameAnalyticsManager.AddErrorEventOnce(
"DebugConsole.ExecuteCommand:LengthZero",
GameAnalyticsManager.ErrorSeverity.Error,
"Failed to execute command \"" + command + "\"!");
return;
}
Identifier firstCommand = splitCommand[0].ToIdentifier();
if (firstCommand != "admin")
{
NewCommand(command);
}
#if CLIENT
if (GameMain.Client != null)
{
Command matchingCommand = commands.Find(c => c.Names.Contains(firstCommand));
if (matchingCommand == null)
{
//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
GameMain.Client.SendConsoleCommand(command);
NewMessage("Server command: " + command, Color.Cyan);
return;
}
else
else if (GameMain.Client.HasConsoleCommandPermission(firstCommand))
{
matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray());
if (matchingCommand.RelayToServer)
{
GameMain.Client.SendConsoleCommand(command);
NewMessage("Server command: " + command, Color.Cyan);
}
else
{
matchingCommand.ClientExecute(splitCommand.Skip(1).ToArray());
}
return;
}
return;
}
if (!IsCommandPermitted(firstCommand, GameMain.Client))
{
if (!IsCommandPermitted(firstCommand, GameMain.Client))
{
#if DEBUG
AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build.");
AddWarning($"You're not permitted to use the command \"{firstCommand}\". Executing the command anyway because this is a debug build.");
#else
ThrowError($"You're not permitted to use the command \"{firstCommand}\"!");
return;
#endif
}
}
}
#endif
bool commandFound = false;
foreach (Command c in commands)
{
if (!c.Names.Contains(firstCommand)) { continue; }
c.Execute(splitCommand.Skip(1).ToArray());
commandFound = true;
break;
}
bool commandFound = false;
foreach (Command c in commands)
{
if (!c.Names.Contains(firstCommand)) { continue; }
c.Execute(splitCommand.Skip(1).ToArray());
commandFound = true;
break;
}
if (!commandFound)
{
ThrowError("Command \"" + splitCommand[0] + "\" not found.");
if (!commandFound)
{
ThrowError("Command \"" + splitCommand[0] + "\" not found.");
}
}
}
private static string[] ListCharacterNames() => Character.CharacterList.OrderBy(c => c.IsDead).ThenByDescending(c => c.IsHuman).ThenBy(c => c.Name).Select(c => c.Name).Distinct().ToArray();
private static string[] ListAvailableLocations()
{
List<string> locationNames = new();
foreach(var submarine in Submarine.Loaded)
{
locationNames.Add(submarine.Info.Name);
}
private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null)
if (Level.Loaded != null)
{
foreach (var cave in Level.Loaded.Caves)
{
string caveName = cave.CaveGenerationParams.Name;
// add index in case there are duplicate names
int index = 1;
while (locationNames.Contains($"{caveName}_{index}"))
{
index++;
}
locationNames.Add($"{caveName}_{index}");
}
}
return locationNames.ToArray();
}
private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition)
{
if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase))
{
var randomWaypoint = GetRandomWaypoint(mainSub.GetWaypoints(alsoFromConnectedSubs:false));
if (randomWaypoint != null)
{
teleportPosition = randomWaypoint.WorldPosition;
return true;
}
LogError("No waypoints found in the main sub!");
}
foreach (var submarine in Submarine.Loaded)
{
if (string.Equals(submarine.Info.Name, locationName, StringComparison.InvariantCultureIgnoreCase))
{
var randomWaypoint = GetRandomWaypoint(submarine.GetWaypoints(alsoFromConnectedSubs:false));
if (randomWaypoint != null)
{
teleportPosition = randomWaypoint.WorldPosition;
return true;
}
LogError($"No waypoints found in sub {submarine.Info.Name}!");
}
}
if (Level.Loaded is Level loadedLevel)
{
(string locationNameNoIndex, int locationIndex) = SplitIndex(locationName);
int caveIndex = 1;
foreach (var cave in loadedLevel.Caves)
{
if (string.Equals(cave.CaveGenerationParams.Name, locationNameNoIndex, StringComparison.InvariantCultureIgnoreCase))
{
if (caveIndex != locationIndex)
{
caveIndex++;
continue;
}
var randomWaypoint = GetRandomWaypoint(cave.Tunnels.GetRandom(Rand.RandSync.Unsynced).WayPoints);
if (randomWaypoint != null)
{
teleportPosition = randomWaypoint.WorldPosition;
return true;
}
LogError($"No waypoints found in cave {cave.CaveGenerationParams.Name}!");
}
}
}
teleportPosition = Vector2.Zero;
return false;
WayPoint GetRandomWaypoint(IReadOnlyList<WayPoint> waypoints)
{
if (waypoints.None())
{
return null;
}
if (waypoints.Any(point => point.SpawnType == SpawnType.Human))
{
return waypoints.GetRandom(point => point.SpawnType == SpawnType.Human, Rand.RandSync.Unsynced);
}
if (waypoints.Any(point => point.SpawnType == SpawnType.Path))
{
return waypoints.GetRandom(point => point.SpawnType == SpawnType.Path, Rand.RandSync.Unsynced);
}
return waypoints.GetRandom(Rand.RandSync.Unsynced);
}
(string, int) SplitIndex(string caveName)
{
string[] splitName = caveName.Split('_');
if (splitName.Length == 1)
{
return (splitName[0], -1);
}
else
{
return (splitName[0], int.Parse(splitName[1]));
}
}
}
private static Character FindMatchingCharacter(string[] args, bool ignoreRemotePlayers = false, Client allowedRemotePlayer = null, bool botsOnly = false)
{
if (args.Length == 0) return null;
@@ -2202,6 +2367,11 @@ namespace Barotrauma
c.Name.Equals(characterName, StringComparison.OrdinalIgnoreCase) &&
(!c.IsRemotePlayer || !ignoreRemotePlayers || allowedRemotePlayer?.Character == c));
if (botsOnly)
{
matchingCharacters = matchingCharacters.FindAll(c => c is AICharacter);
}
if (!matchingCharacters.Any())
{
NewMessage("Character \""+ characterName + "\" not found", Color.Red);
@@ -2232,6 +2402,68 @@ namespace Barotrauma
return null;
}
private static void TeleportCharacter(Vector2 cursorWorldPos, Character controlledCharacter, string[] args)
{
if (Screen.Selected != GameMain.GameScreen)
{
NewMessage("Cannot teleport a character in the menu or the editor screens.", color: Color.Yellow);
return;
}
Character targetCharacter = controlledCharacter;
Vector2 worldPosition = cursorWorldPos;
string locationNameArgument = "";
var availableLocations = ListAvailableLocations();
if (args.Length > 0)
{
if (args.Length > 1)
{
// remove location name from args
if (availableLocations.Contains(args.Last())
|| string.Equals(args.Last(), "mainsub", StringComparison.InvariantCultureIgnoreCase))
{
locationNameArgument = args.Last();
args = args.Take(args.Length - 1).ToArray();
}
else
{
NewMessage("Invalid arguments", color: Color.Yellow);
return;
}
}
// the remaining args should be the character name and a possible index
if (args[0].ToLowerInvariant() != "me")
{
Character match = FindMatchingCharacter(args, ignoreRemotePlayers:false);
targetCharacter = match;
}
}
if (!string.IsNullOrWhiteSpace(locationNameArgument))
{
if (TryFindTeleportPosition(locationNameArgument, out Vector2 teleportPosition))
{
worldPosition = teleportPosition;
}
else
{
ThrowError($"No teleport position for location \"{locationNameArgument}\" was found.");
return;
}
}
if (targetCharacter != null)
{
targetCharacter.TeleportTo(worldPosition);
}
else
{
NewMessage("Invalid arguments", color: Color.Yellow);
}
}
public static void SpawnCharacter(string[] args, Vector2 cursorWorldPos, out string errorMsg)
{
@@ -2253,8 +2485,8 @@ namespace Barotrauma
{
job = JobPrefab.Prefabs[characterLowerCase];
}
bool human = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
bool isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
bool addToCrew = false;
if (args.Length > 1)
{
switch (args[1].ToLowerInvariant())
@@ -2288,13 +2520,17 @@ namespace Barotrauma
spawnPosition = cursorWorldPos;
break;
default:
spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy);
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
break;
}
addToCrew =
args.Length > 3 ?
args[3].Equals("true", StringComparison.OrdinalIgnoreCase) :
isHuman;
}
else
{
spawnPoint = WayPoint.GetRandom(human ? SpawnType.Human : SpawnType.Enemy);
spawnPoint = WayPoint.GetRandom(isHuman ? SpawnType.Human : SpawnType.Enemy);
}
if (string.IsNullOrWhiteSpace(args[0])) { return; }
@@ -2313,29 +2549,36 @@ namespace Barotrauma
if (spawnPoint != null) { spawnPosition = spawnPoint.WorldPosition; }
if (human)
if (isHuman)
{
var variant = job != null ? Rand.Range(0, job.Variants, Rand.RandSync.ServerAndClient) : 0;
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
spawnedCharacter = Character.Create(characterInfo, spawnPosition, ToolBox.RandomSeed(8));
if (GameMain.GameSession != null)
{
spawnedCharacter.TeamID = teamType;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
#endif
}
spawnedCharacter.GiveJobItems(spawnPoint);
spawnedCharacter.GiveIdCardTags(spawnPoint);
spawnedCharacter.Info.StartItemsGiven = true;
}
else
{
if (CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier()) != null)
CharacterPrefab prefab = CharacterPrefab.FindBySpeciesName(args[0].ToIdentifier());
if (prefab != null)
{
Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8));
CharacterInfo characterInfo = null;
if (prefab.HasCharacterInfo)
{
characterInfo = new CharacterInfo(prefab.Identifier);
}
spawnedCharacter = Character.Create(args[0], spawnPosition, ToolBox.RandomSeed(8), characterInfo);
}
}
if (addToCrew && GameMain.GameSession != null)
{
spawnedCharacter.TeamID = teamType;
#if CLIENT
GameMain.GameSession.CrewManager.AddCharacter(spawnedCharacter);
#endif
}
}
public static void SpawnItem(string[] args, Vector2 cursorPos, Character controlledCharacter, out string errorMsg)