v1.0.13.1 (first post-1.0 patch)

This commit is contained in:
Regalis11
2023-05-10 15:07:17 +03:00
parent 96fb49ba14
commit ee1db852b1
272 changed files with 5738 additions and 2413 deletions
@@ -10,7 +10,7 @@ namespace Barotrauma
public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
{ {
if (Character == Character.Controlled) { return; } if (Character == Character.Controlled) { return; }
if (!debugai) { return; } if (!DebugAI) { return; }
Vector2 pos = Character.WorldPosition; Vector2 pos = Character.WorldPosition;
pos.Y = -pos.Y; pos.Y = -pos.Y;
Vector2 textOffset = new Vector2(-40, -160); Vector2 textOffset = new Vector2(-40, -160);
@@ -19,6 +19,17 @@ namespace Barotrauma
private IEnumerable<CoroutineStatus> FadeOutColors(float time) private IEnumerable<CoroutineStatus> FadeOutColors(float time)
{ {
Dictionary<MapEntity, Color> originalColors = new Dictionary<MapEntity, Color>();
foreach (var item in thalamusItems)
{
originalColors.Add(item, item.SpriteColor);
}
foreach (var structure in thalamusStructures)
{
originalColors.Add(structure, structure.SpriteColor);
}
float timer = 0; float timer = 0;
while (timer < time) while (timer < time)
{ {
@@ -26,15 +37,16 @@ namespace Barotrauma
float m = MathHelper.Lerp(1, Config.DeadEntityColorMultiplier, MathUtils.InverseLerp(0, time, timer)); float m = MathHelper.Lerp(1, Config.DeadEntityColorMultiplier, MathUtils.InverseLerp(0, time, timer));
foreach (var item in thalamusItems) foreach (var item in thalamusItems)
{ {
if (item.Color.A == 0) { continue; }
if (item.Prefab.BrokenSprites.None()) if (item.Prefab.BrokenSprites.None())
{ {
Color c = item.Prefab.SpriteColor; Color c = originalColors[item];
item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); item.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f);
} }
} }
foreach (var structure in thalamusStructures) foreach (var structure in thalamusStructures)
{ {
Color c = structure.Prefab.SpriteColor; Color c = originalColors[structure];
structure.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f); structure.SpriteColor = new Color(c.R / 255f * m, c.G / 255f * m, c.B / 255f * m, c.A / 255f);
} }
yield return CoroutineStatus.Running; yield return CoroutineStatus.Running;
@@ -6,11 +6,17 @@ namespace Barotrauma
{ {
partial class Attack partial class Attack
{ {
[Serialize("StructureBlunt", IsPropertySaveable.Yes), Editable()] [Serialize("StructureBlunt", IsPropertySaveable.Yes, description: "Name of the sound effect the attack makes when it hits a structure."), Editable()]
public string StructureSoundType { get; private set; } public string StructureSoundType { get; private set; }
/// <summary>
/// Sound to play when the attack deals damage.
/// </summary>
private RoundSound sound; private RoundSound sound;
/// <summary>
/// Particle emitter to use when the attack deals damage.
/// </summary>
private ParticleEmitter particleEmitter; private ParticleEmitter particleEmitter;
partial void InitProjSpecific(ContentXElement element) partial void InitProjSpecific(ContentXElement element)
@@ -582,7 +582,7 @@ namespace Barotrauma
float closestItemDistance = Math.Max(aimAssistAmount, 2.0f); float closestItemDistance = Math.Max(aimAssistAmount, 2.0f);
foreach (MapEntity entity in entityList) foreach (MapEntity entity in entityList)
{ {
if (!(entity is Item item)) if (entity is not Item item)
{ {
continue; continue;
} }
@@ -153,6 +153,7 @@ namespace Barotrauma
private static readonly List<BossProgressBar> bossProgressBars = new List<BossProgressBar>(); private static readonly List<BossProgressBar> bossProgressBars = new List<BossProgressBar>();
private static readonly Dictionary<Identifier, LocalizedString> cachedHudTexts = new Dictionary<Identifier, LocalizedString>(); private static readonly Dictionary<Identifier, LocalizedString> cachedHudTexts = new Dictionary<Identifier, LocalizedString>();
private static LanguageIdentifier cachedHudTextLanguage = LanguageIdentifier.None;
private static GUILayoutGroup bossHealthContainer; private static GUILayoutGroup bossHealthContainer;
@@ -202,10 +203,15 @@ namespace Barotrauma
public static LocalizedString GetCachedHudText(string textTag, InputType keyBind) public static LocalizedString GetCachedHudText(string textTag, InputType keyBind)
{ {
if (cachedHudTextLanguage != GameSettings.CurrentConfig.Language)
{
cachedHudTexts.Clear();
}
Identifier key = (textTag + keyBind).ToIdentifier(); Identifier key = (textTag + keyBind).ToIdentifier();
if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; } if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; }
text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value; text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value;
cachedHudTexts.Add(key, text); cachedHudTexts.Add(key, text);
cachedHudTextLanguage = GameSettings.CurrentConfig.Language;
return text; return text;
} }
@@ -701,10 +701,11 @@ namespace Barotrauma
blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength()); blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength());
radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength()); radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength());
chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength()); chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength());
float afflictionGrainStrength = affliction.GetScreenGrainStrength(); float afflictionGrainStrength = affliction.GetScreenGrainStrength();
if (afflictionGrainStrength > 0.0f) if (afflictionGrainStrength > 0.0f)
{ {
grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength()); grainStrength = Math.Max(grainStrength, afflictionGrainStrength);
Color afflictionGrainColor = affliction.GetActiveEffect()?.GrainColor ?? Color.White; Color afflictionGrainColor = affliction.GetActiveEffect()?.GrainColor ?? Color.White;
grainColor = Color.Lerp(grainColor, afflictionGrainColor, (float)Math.Pow(1.0f - oxygenLowStrength, 2)); grainColor = Color.Lerp(grainColor, afflictionGrainColor, (float)Math.Pow(1.0f - oxygenLowStrength, 2));
} }
@@ -1020,12 +1021,8 @@ namespace Barotrauma
foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions) foreach (KeyValuePair<Affliction, LimbHealth> kvp in afflictions)
{ {
var affliction = kvp.Key; var affliction = kvp.Key;
if (affliction.Prefab.AfflictionOverlay != null) affliction.Prefab.AfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f,
{ new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y));
Sprite ScreenAfflictionOverlay = affliction.Prefab.AfflictionOverlay;
ScreenAfflictionOverlay?.Draw(spriteBatch, Vector2.Zero, Color.White * affliction.GetAfflictionOverlayMultiplier(), Vector2.Zero, 0.0f,
new Vector2(GameMain.GraphicsWidth / DamageOverlay.size.X, GameMain.GraphicsHeight / DamageOverlay.size.Y));
}
} }
float damageOverlayAlpha = DamageOverlayTimer; float damageOverlayAlpha = DamageOverlayTimer;
@@ -125,9 +125,11 @@ namespace Barotrauma
public static string IncrementModVersion(string modVersion) public static string IncrementModVersion(string modVersion)
{ {
if (string.IsNullOrWhiteSpace(modVersion)) { return string.Empty; }
//look for an integer at the end of the string and increment it //look for an integer at the end of the string and increment it
int startIndex = modVersion.Length - 1; int startIndex = modVersion.Length - 1;
while (char.IsDigit(modVersion[startIndex])) { startIndex--; } while (startIndex > 0 && char.IsDigit(modVersion[startIndex])) { startIndex--; }
startIndex++; startIndex++;
if (startIndex >= modVersion.Length if (startIndex >= modVersion.Length
@@ -425,6 +425,10 @@ namespace Barotrauma
{ {
CheatsEnabled = true; CheatsEnabled = true;
SteamAchievementManager.CheatsEnabled = true; SteamAchievementManager.CheatsEnabled = true;
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
campaign.CheatsEnabled = true;
}
NewMessage("Enabled cheat commands.", Color.Red); NewMessage("Enabled cheat commands.", Color.Red);
#if USE_STEAM #if USE_STEAM
NewMessage("Steam achievements have been disabled during this play session.", Color.Red); NewMessage("Steam achievements have been disabled during this play session.", Color.Red);
@@ -632,15 +636,29 @@ namespace Barotrauma
commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) => commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) =>
{ {
if (Character.Controlled == null) { return; } if (Character.Controlled == null) { return; }
WikiImage.Create(Character.Controlled); try
{
WikiImage.Create(Character.Controlled);
}
catch (Exception e)
{
DebugConsole.ThrowError("The command 'wikiimage_character' failed.", e);
}
})); }));
commands.Add(new Command("wikiimage_sub", "Save an image of the main submarine with a transparent background.", (string[] args) => commands.Add(new Command("wikiimage_sub", "Save an image of the main submarine with a transparent background.", (string[] args) =>
{ {
if (Submarine.MainSub == null) { return; } if (Submarine.MainSub == null) { return; }
MapEntity.SelectedList.Clear(); try
MapEntity.ClearHighlightedEntities(); {
WikiImage.Create(Submarine.MainSub); MapEntity.SelectedList.Clear();
MapEntity.ClearHighlightedEntities();
WikiImage.Create(Submarine.MainSub);
}
catch (Exception e)
{
DebugConsole.ThrowError("The command 'wikiimage_sub' failed.", e);
}
})); }));
AssignRelayToServer("kick", false); AssignRelayToServer("kick", false);
@@ -1146,7 +1164,7 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = 1f; GameMain.LightManager.LosAlpha = 1f;
} }
NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.White); NewMessage("Dev mode " + (GameMain.DevMode ? "enabled" : "disabled"), Color.Yellow);
}); });
AssignRelayToServer("devmode", false); AssignRelayToServer("devmode", false);
@@ -1248,8 +1266,8 @@ namespace Barotrauma
AssignOnExecute("debugai", (string[] args) => AssignOnExecute("debugai", (string[] args) =>
{ {
HumanAIController.debugai = !HumanAIController.debugai; HumanAIController.DebugAI = !HumanAIController.DebugAI;
if (HumanAIController.debugai) if (HumanAIController.DebugAI)
{ {
GameMain.DevMode = true; GameMain.DevMode = true;
GameMain.DebugDraw = true; GameMain.DebugDraw = true;
@@ -1264,7 +1282,7 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = 1f; GameMain.LightManager.LosAlpha = 1f;
} }
NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); NewMessage(HumanAIController.DebugAI ? "AI debug info visible" : "AI debug info hidden", Color.Yellow);
}); });
AssignRelayToServer("debugai", false); AssignRelayToServer("debugai", false);
@@ -2323,7 +2341,7 @@ namespace Barotrauma
{ {
if (mapEntity is Item item) if (mapEntity is Item item)
{ {
item.Rect = new Rectangle(item.Rect.X, item.Rect.Y, item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y,
(int)(item.Prefab.Sprite.size.X * item.Prefab.Scale), (int)(item.Prefab.Sprite.size.X * item.Prefab.Scale),
(int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale)); (int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale));
} }
@@ -2859,7 +2877,7 @@ namespace Barotrauma
NewMessage("Valid ranks are:", Color.White); NewMessage("Valid ranks are:", Color.White);
foreach (PermissionPreset permissionPreset in PermissionPreset.List) foreach (PermissionPreset permissionPreset in PermissionPreset.List)
{ {
NewMessage(" - " + permissionPreset.Name, Color.White); NewMessage(" - " + permissionPreset.DisplayName, Color.White);
} }
ShowQuestionPrompt("Rank to grant to client " + args[0] + "?", (rank) => ShowQuestionPrompt("Rank to grant to client " + args[0] + "?", (rank) =>
{ {
@@ -3345,6 +3363,11 @@ namespace Barotrauma
else else
{ {
NewMessage("Level seed: " + Level.Loaded.Seed); NewMessage("Level seed: " + Level.Loaded.Seed);
NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier);
NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()));
NewMessage("Mirrored: " + Level.Loaded.Mirrored);
NewMessage("Level size: " + Level.Loaded.Size.X + "x" + Level.Loaded.Size.Y);
NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown"));
} }
}); });
} }
@@ -517,6 +517,10 @@ namespace Barotrauma
GlyphData gd = GetGlyphData(charIndex); GlyphData gd = GetGlyphData(charIndex);
if (gd.TexIndex >= 0) if (gd.TexIndex >= 0)
{ {
if (gd.TexIndex < 0 || gd.TexIndex >= textures.Count)
{
throw new ArgumentOutOfRangeException($"Error while rendering text. Texture index was out of range. Text: {text}, char: {charIndex} index: {gd.TexIndex}, texture count: {textures.Count}");
}
Texture2D tex = textures[gd.TexIndex]; Texture2D tex = textures[gd.TexIndex];
Vector2 drawOffset; Vector2 drawOffset;
drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y;
@@ -240,7 +240,7 @@ namespace Barotrauma
private void UpdatePending() private void UpdatePending()
{ {
if (!(pendingHealList is { } healList)) { return; } if (pendingHealList is not { } healList) { return; }
ImmutableArray<MedicalClinic.NetCrewMember> pendingList = medicalClinic.PendingHeals.ToImmutableArray(); ImmutableArray<MedicalClinic.NetCrewMember> pendingList = medicalClinic.PendingHeals.ToImmutableArray();
@@ -493,20 +493,26 @@ namespace Barotrauma
GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone")) GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone"))
{ {
OnClicked = (_, _) => OnClicked = (button, _) =>
{ {
if (isWaitingForServer) { return true; }
button.Enabled = false;
isWaitingForServer = true; isWaitingForServer = true;
medicalClinic.TreatAllButtonAction(OnReceived);
bool wasSuccessful = medicalClinic.TreatAllButtonAction(_ => ReEnableButton());
if (!wasSuccessful) { ReEnableButton(); }
void ReEnableButton()
{
isWaitingForServer = false;
button.Enabled = true;
}
return true; return true;
} }
}; };
crewHealList = new CrewHealList(crewList, parent, treatAllButton); crewHealList = new CrewHealList(crewList, parent, treatAllButton);
void OnReceived(MedicalClinic.CallbackOnlyRequest obj)
{
isWaitingForServer = false;
}
} }
private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel) private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel)
@@ -524,7 +530,7 @@ namespace Barotrauma
new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont) new GUITextBlock(new RectTransform(Vector2.One, healthLayout.RectTransform), string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont)
{ {
TextGetter = () => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)(info.Character?.HealthPercentage ?? 100f)}"), TextGetter = () => TextManager.GetWithVariable("percentageformat", "[value]", $"{(int)MathF.Round(info.Character?.HealthPercentage ?? 100f)}"),
TextColor = GUIStyle.Green TextColor = GUIStyle.Green
}; };
@@ -585,8 +591,10 @@ namespace Barotrauma
OnClicked = (button, _) => OnClicked = (button, _) =>
{ {
button.Enabled = false; button.Enabled = false;
medicalClinic.HealAllButtonAction(request => isWaitingForServer = true;
bool wasSuccessful = medicalClinic.HealAllButtonAction(request =>
{ {
isWaitingForServer = false;
switch (request.HealResult) switch (request.HealResult)
{ {
case MedicalClinic.HealRequestResult.InsufficientFunds: case MedicalClinic.HealRequestResult.InsufficientFunds:
@@ -600,6 +608,12 @@ namespace Barotrauma
button.Enabled = true; button.Enabled = true;
ClosePopup(); ClosePopup();
}); });
if (!wasSuccessful)
{
isWaitingForServer = false;
button.Enabled = true;
}
ClosePopup(); ClosePopup();
return true; return true;
} }
@@ -610,11 +624,19 @@ namespace Barotrauma
ClickSound = GUISoundType.Cart, ClickSound = GUISoundType.Cart,
OnClicked = (button, _) => OnClicked = (button, _) =>
{ {
if (isWaitingForServer) { return true; }
button.Enabled = false; button.Enabled = false;
medicalClinic.ClearAllButtonAction(_ => isWaitingForServer = true;
bool wasSuccessful = medicalClinic.ClearAllButtonAction(_ => ReEnableButton());
if (!wasSuccessful) { ReEnableButton(); }
void ReEnableButton()
{ {
isWaitingForServer = false;
button.Enabled = true; button.Enabled = true;
}); }
return true; return true;
} }
}; };
@@ -701,10 +723,15 @@ namespace Barotrauma
OnClicked = (button, _) => OnClicked = (button, _) =>
{ {
button.Enabled = false; button.Enabled = false;
medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ => bool wasSuccessful = medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ =>
{ {
button.Enabled = true; button.Enabled = true;
}); });
if (!wasSuccessful)
{
button.Enabled = true;
}
return true; return true;
} }
}; };
@@ -792,7 +819,13 @@ namespace Barotrauma
selectedCrewAfflictionList = popupAfflictionList; selectedCrewAfflictionList = popupAfflictionList;
isWaitingForServer = true; isWaitingForServer = true;
medicalClinic.RequestAfflictions(info, OnReceived); bool wasSuccessful = medicalClinic.RequestAfflictions(info, OnReceived);
if (!wasSuccessful)
{
isWaitingForServer = false;
ClosePopup();
}
void OnReceived(MedicalClinic.AfflictionRequest request) void OnReceived(MedicalClinic.AfflictionRequest request)
{ {
@@ -800,6 +833,16 @@ namespace Barotrauma
if (request.Result != MedicalClinic.RequestResult.Success) if (request.Result != MedicalClinic.RequestResult.Success)
{ {
switch (request.Result)
{
case MedicalClinic.RequestResult.CharacterInfoMissing:
DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName}\" in medical clini because the character health was missing.");
break;
case MedicalClinic.RequestResult.CharacterNotFound:
DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName} in medical clinic because the server was unable to find a character with ID {info.ID}.");
break;
}
feedbackBlock.Text = GetErrorText(request.Result); feedbackBlock.Text = GetErrorText(request.Result);
feedbackBlock.TextColor = GUIStyle.Red; feedbackBlock.TextColor = GUIStyle.Red;
return; return;
@@ -953,14 +996,20 @@ namespace Barotrauma
} }
existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray(); existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray();
ToggleElements(ElementState.Disabled, elementsToDisable); ToggleElements(ElementState.Disabled, elementsToDisable);
medicalClinic.AddPendingButtonAction(existingMember, request => bool wasSuccessful = medicalClinic.AddPendingButtonAction(existingMember, request =>
{ {
if (request.Result == MedicalClinic.RequestResult.Timeout) if (request.Result == MedicalClinic.RequestResult.Timeout)
{ {
ToggleElements(ElementState.Enabled, elementsToDisable); ToggleElements(ElementState.Enabled, elementsToDisable);
} }
}); });
if (!wasSuccessful)
{
ToggleElements(ElementState.Enabled, elementsToDisable);
}
} }
#warning TODO: this doesn't seem like the right place for this, and it's not clear from the method signature how this differs from ToolBox.LimitString #warning TODO: this doesn't seem like the right place for this, and it's not clear from the method signature how this differs from ToolBox.LimitString
@@ -1090,9 +1139,8 @@ namespace Barotrauma
{ {
return result switch return result switch
{ {
MedicalClinic.RequestResult.Error => TextManager.Get("error"),
MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"), MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"),
_ => "What the hell did you just do" // this should never happen _ => TextManager.Get("error")
}; };
} }
@@ -1072,18 +1072,19 @@ namespace Barotrauma
if (save) if (save)
{ {
GUI.SetSavingIndicatorState(true); GUI.SetSavingIndicatorState(true);
if (GameSession.Submarine != null && !GameSession.Submarine.Removed) if (GameSession.Submarine != null && !GameSession.Submarine.Removed)
{ {
GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine); GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine);
} }
if (GameSession.Campaign is CampaignMode campaign)
// Update store stock when saving and quitting in an outpost (normally updated when CampaignMode.End() is called)
if (GameSession?.Campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost)
{ {
spCampaign.UpdateStoreStock(); if (campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost)
{
spCampaign.UpdateStoreStock();
}
GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
campaign.End();
} }
SaveUtil.SaveGame(GameSession.SavePath); SaveUtil.SaveGame(GameSession.SavePath);
} }
@@ -148,7 +148,10 @@ namespace Barotrauma
} }
if (Submarine.MainSub == null || Level.Loaded == null) { return; } if (Submarine.MainSub == null || Level.Loaded == null) { return; }
endRoundButton.Visible = false; bool allowEndingRound = false;
endRoundButton.Color = endRoundButton.Style.Color;
endRoundButton.HoverColor = endRoundButton.Style.HoverColor;
RichString overrideEndRoundButtonToolTip = string.Empty;
var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub);
LocalizedString buttonText = ""; LocalizedString buttonText = "";
switch (availableTransition) switch (availableTransition)
@@ -159,12 +162,12 @@ namespace Barotrauma
{ {
string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation"; string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation";
buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]"); buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]");
endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; allowEndingRound = !ForceMapUI && !ShowCampaignUI;
} }
break; break;
case TransitionType.LeaveLocation: case TransitionType.LeaveLocation:
buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]");
endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; allowEndingRound = !ForceMapUI && !ShowCampaignUI;
break; break;
case TransitionType.ReturnToPreviousLocation: case TransitionType.ReturnToPreviousLocation:
case TransitionType.ReturnToPreviousEmptyLocation: case TransitionType.ReturnToPreviousEmptyLocation:
@@ -172,32 +175,37 @@ namespace Barotrauma
{ {
string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation"; string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation";
buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]");
endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; allowEndingRound = !ForceMapUI && !ShowCampaignUI;
} }
break; break;
case TransitionType.None: case TransitionType.None:
default: default:
if (Level.Loaded.Type == LevelData.LevelType.Outpost && bool inFriendlySub = Character.Controlled is { IsInFriendlySub: true };
!Level.Loaded.IsEndBiome && if (Level.Loaded.Type == LevelData.LevelType.Outpost && !Level.Loaded.IsEndBiome &&
(Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) (inFriendlySub || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false)))
{ {
if (Missions.Any(m => m is SalvageMission salvageMission && salvageMission.AnyTargetNeedsToBeRetrievedToSub))
{
overrideEndRoundButtonToolTip = TextManager.Get("SalvageTargetNotInSub");
endRoundButton.Color = GUIStyle.Red * 0.7f;
endRoundButton.HoverColor = GUIStyle.Red;
}
buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]");
endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI; allowEndingRound = !ForceMapUI && !ShowCampaignUI;
} }
else else
{ {
endRoundButton.Visible = false; allowEndingRound = false;
} }
break; break;
} }
if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted())
{ {
endRoundButton.Visible = false; allowEndingRound = false;
} }
if (ReadyCheckButton != null) { ReadyCheckButton.Visible = allowEndingRound; }
if (ReadyCheckButton != null) { ReadyCheckButton.Visible = endRoundButton.Visible; } endRoundButton.Visible = allowEndingRound && Character.Controlled is { IsIncapacitated: false };
if (endRoundButton.Visible) if (endRoundButton.Visible)
{ {
if (!AllowedToManageCampaign(ClientPermissions.ManageMap)) if (!AllowedToManageCampaign(ClientPermissions.ManageMap))
@@ -215,7 +223,11 @@ namespace Barotrauma
prevCampaignUIAutoOpenType = availableTransition; prevCampaignUIAutoOpenType = availableTransition;
} }
endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5); endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5);
if (endRoundButton.Text != buttonText) if (overrideEndRoundButtonToolTip != string.Empty)
{
endRoundButton.ToolTip = overrideEndRoundButtonToolTip;
}
else if (endRoundButton.Text != buttonText)
{ {
endRoundButton.ToolTip = buttonText; endRoundButton.ToolTip = buttonText;
} }
@@ -328,6 +340,11 @@ namespace Barotrauma
CampaignUI.UpgradeStore?.RequestRefresh(); CampaignUI.UpgradeStore?.RequestRefresh();
break; break;
} }
if (npc.AIController is HumanAIController humanAi && humanAi.IsInHostileFaction())
{
npc.Speak(TextManager.Get("dialoglowrepcampaigninteraction").Value, identifier: "dialoglowrepcampaigninteraction".ToIdentifier(), minDurationBetweenSimilar: 60.0f);
}
} }
public override void AddToGUIUpdateList() public override void AddToGUIUpdateList()
@@ -348,7 +348,7 @@ namespace Barotrauma
//-------------------------------------- //--------------------------------------
//wait for the new level to be loaded //wait for the new level to be loaded
DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, seconds: 60); DateTime timeOut = DateTime.Now + GameClient.LevelTransitionTimeOut;
while (Level.Loaded == prevLevel || Level.Loaded == null) while (Level.Loaded == prevLevel || Level.Loaded == null)
{ {
if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; } if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; }
@@ -358,8 +358,12 @@ namespace Barotrauma
endTransition.Stop(); endTransition.Stop();
overlayColor = Color.Transparent; overlayColor = Color.Transparent;
if (DateTime.Now > timeOut) { GameMain.NetLobbyScreen.Select(); } if (DateTime.Now > timeOut)
if (!(Screen.Selected is RoundSummaryScreen)) {
DebugConsole.ThrowError("Failed to start the round. Timed out while waiting for the level transition to finish.");
GameMain.NetLobbyScreen.Select();
}
if (Screen.Selected is not RoundSummaryScreen)
{ {
if (continueButton != null) if (continueButton != null)
{ {
@@ -947,7 +951,9 @@ namespace Barotrauma
if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); } if (firedCharacter != null) { CrewManager.FireCharacter(firedCharacter); }
} }
if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null) if (map?.CurrentLocation?.HireManager != null && CampaignUI?.CrewManagement != null &&
/*can't apply until we have the latest save file*/
!NetIdUtils.IdMoreRecent(pendingSaveID, LastSaveID))
{ {
CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires); CampaignUI.CrewManagement.SetHireables(map.CurrentLocation, availableHires);
if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters); } if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters); }
@@ -17,7 +17,8 @@ namespace Barotrauma
{ {
Undecided, Undecided,
Success, Success,
Error, CharacterInfoMissing,
CharacterNotFound,
Timeout Timeout
} }
@@ -34,7 +35,9 @@ namespace Barotrauma
private readonly List<RequestAction<CallbackOnlyRequest>> addRequests = new List<RequestAction<CallbackOnlyRequest>>(); private readonly List<RequestAction<CallbackOnlyRequest>> addRequests = new List<RequestAction<CallbackOnlyRequest>>();
private readonly List<RequestAction<CallbackOnlyRequest>> removeRequests = new List<RequestAction<CallbackOnlyRequest>>(); private readonly List<RequestAction<CallbackOnlyRequest>> removeRequests = new List<RequestAction<CallbackOnlyRequest>>();
public void RequestAfflictions(CharacterInfo info, Action<AfflictionRequest> onReceived) private static readonly LeakyBucket requestBucket = new(RateLimitExpiry / (float)RateLimitMaxRequests, 10);
public bool RequestAfflictions(CharacterInfo info, Action<AfflictionRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
@@ -42,23 +45,26 @@ namespace Barotrauma
if (Screen.Selected is TestScreen) if (Screen.Selected is TestScreen)
{ {
onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray())); onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray()));
return; return true;
} }
#endif #endif
if (info is not { Character.CharacterHealth: { } health }) if (info is not { Character.CharacterHealth: { } health })
{ {
onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray<NetAffliction>.Empty)); onReceived.Invoke(new AfflictionRequest(RequestResult.CharacterInfoMissing, ImmutableArray<NetAffliction>.Empty));
return; return true;
} }
ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health).ToImmutableArray(); ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health);
onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions)); onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions));
return; return true;
} }
afflictionRequests.Add(new RequestAction<AfflictionRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
SendAfflictionRequest(info); {
afflictionRequests.Add(new RequestAction<AfflictionRequest>(onReceived, GetTimeout()));
SendAfflictionRequest(info);
});
} }
public void RequestLatestPending(Action<PendingRequest> onReceived) public void RequestLatestPending(Action<PendingRequest> onReceived)
@@ -66,8 +72,11 @@ namespace Barotrauma
// no need to worry about syncing when there's only one pair of eyes capable of looking at the UI // no need to worry about syncing when there's only one pair of eyes capable of looking at the UI
if (GameMain.IsSingleplayer) { return; } if (GameMain.IsSingleplayer) { return; }
pendingHealRequests.Add(new RequestAction<PendingRequest>(onReceived, GetTimeout())); requestBucket.TryEnqueue(() =>
SendPendingRequest(); {
pendingHealRequests.Add(new RequestAction<PendingRequest>(onReceived, GetTimeout()));
SendPendingRequest();
});
} }
public void Update(float deltaTime) public void Update(float deltaTime)
@@ -79,6 +88,7 @@ namespace Barotrauma
UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout);
requestBucket.Update(deltaTime);
static void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } static void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); }
} }
@@ -146,21 +156,25 @@ namespace Barotrauma
return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault();
} }
public void TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived) public bool TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
AddEverythingToPending(); AddEverythingToPending();
onReceived(new CallbackOnlyRequest(RequestResult.Success)); onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke(); OnUpdate?.Invoke();
return; return true;
} }
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); {
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable);
});
} }
public void HealAllButtonAction(Action<HealRequest> onReceived)
public bool HealAllButtonAction(Action<HealRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
@@ -171,33 +185,39 @@ namespace Barotrauma
OnUpdate?.Invoke(); OnUpdate?.Invoke();
} }
return; return true;
} }
if (campaign?.CampaignUI?.MedicalClinic is { } ui) if (campaign?.CampaignUI?.MedicalClinic is { } openedUi)
{ {
ui.ClosePopup(); openedUi.ClosePopup();
} }
healAllRequests.Add(new RequestAction<HealRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); {
healAllRequests.Add(new RequestAction<HealRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable);
});
} }
public void ClearAllButtonAction(Action<CallbackOnlyRequest> onReceived) public bool ClearAllButtonAction(Action<CallbackOnlyRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
ClearPendingHeals(); ClearPendingHeals();
onReceived(new CallbackOnlyRequest(RequestResult.Success)); onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke(); OnUpdate?.Invoke();
return; return true;
} }
clearAllRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); {
clearAllRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable);
});
} }
private void ClearRequstReceived() private void ClearRequestReceived()
{ {
ClearPendingHeals(); ClearPendingHeals();
if (TryDequeue(clearAllRequests, out var callback)) if (TryDequeue(clearAllRequests, out var callback))
@@ -224,28 +244,31 @@ namespace Barotrauma
OnUpdate?.Invoke(); OnUpdate?.Invoke();
} }
public void AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived) public bool AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
InsertPendingCrewMember(crewMember); InsertPendingCrewMember(crewMember);
onReceived(new CallbackOnlyRequest(RequestResult.Success)); onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke(); OnUpdate?.Invoke();
return; return true;
} }
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); {
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable);
});
} }
public void RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action<CallbackOnlyRequest> onReceived) public bool RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action<CallbackOnlyRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
RemovePendingAffliction(crewMember, affliction); RemovePendingAffliction(crewMember, affliction);
onReceived(new CallbackOnlyRequest(RequestResult.Success)); onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke(); OnUpdate?.Invoke();
return; return true;
} }
INetSerializableStruct removedAffliction = new NetRemovedAffliction INetSerializableStruct removedAffliction = new NetRemovedAffliction
@@ -254,11 +277,14 @@ namespace Barotrauma
Affliction = affliction Affliction = affliction
}; };
removeRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); {
removeRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable);
});
} }
private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) private void NewAdditionReceived(IReadMessage inc, MessageFlag flag)
{ {
var crewMembers = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc); var crewMembers = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
foreach (var crewMember in crewMembers) foreach (var crewMember in crewMembers)
@@ -300,7 +326,7 @@ namespace Barotrauma
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc); NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
if (TryDequeue(afflictionRequests, out var callback)) if (TryDequeue(afflictionRequests, out var callback))
{ {
RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.Error : RequestResult.Success; RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.CharacterNotFound : RequestResult.Success;
callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray()));
} }
} }
@@ -336,7 +362,7 @@ namespace Barotrauma
IWriteMessage msg = StartSending(); IWriteMessage msg = StartSending();
msg.WriteByte((byte)header); msg.WriteByte((byte)header);
netStruct?.Write(msg); netStruct?.Write(msg);
GameMain.Client.ClientPeer?.Send(msg, deliveryMethod); GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod);
} }
public void ClientRead(IReadMessage inc) public void ClientRead(IReadMessage inc)
@@ -356,7 +382,7 @@ namespace Barotrauma
PendingRequestReceived(inc); PendingRequestReceived(inc);
break; break;
case NetworkHeader.ADD_PENDING: case NetworkHeader.ADD_PENDING:
NewAdditonReceived(inc, flag); NewAdditionReceived(inc, flag);
break; break;
case NetworkHeader.REMOVE_PENDING: case NetworkHeader.REMOVE_PENDING:
NewRemovalReceived(inc, flag); NewRemovalReceived(inc, flag);
@@ -365,7 +391,7 @@ namespace Barotrauma
HealRequestReceived(inc); HealRequestReceived(inc);
break; break;
case NetworkHeader.CLEAR_PENDING: case NetworkHeader.CLEAR_PENDING:
ClearRequstReceived(); ClearRequestReceived();
break; break;
} }
} }
@@ -40,7 +40,7 @@ namespace Barotrauma
private void CreateMessageBox(string author) private void CreateMessageBox(string author)
{ {
Vector2 relativeSize = new Vector2(0.3f * GUI.AspectRatioAdjustment, 0.15f); Vector2 relativeSize = new Vector2(0.2f / GUI.AspectRatioAdjustment, 0.15f);
Point minSize = new Point(300, 200); Point minSize = new Point(300, 200);
msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true }; msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true };
@@ -213,10 +213,11 @@ namespace Barotrauma
}; };
List<Mission> missionsToDisplay = new List<Mission>(selectedMissions.Where(m => m.Prefab.ShowInMenus)); List<Mission> missionsToDisplay = new List<Mission>(selectedMissions.Where(m => m.Prefab.ShowInMenus));
if (!selectedMissions.Any() && startLocation != null) if (startLocation != null)
{ {
foreach (Mission mission in startLocation.SelectedMissions) foreach (Mission mission in startLocation.SelectedMissions)
{ {
if (missionsToDisplay.Contains(mission)) { continue; }
if (!mission.Prefab.ShowInMenus) { continue; } if (!mission.Prefab.ShowInMenus) { continue; }
if (mission.Locations[0] == mission.Locations[1] || if (mission.Locations[0] == mission.Locations[1] ||
mission.Locations.Contains(campaignMode?.Map.SelectedLocation)) mission.Locations.Contains(campaignMode?.Map.SelectedLocation))
@@ -316,7 +317,7 @@ namespace Barotrauma
RichString reputationText = displayedMission.GetReputationRewardText(); RichString reputationText = displayedMission.GetReputationRewardText();
if (!reputationText.IsNullOrEmpty()) if (!reputationText.IsNullOrEmpty())
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), reputationText, wrap: true);
} }
int totalReward = displayedMission.GetFinalReward(Submarine.MainSub); int totalReward = displayedMission.GetFinalReward(Submarine.MainSub);
@@ -88,6 +88,11 @@ namespace Barotrauma.Items.Components
public void ClientEventRead(IReadMessage msg, float sendingTime) public void ClientEventRead(IReadMessage msg, float sendingTime)
{ {
UInt16 userID = msg.ReadUInt16();
if (userID != Entity.NullEntityID)
{
user = Entity.FindEntityByID(userID) as Character;
}
CurrPowerConsumption = powerConsumption; CurrPowerConsumption = powerConsumption;
charging = true; charging = true;
timer = Duration; timer = Duration;
@@ -21,6 +21,9 @@ namespace Barotrauma.Items.Components
/// </summary> /// </summary>
private float lightColorMultiplier; private float lightColorMultiplier;
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the light sprite.")]
public float LightSpriteScale { get; set; }
public Vector2 DrawSize public Vector2 DrawSize
{ {
get { return new Vector2(Light.Range * 2, Light.Range * 2); } get { return new Vector2(Light.Range * 2, Light.Range * 2); }
@@ -92,7 +95,13 @@ namespace Barotrauma.Items.Components
{ {
color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value); color = new Color(lightColor, Light.OverrideLightSpriteAlpha.Value);
} }
Light.LightSprite.Draw(spriteBatch, new Vector2(drawPos.X, -drawPos.Y), color * lightBrightness, origin, -Light.Rotation, item.Scale, Light.LightSpriteEffect, itemDepth - 0.0001f); Light.LightSprite.Draw(spriteBatch,
new Vector2(drawPos.X, -drawPos.Y),
color * lightBrightness,
origin,
-Light.Rotation,
item.Scale * LightSpriteScale,
Light.LightSpriteEffect, itemDepth - 0.0001f);
} }
} }
@@ -528,7 +528,7 @@ namespace Barotrauma.Items.Components
if (slotRect.Contains(PlayerInput.MousePosition)) if (slotRect.Contains(PlayerInput.MousePosition))
{ {
var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name); var suitableIngredients = requiredItem.ItemPrefabs.Select(ip => ip.Name).Distinct();
LocalizedString toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients); LocalizedString toolTipText = string.Join(", ", suitableIngredients.Count() > 3 ? suitableIngredients.SkipLast(suitableIngredients.Count() - 3) : suitableIngredients);
if (suitableIngredients.Count() > 3) { toolTipText += "..."; } if (suitableIngredients.Count() > 3) { toolTipText += "..."; }
if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f) if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f)
@@ -550,6 +550,8 @@ namespace Barotrauma.Items.Components
{ {
toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText); toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText);
} }
toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖";
if (!requiredItemPrefab.Description.IsNullOrEmpty()) if (!requiredItemPrefab.Description.IsNullOrEmpty())
{ {
toolTipText += '\n' + requiredItemPrefab.Description; toolTipText += '\n' + requiredItemPrefab.Description;
@@ -594,7 +596,7 @@ namespace Barotrauma.Items.Components
if (tooltip != null) if (tooltip != null)
{ {
GUIComponent.DrawToolTip(spriteBatch, tooltip.Tooltip, tooltip.TargetElement); GUIComponent.DrawToolTip(spriteBatch, RichString.Rich(tooltip.Tooltip), tooltip.TargetElement);
tooltip = null; tooltip = null;
} }
} }
@@ -403,7 +403,8 @@ namespace Barotrauma.Items.Components
private bool VisibleOnItemFinder(Item it) private bool VisibleOnItemFinder(Item it)
{ {
if (!item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; } if (it?.Submarine == null) { return false; }
if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; }
if (it.NonInteractable || it.HiddenInGame) { return false; } if (it.NonInteractable || it.HiddenInGame) { return false; }
if (it.GetComponent<Pickable>() == null) { return false; } if (it.GetComponent<Pickable>() == null) { return false; }
@@ -702,6 +703,12 @@ namespace Barotrauma.Items.Components
private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container) private void DrawHUDFront(SpriteBatch spriteBatch, GUICustomComponent container)
{ {
if (miniMapFrame == null)
{
//frame not created yet, could happen if the item hasn't been inside any sub this round?
return;
}
if (Voltage < MinVoltage) if (Voltage < MinVoltage)
{ {
Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip); Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip);
@@ -1057,7 +1064,9 @@ namespace Barotrauma.Items.Components
waterVolume += linkedHull.WaterVolume; waterVolume += linkedHull.WaterVolume;
totalVolume += linkedHull.Volume; totalVolume += linkedHull.Volume;
} }
hullData.HullWaterAmount = MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100); hullData.HullWaterAmount =
waterVolume > 1.0f ?
MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100) : 0.0f;
} }
else else
{ {
@@ -1311,7 +1311,6 @@ namespace Barotrauma.Items.Components
float worldPingRadiusSqr = worldPingRadius * worldPingRadius; float worldPingRadiusSqr = worldPingRadius * worldPingRadius;
disruptedDirections.Clear(); disruptedDirections.Clear();
if (Level.Loaded == null) { return; }
for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex) for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
{ {
@@ -1434,8 +1433,10 @@ namespace Barotrauma.Items.Components
if (connectedSubs.Contains(submarine)) { continue; } if (connectedSubs.Contains(submarine)) { continue; }
} }
Rectangle worldBorders = Submarine.MainSub.GetDockedBorders(); //display the actual walls if the ping source is inside the sub (but not inside a hull, that's handled above)
worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint(); //only relevant in the end levels or maybe custom subs with some kind of non-hulled parts
Rectangle worldBorders = submarine.GetDockedBorders();
worldBorders.Location += submarine.WorldPosition.ToPoint();
if (Submarine.RectContains(worldBorders, pingSource)) if (Submarine.RectContains(worldBorders, pingSource))
{ {
CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive); CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive);
@@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components
User = Entity.FindEntityByID(userId) as Character; User = Entity.FindEntityByID(userId) as Character;
Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle());
float rotation = msg.ReadSingle(); float rotation = msg.ReadSingle();
SpreadCounter = msg.ReadByte(); spreadIndex = msg.ReadByte();
if (User != null) if (User != null)
{ {
Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false);
@@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components
private readonly List<GUIComponent> uiElements = new List<GUIComponent>(); private readonly List<GUIComponent> uiElements = new List<GUIComponent>();
private GUILayoutGroup uiElementContainer; private GUILayoutGroup uiElementContainer;
private bool readingNetworkEvent;
private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale)); private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale));
public override bool RecreateGUIOnResolutionChange => true; public override bool RecreateGUIOnResolutionChange => true;
@@ -100,7 +102,7 @@ namespace Barotrauma.Items.Components
{ {
ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue); ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue);
} }
else else if (!readingNetworkEvent)
{ {
item.CreateClientEvent(this); item.CreateClientEvent(this);
} }
@@ -126,7 +128,7 @@ namespace Barotrauma.Items.Components
{ {
ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue); ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue);
} }
else else if (!readingNetworkEvent)
{ {
item.CreateClientEvent(this); item.CreateClientEvent(this);
} }
@@ -161,7 +163,7 @@ namespace Barotrauma.Items.Components
{ {
TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected); TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected);
} }
else else if (!readingNetworkEvent)
{ {
item.CreateClientEvent(this); item.CreateClientEvent(this);
} }
@@ -181,12 +183,12 @@ namespace Barotrauma.Items.Components
}; };
btn.OnClicked += (_, userdata) => btn.OnClicked += (_, userdata) =>
{ {
CustomInterfaceElement btnElement = userdata as CustomInterfaceElement;; CustomInterfaceElement btnElement = userdata as CustomInterfaceElement;
if (GameMain.Client == null) if (GameMain.Client == null)
{ {
ButtonClicked(btnElement); ButtonClicked(btnElement);
} }
else else if (!readingNetworkEvent)
{ {
item.CreateClientEvent(this, new EventData(btnElement)); item.CreateClientEvent(this, new EventData(btnElement));
} }
@@ -248,7 +250,7 @@ namespace Barotrauma.Items.Components
int visibleElementCount = 0; int visibleElementCount = 0;
foreach (var uiElement in uiElements) foreach (var uiElement in uiElements)
{ {
if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; } if (uiElement.UserData is not CustomInterfaceElement element) { continue; }
bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0); bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0);
if (visible) { visibleElementCount++; } if (visible) { visibleElementCount++; }
if (uiElement.Visible != visible) if (uiElement.Visible != visible)
@@ -297,9 +299,10 @@ namespace Barotrauma.Items.Components
LocalizedString CreateLabelText(int elementIndex) LocalizedString CreateLabelText(int elementIndex)
{ {
return string.IsNullOrWhiteSpace(customInterfaceElementList[elementIndex].Label) ? var label = customInterfaceElementList[elementIndex].Label;
return string.IsNullOrWhiteSpace(label) ?
TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) : TextManager.GetWithVariable("connection.signaloutx", "[num]", (elementIndex + 1).ToString()) :
customInterfaceElementList[elementIndex].Label; TextManager.Get(label).Fallback(label);
} }
uiElementContainer.Recalculate(); uiElementContainer.Recalculate();
@@ -334,7 +337,9 @@ namespace Barotrauma.Items.Components
{ {
if (uiElements[i] is GUITextBox tb) if (uiElements[i] is GUITextBox tb)
{ {
tb.Text = customInterfaceElementList[i].Signal; tb.Text = Screen.Selected is { IsEditor: true } ?
customInterfaceElementList[i].Signal :
TextManager.Get(customInterfaceElementList[i].Signal).Value;
} }
else if (uiElements[i] is GUINumberInput ni) else if (uiElements[i] is GUINumberInput ni)
{ {
@@ -386,45 +391,53 @@ namespace Barotrauma.Items.Components
public void ClientEventRead(IReadMessage msg, float sendingTime) public void ClientEventRead(IReadMessage msg, float sendingTime)
{ {
for (int i = 0; i < customInterfaceElementList.Count; i++) readingNetworkEvent = true;
try
{ {
var element = customInterfaceElementList[i]; for (int i = 0; i < customInterfaceElementList.Count; i++)
if (element.HasPropertyName)
{ {
string newValue = msg.ReadString(); var element = customInterfaceElementList[i];
if (!element.IsNumberInput) if (element.HasPropertyName)
{ {
TextChanged(element, newValue); string newValue = msg.ReadString();
if (!element.IsNumberInput)
{
TextChanged(element, newValue);
}
else
{
switch (element.NumberType)
{
case NumberType.Int when int.TryParse(newValue, out int value):
ValueChanged(element, value);
break;
case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value):
ValueChanged(element, value);
break;
}
}
} }
else else
{ {
switch (element.NumberType) bool elementState = msg.ReadBoolean();
if (element.ContinuousSignal)
{ {
case NumberType.Int when int.TryParse(newValue, out int value): ((GUITickBox)uiElements[i]).Selected = elementState;
ValueChanged(element, value); TickBoxToggled(element, elementState);
break; }
case NumberType.Float when TryParseFloatInvariantCulture(newValue, out float value): else if (elementState)
ValueChanged(element, value); {
break; ButtonClicked(element);
} }
} }
} }
else
{
bool elementState = msg.ReadBoolean();
if (element.ContinuousSignal)
{
((GUITickBox)uiElements[i]).Selected = elementState;
TickBoxToggled(element, elementState);
}
else if (elementState)
{
ButtonClicked(element);
}
}
}
UpdateSignalsProjSpecific(); UpdateSignalsProjSpecific();
}
finally
{
readingNetworkEvent = false;
}
} }
} }
} }
@@ -212,7 +212,7 @@ namespace Barotrauma.Items.Components
Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite; Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite;
foreach (Limb limb in c.AnimController.Limbs) foreach (Limb limb in c.AnimController.Limbs)
{ {
if (limb.Mass < 1.0f) { continue; } if (limb.Mass < 0.5f && limb != c.AnimController.MainLimb) { continue; }
float noise1 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.02f); float noise1 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.02f);
float noise2 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.008f); float noise2 = PerlinNoise.GetPerlin((thermalEffectState + limb.Params.ID + c.ID) * 0.01f, (thermalEffectState + limb.Params.ID + c.ID) * 0.008f);
Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f); Vector2 spriteScale = ConvertUnits.ToDisplayUnits(limb.body.GetSize()) / pingCircle.size * (noise1 * 0.5f + 2f);
@@ -1398,7 +1398,7 @@ namespace Barotrauma
} }
else else
{ {
throw new Exception("Failed to read component state - " + components[componentIndex].GetType() + " is not IServerSerializable."); throw new Exception($"Failed to read component state - {components[componentIndex].GetType()} in item \"{Prefab.Identifier}\" is not IServerSerializable.");
} }
} }
break; break;
@@ -1411,13 +1411,14 @@ namespace Barotrauma
} }
else else
{ {
throw new Exception("Failed to read inventory state - " + components[containerIndex].GetType() + " is not an ItemContainer."); throw new Exception($"Failed to read inventory state - {components[containerIndex].GetType()} in item \"{Prefab.Identifier}\" is not an ItemContainer.");
} }
} }
break; break;
case EventType.Status: case EventType.Status:
bool loadingRound = msg.ReadBoolean();
float newCondition = msg.ReadSingle(); float newCondition = msg.ReadSingle();
SetCondition(newCondition, isNetworkEvent: true); SetCondition(newCondition, isNetworkEvent: true, executeEffects: !loadingRound);
break; break;
case EventType.AssignCampaignInteraction: case EventType.AssignCampaignInteraction:
CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte(); CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte();
@@ -1459,9 +1460,9 @@ namespace Barotrauma
byte length = msg.ReadByte(); byte length = msg.ReadByte();
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
var statIdentifier = INetSerializableStruct.Read<ItemStatManager.TalentStatIdentifier>(msg); var statIdentifier = INetSerializableStruct.Read<TalentStatIdentifier>(msg);
var statValue = msg.ReadSingle(); var statValue = msg.ReadSingle();
StatManager.ApplyStat(statIdentifier, statValue); StatManager.ApplyStatDirect(statIdentifier, statValue);
} }
break; break;
case EventType.Upgrade: case EventType.Upgrade:
@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Linq;
namespace Barotrauma namespace Barotrauma
{ {
@@ -320,5 +321,45 @@ namespace Barotrauma
return IsHorizontal ? rect.Height : rect.Width; return IsHorizontal ? rect.Height : rect.Width;
} }
} }
public override void UpdateEditing(Camera cam, float deltaTime)
{
if (editingHUD == null || editingHUD.UserData != this)
{
editingHUD = CreateEditingHUD();
}
}
private GUIComponent CreateEditingHUD(bool inGame = false)
{
editingHUD = new GUIFrame(new RectTransform(new Vector2(0.3f, 0.15f), GUI.Canvas, Anchor.CenterRight) { MinSize = new Point(400, 0) })
{
UserData = this
};
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.85f), editingHUD.RectTransform, Anchor.Center))
{
Stretch = true,
AbsoluteSpacing = (int)(GUI.Scale * 5)
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.2f), paddedFrame.RectTransform), TextManager.Get("entityname.gap"), font: GUIStyle.LargeFont);
var hiddenInGameTickBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), paddedFrame.RectTransform), TextManager.Get("sp.hiddeningame.name"))
{
Selected = HiddenInGame
};
hiddenInGameTickBox.OnSelected += (GUITickBox tickbox) =>
{
HiddenInGame = tickbox.Selected;
return true;
};
editingHUD.RectTransform.Resize(new Point(
editingHUD.Rect.Width,
(int)(paddedFrame.Children.Sum(c => c.Rect.Height + paddedFrame.AbsoluteSpacing) / paddedFrame.RectTransform.RelativeSize.Y * 1.25f)));
PositionEditingHUD();
return editingHUD;
}
} }
} }
@@ -240,7 +240,8 @@ namespace Barotrauma.Lights
range * range *
((Character.Controlled?.Submarine != null && light.ParentSub == Character.Controlled?.Submarine) ? 2.0f : 1.0f) * ((Character.Controlled?.Submarine != null && light.ParentSub == Character.Controlled?.Submarine) ? 2.0f : 1.0f) *
(light.CastShadows ? 10.0f : 1.0f) * (light.CastShadows ? 10.0f : 1.0f) *
(light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)); (light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)) *
light.PriorityMultiplier;
} }
//find the lights with an active light volume //find the lights with an active light volume
@@ -377,6 +377,8 @@ namespace Barotrauma.Lights
public float Priority; public float Priority;
public float PriorityMultiplier = 1.0f;
private Vector2 lightTextureTargetSize; private Vector2 lightTextureTargetSize;
public Vector2 LightTextureTargetSize public Vector2 LightTextureTargetSize
@@ -537,7 +537,7 @@ namespace Barotrauma
float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * MaxHealth; float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * MaxHealth;
if (!invalidMessage && i < Sections.Length) if (!invalidMessage && i < Sections.Length)
{ {
SetDamage(i, damage); SetDamage(i, damage, isNetworkEvent: true);
} }
} }
} }
@@ -75,7 +75,7 @@ namespace Barotrauma.Networking
float gain = 1.0f; float gain = 1.0f;
float noiseGain = 0.0f; float noiseGain = 0.0f;
Vector3? position = null; Vector3? position = null;
if (character != null) if (character != null && !character.IsDead)
{ {
if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat) if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat)
{ {
@@ -8,7 +8,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
@@ -16,6 +15,10 @@ namespace Barotrauma.Networking
{ {
sealed class GameClient : NetworkMember sealed class GameClient : NetworkMember
{ {
public static readonly TimeSpan CampaignSaveTransferTimeOut = new TimeSpan(0, 0, seconds: 100);
//this should be longer than CampaignSaveTransferTimeOut - we shouldn't give up starting the round if we're still waiting for the save file
public static readonly TimeSpan LevelTransitionTimeOut = new TimeSpan(0, 0, seconds: 150);
public override bool IsClient => true; public override bool IsClient => true;
public override bool IsServer => false; public override bool IsServer => false;
@@ -76,6 +79,8 @@ namespace Barotrauma.Networking
Interrupted Interrupted
} }
private UInt16? debugStartGameCampaignSaveID;
private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted; private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted;
public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize; public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize;
@@ -512,6 +517,7 @@ namespace Barotrauma.Networking
DisplayInLoadingScreens = true DisplayInLoadingScreens = true
}; };
Quit(); Quit();
GUI.DisableHUD = false;
GameMain.ServerListScreen.Select(); GameMain.ServerListScreen.Select();
return; return;
} }
@@ -859,17 +865,24 @@ namespace Barotrauma.Networking
ContentFile file = ContentPackageManager.EnabledPackages.All ContentFile file = ContentPackageManager.EnabledPackages.All
.Select(p => .Select(p =>
p.Files.FirstOrDefault(f => f.Path == filePath)) p.Files.FirstOrDefault(f => f.Path == filePath))
.FirstOrDefault(f => !(f is null)); .FirstOrDefault(f => f is not null);
contentToPreload.AddIfNotNull(file); contentToPreload.AddIfNotNull(file);
} }
string campaignErrorInfo = string.Empty;
if (GameMain.GameSession?.Campaign is MultiPlayerCampaign campaign)
{
campaignErrorInfo = $" Round start save ID: {debugStartGameCampaignSaveID}, last save id: {campaign.LastSaveID}, pending save id: {campaign.PendingSaveID}.";
}
GameMain.GameSession.EventManager.PreloadContent(contentToPreload); GameMain.GameSession.EventManager.PreloadContent(contentToPreload);
int subEqualityCheckValue = inc.ReadInt32(); int subEqualityCheckValue = inc.ReadInt32();
if (subEqualityCheckValue != (Submarine.MainSub?.Info?.EqualityCheckVal ?? 0)) if (subEqualityCheckValue != (Submarine.MainSub?.Info?.EqualityCheckVal ?? 0))
{ {
string errorMsg = "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server." + string errorMsg =
" There may have been an error in receiving the up-to-date submarine file from the server."; "Submarine equality check failed. The submarine loaded at your end doesn't match the one loaded by the server. " +
$"There may have been an error in receiving the up-to-date submarine file from the server. Round init status: {roundInitStatus}." + campaignErrorInfo;
GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:SubsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg); throw new Exception(errorMsg);
} }
@@ -886,7 +899,7 @@ namespace Barotrauma.Networking
$"Mission equality check failed. Mission count doesn't match the server. " + $"Mission equality check failed. Mission count doesn't match the server. " +
$"Server: {string.Join(", ", serverMissionIdentifiers)}, " + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
$"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
$"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo;
GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsCountMismatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg); throw new Exception(errorMsg);
} }
@@ -899,7 +912,7 @@ namespace Barotrauma.Networking
$"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " + $"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " +
$"Server: {string.Join(", ", serverMissionIdentifiers)}, " + $"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
$"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " + $"client: {string.Join(", ", GameMain.GameSession.GameMode.Missions.Select(m => m.Prefab.Identifier))}, " +
$"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))})"; $"game session: {string.Join(", ", GameMain.GameSession.Missions.Select(m => m.Prefab.Identifier))}). Round init status: {roundInitStatus}." + campaignErrorInfo;
GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:MissionsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg); throw new Exception(errorMsg);
} }
@@ -922,7 +935,7 @@ namespace Barotrauma.Networking
", level value count: " + levelEqualityCheckValues.Count + ", level value count: " + levelEqualityCheckValues.Count +
", seed: " + Level.Loaded.Seed + ", seed: " + Level.Loaded.Seed +
", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" +
", mirrored: " + Level.Loaded.Mirrored + ")."; ", mirrored: " + Level.Loaded.Mirrored + "). Round init status: " + roundInitStatus + "." + campaignErrorInfo;
GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg); throw new Exception(errorMsg);
} }
@@ -1322,6 +1335,8 @@ namespace Barotrauma.Networking
eventErrorWritten = false; eventErrorWritten = false;
GameMain.NetLobbyScreen.StopWaitingForStartRound(); GameMain.NetLobbyScreen.StopWaitingForStartRound();
debugStartGameCampaignSaveID = null;
while (CoroutineManager.IsCoroutineRunning("EndGame")) while (CoroutineManager.IsCoroutineRunning("EndGame"))
{ {
EndCinematic?.Stop(); EndCinematic?.Stop();
@@ -1471,7 +1486,28 @@ namespace Barotrauma.Networking
roundInitStatus = RoundInitStatus.Interrupted; roundInitStatus = RoundInitStatus.Interrupted;
yield return CoroutineStatus.Failure; yield return CoroutineStatus.Failure;
} }
else if (campaign.Map == null)
if (NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID) ||
NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID))
{
campaign.PendingSaveID = campaignSaveID;
DateTime saveFileTimeOut = DateTime.Now + CampaignSaveTransferTimeOut;
while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID))
{
if (DateTime.Now > saveFileTimeOut)
{
GameStarted = true;
new GUIMessageBox(TextManager.Get("error"), TextManager.Get("campaignsavetransfer.timeout"));
GameMain.NetLobbyScreen.Select();
roundInitStatus = RoundInitStatus.Interrupted;
//use success status, even though this is a failure (no need to show a console error because we show it in the message box)
yield return CoroutineStatus.Success;
}
yield return new WaitForSeconds(0.1f);
}
}
if (campaign.Map == null)
{ {
GameStarted = true; GameStarted = true;
DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet)."); DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet).");
@@ -1480,30 +1516,14 @@ namespace Barotrauma.Networking
yield return CoroutineStatus.Failure; yield return CoroutineStatus.Failure;
} }
if (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID))
{
campaign.PendingSaveID = campaignSaveID;
DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0,0,60);
while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID))
{
if (DateTime.Now > saveFileTimeOut)
{
GameStarted = true;
DebugConsole.ThrowError("Failed to start campaign round (timed out while waiting for the up-to-date save file).");
GameMain.NetLobbyScreen.Select();
roundInitStatus = RoundInitStatus.Interrupted;
yield return CoroutineStatus.Failure;
}
yield return new WaitForSeconds(0.1f);
}
}
campaign.Map.SelectLocation(selectedLocationIndex); campaign.Map.SelectLocation(selectedLocationIndex);
LevelData levelData = nextLocationIndex > -1 ? LevelData levelData = nextLocationIndex > -1 ?
campaign.Map.Locations[nextLocationIndex].LevelData : campaign.Map.Locations[nextLocationIndex].LevelData :
campaign.Map.Connections[nextConnectionIndex].LevelData; campaign.Map.Connections[nextConnectionIndex].LevelData;
debugStartGameCampaignSaveID = campaign.LastSaveID;
if (roundSummary != null) if (roundSummary != null)
{ {
loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel, null); loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel, null);
@@ -1697,7 +1717,7 @@ namespace Barotrauma.Networking
yield return CoroutineStatus.Success; yield return CoroutineStatus.Success;
} }
if (GameMain.GameSession != null) { GameMain.GameSession.EndRound(endMessage, traitorResults, transitionType); } GameMain.GameSession?.EndRound(endMessage, traitorResults, transitionType);
ServerSettings.ServerDetailsChanged = true; ServerSettings.ServerDetailsChanged = true;
@@ -2587,31 +2607,24 @@ namespace Barotrauma.Networking
public void WriteCharacterInfo(IWriteMessage msg, string newName = null) public void WriteCharacterInfo(IWriteMessage msg, string newName = null)
{ {
msg.WriteBoolean(characterInfo == null); msg.WriteBoolean(characterInfo == null);
msg.WritePadBits();
if (characterInfo == null) { return; } if (characterInfo == null) { return; }
msg.WriteString(newName ?? string.Empty); var head = characterInfo.Head;
msg.WriteByte((byte)characterInfo.Head.Preset.TagSet.Count); var netInfo = new NetCharacterInfo(
foreach (Identifier tag in characterInfo.Head.Preset.TagSet) NewName: newName ?? string.Empty,
{ Tags: head.Preset.TagSet.ToImmutableArray(),
msg.WriteIdentifier(tag); HairIndex: (byte)head.HairIndex,
} BeardIndex: (byte)head.BeardIndex,
msg.WriteByte((byte)characterInfo.Head.HairIndex); MoustacheIndex: (byte)head.MoustacheIndex,
msg.WriteByte((byte)characterInfo.Head.BeardIndex); FaceAttachmentIndex: (byte)head.FaceAttachmentIndex,
msg.WriteByte((byte)characterInfo.Head.MoustacheIndex); SkinColor: head.SkinColor,
msg.WriteByte((byte)characterInfo.Head.FaceAttachmentIndex); HairColor: head.HairColor,
msg.WriteColorR8G8B8(characterInfo.Head.SkinColor); FacialHairColor: head.FacialHairColor,
msg.WriteColorR8G8B8(characterInfo.Head.HairColor); JobVariants: GameMain.NetLobbyScreen.JobPreferences.Select(NetJobVariant.FromJobVariant).ToImmutableArray());
msg.WriteColorR8G8B8(characterInfo.Head.FacialHairColor);
var jobPreferences = GameMain.NetLobbyScreen.JobPreferences; msg.WriteNetSerializableStruct(netInfo);
int count = Math.Min(jobPreferences.Count, 3);
msg.WriteByte((byte)count);
for (int i = 0; i < count; i++)
{
msg.WriteIdentifier(jobPreferences[i].Prefab.Identifier);
msg.WriteByte((byte)jobPreferences[i].Variant);
}
} }
public void Vote(VoteType voteType, object data) public void Vote(VoteType voteType, object data)
@@ -2864,12 +2877,14 @@ namespace Barotrauma.Networking
ClientPeer.Send(msg, DeliveryMethod.Reliable); ClientPeer.Send(msg, DeliveryMethod.Reliable);
} }
public bool SpectateClicked(GUIButton button, object userData) public bool SpectateClicked(GUIButton button, object _)
{ {
MultiPlayerCampaign campaign = MultiPlayerCampaign campaign =
GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ?
GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; GameMain.GameSession?.GameMode as MultiPlayerCampaign : null;
if (campaign != null && campaign.LastSaveID < campaign.PendingSaveID)
if (FileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave) ||
(campaign != null && NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID)))
{ {
new GUIMessageBox("", TextManager.Get("campaignfiletransferinprogress")); new GUIMessageBox("", TextManager.Get("campaignfiletransferinprogress"));
return false; return false;
@@ -92,10 +92,10 @@ namespace Barotrauma.Networking
Name = GameMain.Client.Name, Name = GameMain.Client.Name,
OwnerKey = ownerKey, OwnerKey = ownerKey,
SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id), SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id),
SteamAuthTicket = steamAuthTicket switch SteamAuthTicket = steamAuthTicket?.Data switch
{ {
null => Option<byte[]>.None(), null => Option<byte[]>.None(),
var ticket => Option<byte[]>.Some(ticket.Data) var ticketData => Option<byte[]>.Some(ticketData)
}, },
GameVersion = GameMain.Version.ToString(), GameVersion = GameMain.Version.ToString(),
Language = GameSettings.CurrentConfig.Language.Value Language = GameSettings.CurrentConfig.Language.Value
@@ -111,8 +111,25 @@ namespace Barotrauma.Networking
? NetworkConnection.TimeoutThresholdInGame ? NetworkConnection.TimeoutThresholdInGame
: NetworkConnection.TimeoutThreshold; : NetworkConnection.TimeoutThreshold;
IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection); try
{
IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection);
ProcessP2PData(inc);
}
catch (Exception e)
{
string errorMsg = $"Client failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}";
GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnP2PData:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#else
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
}
}
private void ProcessP2PData(IReadMessage inc)
{
var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc); var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (!packetHeader.IsServerMessage()) { return; } if (!packetHeader.IsServerMessage()) { return; }
@@ -141,15 +141,31 @@ namespace Barotrauma.Networking
if (remotePeer.DisconnectTime != null) { return; } if (remotePeer.DisconnectTime != null) { return; }
var peerPacketHeaders = INetSerializableStruct.Read<PeerPacketHeaders>(inc); try
{
ProcessP2PData(steamId, remotePeer, inc);
}
catch (Exception e)
{
string errorMsg = $"Server failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}";
GameAnalyticsManager.AddErrorEventOnce($"SteamP2POwnerPeer.OnP2PData:OwnerReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#else
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
}
}
PacketHeader packetHeader = peerPacketHeaders.PacketHeader; private void ProcessP2PData(ulong steamId, RemotePeer remotePeer, IReadMessage inc)
{
var (deliveryMethod, packetHeader, connectionInitialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (!remotePeer.Authenticated && !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep()) if (remotePeer is { Authenticated: false, Authenticating: false } && packetHeader.IsConnectionInitializationStep())
{ {
remotePeer.DisconnectTime = null; remotePeer.DisconnectTime = null;
ConnectionInitialization initialization = peerPacketHeaders.Initialization ?? throw new Exception("Initialization step missing"); ConnectionInitialization initialization = connectionInitialization ?? throw new Exception("Initialization step missing");
if (initialization == ConnectionInitialization.SteamTicketAndVersion) if (initialization == ConnectionInitialization.SteamTicketAndVersion)
{ {
remotePeer.Authenticating = true; remotePeer.Authenticating = true;
@@ -181,6 +197,7 @@ namespace Barotrauma.Networking
ForwardToServerProcess(outMsg); ForwardToServerProcess(outMsg);
} }
} }
public override void Update(float deltaTime) public override void Update(float deltaTime)
@@ -428,7 +428,7 @@ namespace Barotrauma.Networking
if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; } if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; }
Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None; Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None;
ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray(); ContentPackages = ExtractContentPackageInfo(ServerName, valueGetter).ToImmutableArray();
bool getBool(string key) bool getBool(string key)
{ {
@@ -437,8 +437,34 @@ namespace Barotrauma.Networking
} }
} }
private static ContentPackageInfo[] ExtractContentPackageInfo(Func<string, string?> valueGetter) private static ContentPackageInfo[] ExtractContentPackageInfo(string serverName, Func<string, string?> valueGetter)
{ {
//workaround to ServerRules queries truncating the values to 255 bytes
int individualPackageIndex = 0;
string? individualPackage = valueGetter($"contentpackage{individualPackageIndex}");
if (!individualPackage.IsNullOrEmpty())
{
List<ContentPackageInfo> contentPackages = new List<ContentPackageInfo>();
do
{
string[] splitPackageInfo = individualPackage.Split(',');
if (splitPackageInfo.Length != 3)
{
DebugConsole.Log(
$"Error in a server's content package list: malformed content package info ({individualPackage}).");
return Array.Empty<ContentPackageInfo>();
}
string name = splitPackageInfo[0];
string hash = splitPackageInfo[1];
ulong.TryParse(splitPackageInfo[2], out ulong id);
contentPackages.Add(new ContentPackageInfo(name, hash, Option<ContentPackageId>.Some(new SteamWorkshopId(id))));
individualPackageIndex++;
individualPackage = valueGetter($"contentpackage{individualPackageIndex}");
} while (!individualPackage.IsNullOrEmpty());
return contentPackages.ToArray();
}
string? joinedNames = valueGetter("contentpackage"); string? joinedNames = valueGetter("contentpackage");
string? joinedHashes = valueGetter("contentpackagehash"); string? joinedHashes = valueGetter("contentpackagehash");
string? joinedWorkshopIds = valueGetter("contentpackageid"); string? joinedWorkshopIds = valueGetter("contentpackageid");
@@ -448,9 +474,11 @@ namespace Barotrauma.Networking
#warning TODO: genericize #warning TODO: genericize
ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray(); ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray();
if (contentPackageNames.Length != contentPackageHashes.Length if (contentPackageNames.Length != contentPackageHashes.Length || contentPackageHashes.Length != contentPackageIds.Length)
|| contentPackageHashes.Length != contentPackageIds.Length)
{ {
DebugConsole.Log(
$"The number of names, hashes and Workshop IDs on server \"{serverName}\"" +
$" doesn't match: {contentPackageNames.Length} names ({string.Join(", ", contentPackageNames)}), {contentPackageHashes.Length} hashes, {contentPackageIds.Length} ids)");
return Array.Empty<ContentPackageInfo>(); return Array.Empty<ContentPackageInfo>();
} }
@@ -35,7 +35,7 @@ namespace Barotrauma
} }
private static Option<ServerInfo> InfoFromListEntry(Steamworks.Data.ServerInfo entry) => private static Option<ServerInfo> InfoFromListEntry(Steamworks.Data.ServerInfo entry) =>
entry.Name.IsNullOrEmpty() entry.Name.IsNullOrEmpty() || entry.Address is null
? Option<ServerInfo>.None() ? Option<ServerInfo>.None()
: Option<ServerInfo>.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort)) : Option<ServerInfo>.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort))
{ {
@@ -71,10 +71,10 @@ namespace Barotrauma
foreach (var lobby in lobbies) foreach (var lobby in lobbies)
{ {
string lobbyOwnerStr = lobby.GetData("lobbyowner"); string lobbyOwnerStr = lobby.GetData("lobbyowner") ?? "";
lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr); lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr);
string serverName = lobby.GetData("name"); string serverName = lobby.GetData("name") ?? "";
if (string.IsNullOrEmpty(serverName)) { continue; } if (string.IsNullOrEmpty(serverName)) { continue; }
var ownerId = SteamId.Parse(lobbyOwnerStr); var ownerId = SteamId.Parse(lobbyOwnerStr);
@@ -9,6 +9,9 @@ namespace Barotrauma.Networking
{ {
partial class ServerSettings : ISerializableEntity partial class ServerSettings : ISerializableEntity
{ {
private static readonly LocalizedString packetAmountTooltip = TextManager.Get("ServerSettingsMaxPacketAmountTooltip");
private static readonly RichString packetAmountTooltipWarning = RichString.Rich($"{packetAmountTooltip}\n\n‖color:gui.red‖{TextManager.Get("PacketLimitWarning")}‖end‖");
partial class NetPropertyData partial class NetPropertyData
{ {
public GUIComponent GUIComponent; public GUIComponent GUIComponent;
@@ -28,7 +31,15 @@ namespace Barotrauma.Networking
if (GUIComponent == null) return null; if (GUIComponent == null) return null;
else if (GUIComponent is GUITickBox tickBox) return tickBox.Selected; else if (GUIComponent is GUITickBox tickBox) return tickBox.Selected;
else if (GUIComponent is GUITextBox textBox) return textBox.Text; else if (GUIComponent is GUITextBox textBox) return textBox.Text;
else if (GUIComponent is GUIScrollBar scrollBar) return scrollBar.BarScrollValue; else if (GUIComponent is GUIScrollBar scrollBar)
{
if (property.PropertyType == typeof(int))
{
return (int)MathF.Floor(scrollBar.BarScrollValue);
}
return scrollBar.BarScrollValue;
}
else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) return radioButtonGroup.Selected; else if (GUIComponent is GUIRadioButtonGroup radioButtonGroup) return radioButtonGroup.Selected;
else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData; else if (GUIComponent is GUIDropDown dropdown) return dropdown.SelectedData;
else if (GUIComponent is GUINumberInput numInput) else if (GUIComponent is GUINumberInput numInput)
@@ -44,9 +55,9 @@ namespace Barotrauma.Networking
else if (GUIComponent is GUITextBox textBox) textBox.Text = (string)value; else if (GUIComponent is GUITextBox textBox) textBox.Text = (string)value;
else if (GUIComponent is GUIScrollBar scrollBar) else if (GUIComponent is GUIScrollBar scrollBar)
{ {
if (value.GetType() == typeof(int)) if (value is int i)
{ {
scrollBar.BarScrollValue = (int)value; scrollBar.BarScrollValue = i;
} }
else else
{ {
@@ -941,11 +952,58 @@ namespace Barotrauma.Networking
return true; return true;
}; };
GUILayoutGroup karmaAndDosLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.2f), antigriefingTab.RectTransform), isHorizontal: false);
GUILayoutGroup lowerLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true);
GUILayoutGroup upperLayout = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.5f), karmaAndDosLayout.RectTransform), isHorizontal: true);
// karma -------------------------------------------------------------------------- // karma --------------------------------------------------------------------------
var karmaBox = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform), TextManager.Get("ServerSettingsUseKarma")); var karmaBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsUseKarma"));
GetPropertyData(nameof(KarmaEnabled)).AssignGUIComponent(karmaBox); GetPropertyData(nameof(KarmaEnabled)).AssignGUIComponent(karmaBox);
var enableDosProtection = new GUITickBox(new RectTransform(new Vector2(0.5f, 1f), upperLayout.RectTransform), TextManager.Get("ServerSettingsEnableDoSProtection"))
{
ToolTip = TextManager.Get("ServerSettingsEnableDoSProtectionTooltip")
};
GetPropertyData(nameof(EnableDoSProtection)).AssignGUIComponent(enableDosProtection);
CreateLabeledSlider(lowerLayout, "ServerSettingsMaxPacketAmount", out GUIScrollBar maxPacketSlider, out GUITextBlock maxPacketSliderLabel);
LocalizedString maxPacketCountLabel = maxPacketSliderLabel.Text;
maxPacketSlider.Step = 0.001f;
maxPacketSlider.Range = new Vector2(PacketLimitMin, PacketLimitMax);
maxPacketSlider.ToolTip = packetAmountTooltip;
maxPacketSlider.OnMoved = (scrollBar, _) =>
{
GUITextBlock textBlock = (GUITextBlock)scrollBar.UserData;
int value = (int)MathF.Floor(scrollBar.BarScrollValue);
LocalizedString valueText = value > PacketLimitMin
? value.ToString()
: TextManager.Get("ServerSettingsNoLimit");
switch (value)
{
case <= PacketLimitMin:
textBlock.TextColor = GUIStyle.Green;
scrollBar.ToolTip = packetAmountTooltip;
break;
case < PacketLimitWarning:
textBlock.TextColor = GUIStyle.Red;
scrollBar.ToolTip = packetAmountTooltipWarning;
break;
default:
textBlock.TextColor = GUIStyle.TextColorNormal;
scrollBar.ToolTip = packetAmountTooltip;
break;
}
textBlock.Text = $"{maxPacketCountLabel} {valueText}";
return true;
};
GetPropertyData(nameof(MaxPacketAmount)).AssignGUIComponent(maxPacketSlider);
maxPacketSlider.OnMoved(maxPacketSlider, maxPacketSlider.BarScroll);
karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform)); karmaPresetDD = new GUIDropDown(new RectTransform(new Vector2(1.0f, 0.05f), antigriefingTab.RectTransform));
foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys) foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys)
{ {
@@ -276,6 +276,8 @@ namespace Barotrauma.Networking
if (GameMain.Client?.Character != null) if (GameMain.Client?.Character != null)
{ {
var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default; var messageType = !ForceLocal && ChatMessage.CanUseRadio(GameMain.Client.Character, out _) ? ChatMessageType.Radio : ChatMessageType.Default;
if (GameMain.Client.Character.IsDead) { messageType = ChatMessageType.Dead; }
GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); GameMain.Client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
} }
//encode audio and enqueue it //encode audio and enqueue it
@@ -146,7 +146,7 @@ namespace Barotrauma.Networking
if ((client.VoipSound.CurrentAmplitude * client.VoipSound.Gain * GameMain.SoundManager.GetCategoryGainMultiplier("voip")) > 0.1f) //TODO: might need to tweak if ((client.VoipSound.CurrentAmplitude * client.VoipSound.Gain * GameMain.SoundManager.GetCategoryGainMultiplier("voip")) > 0.1f) //TODO: might need to tweak
{ {
if (client.Character != null && !client.Character.Removed) if (client.Character != null && !client.Character.Removed && !client.Character.IsDead)
{ {
Vector3 clientPos = new Vector3(client.Character.WorldPosition.X, client.Character.WorldPosition.Y, 0.0f); Vector3 clientPos = new Vector3(client.Character.WorldPosition.X, client.Character.WorldPosition.Y, 0.0f);
Vector3 listenerPos = GameMain.SoundManager.ListenerPosition; Vector3 listenerPos = GameMain.SoundManager.ListenerPosition;
@@ -169,7 +169,10 @@ namespace Barotrauma
sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language);
if (ContentPackageManager.EnabledPackages.All != null) if (ContentPackageManager.EnabledPackages.All != null)
{ {
sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); sb.AppendLine("Selected content packages: " +
(!ContentPackageManager.EnabledPackages.All.Any() ?
"None" :
string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => $"{c.Name} ({c.Hash?.ShortRepresentation ?? "unknown"})"))));
} }
sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed));
sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")"));
@@ -18,7 +18,7 @@ namespace Barotrauma
public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; } public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; }
private GUIButton nextButton; private GUIButton nextButton;
private GUILayoutGroup characterInfoColumns; private GUIListBox characterInfoColumns;
public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable<SubmarineInfo> submarines, IEnumerable<CampaignMode.SaveInfo> saveFiles = null) public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable<SubmarineInfo> submarines, IEnumerable<CampaignMode.SaveInfo> saveFiles = null)
: base(newGameContainer, loadGameContainer) : base(newGameContainer, loadGameContainer)
@@ -249,11 +249,7 @@ namespace Barotrauma
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform), new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform),
TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft); TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft);
characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true) characterInfoColumns = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true);
{
Stretch = true,
RelativeSpacing = 0.01f
};
var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f),
secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true) secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true)
@@ -306,8 +302,8 @@ namespace Barotrauma
for (int i = 0; i < characterInfos.Count; i++) for (int i = 0; i < characterInfos.Count; i++)
{ {
var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f / characterInfos.Count, 1.0f), var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(Math.Max(1.0f / characterInfos.Count, 0.33f), 1.0f),
characterInfoColumns.RectTransform)); characterInfoColumns.Content.RectTransform));
var (characterInfo, job) = characterInfos[i]; var (characterInfo, job) = characterInfos[i];
@@ -202,18 +202,21 @@ namespace Barotrauma
case CampaignMode.InteractionType.PurchaseSub: case CampaignMode.InteractionType.PurchaseSub:
submarineSelection?.Update(); submarineSelection?.Update();
break; break;
case CampaignMode.InteractionType.Crew: case CampaignMode.InteractionType.Crew:
CrewManagement?.Update(); CrewManagement?.Update();
break; break;
case CampaignMode.InteractionType.Store: case CampaignMode.InteractionType.Store:
Store?.Update(deltaTime); Store?.Update(deltaTime);
break; break;
case CampaignMode.InteractionType.MedicalClinic: case CampaignMode.InteractionType.MedicalClinic:
MedicalClinic?.Update(deltaTime); MedicalClinic?.Update(deltaTime);
break; break;
case CampaignMode.InteractionType.Map:
if (StartButton != null)
{
StartButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap) && Character.Controlled is { IsIncapacitated: false };
}
break;
} }
} }
@@ -568,7 +571,6 @@ namespace Barotrauma
StartButton.Visible = false; StartButton.Visible = false;
missionList.Enabled = false; missionList.Enabled = false;
} }
//locationInfoPanel?.UpdateAuto(1.0f);
} }
public void SelectTab(CampaignMode.InteractionType tab, Character npc = null) public void SelectTab(CampaignMode.InteractionType tab, Character npc = null)
@@ -343,7 +343,7 @@ namespace Barotrauma
editorContainer.ClearChildren(); editorContainer.ClearChildren();
paramsList.Content.ClearChildren(); paramsList.Content.ClearChildren();
foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams) foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams.OrderBy(p => p.Name))
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paramsList.Content.RectTransform) { MinSize = new Point(0, 20) }, new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paramsList.Content.RectTransform) { MinSize = new Point(0, 20) },
genParams.Identifier.Value) genParams.Identifier.Value)
@@ -359,7 +359,7 @@ namespace Barotrauma
editorContainer.ClearChildren(); editorContainer.ClearChildren();
caveParamsList.Content.ClearChildren(); caveParamsList.Content.ClearChildren();
foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams) foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams.OrderBy(p => p.Name))
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), caveParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), caveParamsList.Content.RectTransform) { MinSize = new Point(0, 20) },
genParams.Name) genParams.Name)
@@ -375,7 +375,7 @@ namespace Barotrauma
editorContainer.ClearChildren(); editorContainer.ClearChildren();
ruinParamsList.Content.ClearChildren(); ruinParamsList.Content.ClearChildren();
foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams) foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams.OrderBy(p => p.Identifier))
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), ruinParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), ruinParamsList.Content.RectTransform) { MinSize = new Point(0, 20) },
genParams.Name) genParams.Name)
@@ -391,7 +391,7 @@ namespace Barotrauma
editorContainer.ClearChildren(); editorContainer.ClearChildren();
outpostParamsList.Content.ClearChildren(); outpostParamsList.Content.ClearChildren();
foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams) foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams.OrderBy(p => p.Name))
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), outpostParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), outpostParamsList.Content.RectTransform) { MinSize = new Point(0, 20) },
genParams.Name) genParams.Name)
@@ -2244,9 +2244,9 @@ namespace Barotrauma
List<ContextMenuOption> rankOptions = new List<ContextMenuOption>(); List<ContextMenuOption> rankOptions = new List<ContextMenuOption>();
foreach (PermissionPreset rank in PermissionPreset.List) foreach (PermissionPreset rank in PermissionPreset.List)
{ {
rankOptions.Add(new ContextMenuOption(rank.Name, isEnabled: true, onSelected: () => rankOptions.Add(new ContextMenuOption(rank.DisplayName, isEnabled: true, onSelected: () =>
{ {
LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.Name)); LocalizedString label = TextManager.GetWithVariables(rank.Permissions == ClientPermissions.None ? "clearrankprompt" : "giverankprompt", ("[user]", client.Name), ("[rank]", rank.DisplayName));
GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") }); GUIMessageBox msgBox = new GUIMessageBox(string.Empty, label, new[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
msgBox.Buttons[0].OnClicked = delegate msgBox.Buttons[0].OnClicked = delegate
@@ -2350,7 +2350,7 @@ namespace Barotrauma
}; };
foreach (PermissionPreset permissionPreset in PermissionPreset.List) foreach (PermissionPreset permissionPreset in PermissionPreset.List)
{ {
rankDropDown.AddItem(permissionPreset.Name, permissionPreset, permissionPreset.Description); rankDropDown.AddItem(permissionPreset.DisplayName, permissionPreset, permissionPreset.Description);
} }
rankDropDown.AddItem(TextManager.Get("CustomRank"), null); rankDropDown.AddItem(TextManager.Get("CustomRank"), null);
@@ -1,16 +1,16 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Steam;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.Xna.Framework.Input;
using Barotrauma.IO;
using Barotrauma.Steam;
namespace Barotrauma namespace Barotrauma
{ {
@@ -3546,10 +3546,45 @@ namespace Barotrauma
TextManager.Get("LoadingVanillaSubmarineHeader"), TextManager.Get("LoadingVanillaSubmarineHeader"),
TextManager.Get("LoadingVanillaSubmarineDesc")); TextManager.Get("LoadingVanillaSubmarineDesc"));
public void LoadSub(SubmarineInfo info) public void LoadSub(SubmarineInfo info, bool checkIdConflicts = true)
{ {
Submarine.Unload(); Submarine.Unload();
Submarine selectedSub = null; Submarine selectedSub = null;
if (checkIdConflicts)
{
Dictionary<int, Identifier> entities = new Dictionary<int, Identifier>();
foreach (var subElement in info.SubmarineElement.Elements())
{
int id = subElement.GetAttributeInt("ID", -1);
Identifier identifier = subElement.GetAttributeIdentifier("identifier", string.Empty);
if (entities.TryGetValue(id, out Identifier duplicateEntity))
{
var errorMsg = new GUIMessageBox(
TextManager.Get("error"),
TextManager.GetWithVariables("subeditor.duplicateiderror",
("[entity1]", $"{duplicateEntity} ({id})"),
("[entity2]", $"{identifier} ({id})")),
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
errorMsg.Buttons[0].OnClicked = (bnt, userdata) =>
{
subElement.Remove();
LoadSub(info, checkIdConflicts: false);
errorMsg.Close();
return true;
};
errorMsg.Buttons[1].OnClicked = (bnt, userdata) =>
{
LoadSub(info, checkIdConflicts: false);
errorMsg.Close();
return true;
};
return;
}
entities.Add(id, identifier);
}
}
try try
{ {
selectedSub = new Submarine(info); selectedSub = new Submarine(info);
@@ -5263,7 +5298,7 @@ namespace Barotrauma
} }
} }
if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default) if (PlayerInput.KeyHit(InputType.Use) && mode == Mode.Default)
{ {
if (dummyCharacter != null) if (dummyCharacter != null)
{ {
@@ -5354,6 +5389,16 @@ namespace Barotrauma
else else
{ {
var selectables = MapEntity.mapEntityList.Where(entity => entity.SelectableInEditor).ToList(); var selectables = MapEntity.mapEntityList.Where(entity => entity.SelectableInEditor).ToList();
foreach (var item in Item.ItemList)
{
//attached wires are not normally selectable (by clicking),
//but let's select them manually when selecting all
var wire = item.GetComponent<Wire>();
if (wire != null && wire.Connections.None(c => c == null) && !selectables.Contains(item))
{
selectables.Add(item);
}
}
lock (selectables) lock (selectables)
{ {
selectables.ForEach(MapEntity.AddSelection); selectables.ForEach(MapEntity.AddSelection);
@@ -94,7 +94,7 @@ namespace Barotrauma.Steam
CanBeFocused = false CanBeFocused = false
}; };
var itemTitle = new GUITextBlock(new RectTransform(Vector2.One, itemFrame.RectTransform), var itemTitle = new GUITextBlock(new RectTransform(Vector2.One, itemFrame.RectTransform),
text: item.Title); text: item.Title ?? "");
var itemDownloadProgress var itemDownloadProgress
= new GUIProgressBar(new RectTransform((0.5f, 0.75f), = new GUIProgressBar(new RectTransform((0.5f, 0.75f),
itemFrame.RectTransform, Anchor.CenterRight), 0.0f) itemFrame.RectTransform, Anchor.CenterRight), 0.0f)
@@ -105,7 +105,8 @@ namespace Barotrauma.Steam
currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation)));
currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.UgcId))); currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp
=> cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : "")));
currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString()); currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString());
currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString());
currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString()); currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString());
@@ -16,6 +16,9 @@ namespace Barotrauma.Steam
private static readonly List<Identifier> initializationErrors = new List<Identifier>(); private static readonly List<Identifier> initializationErrors = new List<Identifier>();
public static IReadOnlyList<Identifier> InitializationErrors => initializationErrors; public static IReadOnlyList<Identifier> InitializationErrors => initializationErrors;
private static bool IsInitializedProjectSpecific
=> Steamworks.SteamClient.IsValid && Steamworks.SteamClient.IsLoggedOn;
private static void InitializeProjectSpecific() private static void InitializeProjectSpecific()
{ {
if (IsInitialized) { return; } if (IsInitialized) { return; }
@@ -23,7 +26,6 @@ namespace Barotrauma.Steam
try try
{ {
Steamworks.SteamClient.Init(AppID, false); Steamworks.SteamClient.Init(AppID, false);
IsInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid;
if (IsInitialized) if (IsInitialized)
{ {
@@ -43,13 +45,11 @@ namespace Barotrauma.Steam
} }
catch (DllNotFoundException) catch (DllNotFoundException)
{ {
IsInitialized = false;
initializationErrors.Add("SteamDllNotFound".ToIdentifier()); initializationErrors.Add("SteamDllNotFound".ToIdentifier());
} }
catch (Exception e) catch (Exception e)
{ {
DebugConsole.ThrowError("SteamManager initialization threw an exception", e); DebugConsole.ThrowError("SteamManager initialization threw an exception", e);
IsInitialized = false;
initializationErrors.Add("SteamClientInitFailed".ToIdentifier()); initializationErrors.Add("SteamClientInitFailed".ToIdentifier());
} }
@@ -111,7 +111,7 @@ namespace Barotrauma.Steam
{ {
await Task.Yield(); await Task.Yield();
string thumbnailUrl = item.PreviewImageUrl; string? thumbnailUrl = item.PreviewImageUrl;
if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; } if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; }
var client = new RestClient(thumbnailUrl); var client = new RestClient(thumbnailUrl);
var request = new RestRequest(".", Method.GET); var request = new RestRequest(".", Method.GET);
@@ -17,7 +17,7 @@ namespace Barotrauma.Steam
private string ExtractTitle(ItemOrPackage itemOrPackage) private string ExtractTitle(ItemOrPackage itemOrPackage)
=> itemOrPackage.TryGet(out ContentPackage package) => itemOrPackage.TryGet(out ContentPackage package)
? package.Name ? package.Name
: ((Steamworks.Ugc.Item)itemOrPackage).Title; : (((Steamworks.Ugc.Item)itemOrPackage).Title ?? "");
private void CreateWorkshopItemDetailContainer( private void CreateWorkshopItemDetailContainer(
GUIFrame parent, GUIFrame parent,
@@ -340,6 +340,8 @@ namespace Barotrauma.Steam
subscribeButton.OnClicked = (button, o) => subscribeButton.OnClicked = (button, o) =>
{ {
if (!SteamManager.IsInitialized) { return false; }
if (!workshopItem.IsSubscribed) if (!workshopItem.IsSubscribed)
{ {
workshopItem.Subscribe(); workshopItem.Subscribe();
@@ -360,6 +362,8 @@ namespace Barotrauma.Steam
new RectTransform(Vector2.Zero, subscribeButton.RectTransform), new RectTransform(Vector2.Zero, subscribeButton.RectTransform),
onUpdate: (deltaTime, component) => onUpdate: (deltaTime, component) =>
{ {
if (!SteamManager.IsInitialized) { return; }
if (subscribeButtonSprite.Style is { Identifier: { } styleId }) if (subscribeButtonSprite.Style is { Identifier: { } styleId })
{ {
if (workshopItem.IsSubscribed && styleId != minusButton) if (workshopItem.IsSubscribed && styleId != minusButton)
@@ -380,6 +384,8 @@ namespace Barotrauma.Steam
new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center), new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center),
onDraw: (spriteBatch, component) => onDraw: (spriteBatch, component) =>
{ {
if (!SteamManager.IsInitialized) { return; }
bool visible = workshopItem.IsSubscribed bool visible = workshopItem.IsSubscribed
&& (workshopItem.IsDownloading && (workshopItem.IsDownloading
|| workshopItem.IsDownloadPending || workshopItem.IsDownloadPending
@@ -407,6 +413,8 @@ namespace Barotrauma.Steam
}, },
onUpdate: (deltaTime, component) => onUpdate: (deltaTime, component) =>
{ {
if (!SteamManager.IsInitialized) { return; }
displayedDownloadAmount = Math.Min( displayedDownloadAmount = Math.Min(
workshopItem.DownloadAmount, workshopItem.DownloadAmount,
MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f)); MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f));
@@ -450,7 +458,7 @@ namespace Barotrauma.Steam
var title = new GUITextBlock( var title = new GUITextBlock(
new RectTransform(Vector2.One, itemLayout.RectTransform), new RectTransform(Vector2.One, itemLayout.RectTransform),
workshopItem.Title, font: GUIStyle.Font) workshopItem.Title ?? "", font: GUIStyle.Font)
{ {
CanBeFocused = false CanBeFocused = false
}; };
@@ -570,7 +578,7 @@ namespace Barotrauma.Steam
var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform)); var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform));
var selectedTitle = var selectedTitle =
new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title, new GUITextBlock(new RectTransform((1.0f, 0.5f), titleAndAuthorLayout.RectTransform), workshopItem.Title ?? "",
font: GUIStyle.LargeFont); font: GUIStyle.LargeFont);
var author = workshopItem.Owner; var author = workshopItem.Owner;
@@ -682,9 +690,9 @@ namespace Barotrauma.Steam
TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) => TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) =>
{ {
authorButton.Text = author.Name; authorButton.Text = author.Name ?? "";
authorButton.RectTransform.NonScaledSize = authorButton.RectTransform.NonScaledSize =
((int)(authorButton.Font.MeasureString(author.Name).X + authorPadding.X + authorPadding.Z), ((int)(authorButton.Font.MeasureString(author.Name ?? "").X + authorPadding.X + authorPadding.Z),
authorButton.RectTransform.NonScaledSize.Y); authorButton.RectTransform.NonScaledSize.Y);
}); });
@@ -769,7 +777,7 @@ namespace Barotrauma.Steam
var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform), var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform),
TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont); TextManager.Get("WorkshopItemTags"), font: GUIStyle.SubHeadingFont);
CreateTagsList(workshopItem.Tags.ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false); CreateTagsList((workshopItem.Tags ?? Array.Empty<string>()).ToIdentifiers(), new RectTransform((0.97f, 0.3f), statsVertical0.RectTransform), canBeFocused: false);
#endregion #endregion
var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform)); var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform));
@@ -0,0 +1,51 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace Barotrauma
{
internal class LeakyBucket
{
private readonly Queue<Action> queue;
private readonly int capacity;
private readonly float cooldownInSeconds;
private float timer;
public LeakyBucket(float cooldownInSeconds, int capacity)
{
this.cooldownInSeconds = cooldownInSeconds;
this.capacity = capacity;
queue = new Queue<Action>(capacity);
}
public void Update(float deltaTime)
{
if (timer > 0f)
{
timer -= deltaTime;
return;
}
if (queue.Count is 0) { return; }
TryDequeue();
}
private void TryDequeue()
{
timer = cooldownInSeconds;
if (queue.TryDequeue(out var action))
{
action.Invoke();
}
}
public bool TryEnqueue(Action item)
{
if (queue.Count >= capacity) { return false; }
queue.Enqueue(item);
return true;
}
}
}
@@ -142,11 +142,17 @@ namespace Barotrauma
GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); GameMain.Instance.GraphicsDevice.SetRenderTarget(rt);
GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); GameMain.Instance.GraphicsDevice.Clear(Color.Transparent);
spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform); DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)));
Submarine.Draw(spriteBatch); DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => (e is not Structure || e.SpriteDepth < 0.9f)));
Submarine.DrawFront(spriteBatch); DrawBatch(() => Submarine.DrawDamageable(spriteBatch, null, editing: true));
Submarine.DrawDamageable(spriteBatch, null); DrawBatch(() => Submarine.DrawFront(spriteBatch, editing: true));
spriteBatch.End();
void DrawBatch(Action drawAction)
{
spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform);
drawAction.Invoke();
spriteBatch.End();
}
GameMain.Instance.GraphicsDevice.SetRenderTarget(null); GameMain.Instance.GraphicsDevice.SetRenderTarget(null);
GameMain.Instance.GraphicsDevice.Viewport = prevViewport; GameMain.Instance.GraphicsDevice.Viewport = prevViewport;
@@ -6,14 +6,14 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.0.9.0</Version> <Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations> <Configurations>Debug;Release;Unstable</Configurations>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<WarningsAsErrors>;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors> <WarningsAsErrors>;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+2 -2
View File
@@ -6,14 +6,14 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.0.9.0</Version> <Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations> <Configurations>Debug;Release;Unstable</Configurations>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<WarningsAsErrors>;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors> <WarningsAsErrors>;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.0.9.0</Version> <Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -14,7 +14,7 @@
<Configurations>Debug;Release;Unstable</Configurations> <Configurations>Debug;Release;Unstable</Configurations>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<WarningsAsErrors>;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors> <WarningsAsErrors>;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -6,14 +6,14 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.0.9.0</Version> <Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations> <Configurations>Debug;Release;Unstable</Configurations>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<WarningsAsErrors>;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors> <WarningsAsErrors>;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+2 -2
View File
@@ -6,14 +6,14 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.0.9.0</Version> <Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations> <Configurations>Debug;Release;Unstable</Configurations>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<WarningsAsErrors>;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors> <WarningsAsErrors>;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -13,6 +13,15 @@ namespace Barotrauma
{ {
static partial class DebugConsole static partial class DebugConsole
{ {
private static readonly RateLimiter rateLimiter = new(
maxRequests: 50,
expiryInSeconds: 5,
punishmentRules: new[]
{
(RateLimitAction.OnLimitReached, RateLimitPunishment.Announce),
(RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick)
});
public partial class Command public partial class Command
{ {
/// <summary> /// <summary>
@@ -608,12 +617,12 @@ namespace Barotrauma
NewMessage("Valid ranks are:", Color.White); NewMessage("Valid ranks are:", Color.White);
foreach (PermissionPreset permissionPreset in PermissionPreset.List) foreach (PermissionPreset permissionPreset in PermissionPreset.List)
{ {
NewMessage(" - " + permissionPreset.Name, Color.White); NewMessage(" - " + permissionPreset.DisplayName, Color.White);
} }
ShowQuestionPrompt("Rank to grant to \"" + client.Name + "\"?", (rank) => ShowQuestionPrompt("Rank to grant to \"" + client.Name + "\"?", (rank) =>
{ {
PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase));
if (preset == null) if (preset == null)
{ {
ThrowError("Rank \"" + rank + "\" not found."); ThrowError("Rank \"" + rank + "\" not found.");
@@ -622,7 +631,7 @@ namespace Barotrauma
client.SetPermissions(preset.Permissions, preset.PermittedCommands); client.SetPermissions(preset.Permissions, preset.PermittedCommands);
GameMain.Server.UpdateClientPermissions(client); GameMain.Server.UpdateClientPermissions(client);
NewMessage("Assigned the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); NewMessage("Assigned the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White);
}, args, 1); }, args, 1);
}); });
@@ -1992,6 +2001,7 @@ namespace Barotrauma
"freecam", "freecam",
(Client client, Vector2 cursorWorldPos, string[] args) => (Client client, Vector2 cursorWorldPos, string[] args) =>
{ {
client.UsingFreeCam = true;
GameMain.Server.SetClientCharacter(client, null); GameMain.Server.SetClientCharacter(client, null);
client.SpectateOnly = true; client.SpectateOnly = true;
} }
@@ -2105,7 +2115,7 @@ namespace Barotrauma
} }
string rank = string.Join("", args.Skip(1)); string rank = string.Join("", args.Skip(1));
PermissionPreset preset = PermissionPreset.List.Find(p => p.Name.Equals(rank, StringComparison.OrdinalIgnoreCase)); PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName.Equals(rank, StringComparison.OrdinalIgnoreCase));
if (preset == null) if (preset == null)
{ {
GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient, Color.Red); GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient, Color.Red);
@@ -2114,8 +2124,8 @@ namespace Barotrauma
client.SetPermissions(preset.Permissions, preset.PermittedCommands); client.SetPermissions(preset.Permissions, preset.PermittedCommands);
GameMain.Server.UpdateClientPermissions(client); GameMain.Server.UpdateClientPermissions(client);
GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.Name}\" to {client.Name}.", senderClient); GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.DisplayName}\" to {client.Name}.", senderClient);
NewMessage(senderClient.Name + " granted the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White); NewMessage(senderClient.Name + " granted the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White);
} }
); );
@@ -2509,26 +2519,37 @@ namespace Barotrauma
foreach (Item item in Item.ItemList) foreach (Item item in Item.ItemList)
{ {
item.TryCreateServerEventSpam(); item.TryCreateServerEventSpam();
item.CreateStatusEvent(); item.CreateStatusEvent(loadingRound: false);
} }
foreach (Structure wall in Structure.WallList) foreach (Structure wall in Structure.WallList)
{ {
GameMain.Server.CreateEntityEvent(wall); GameMain.Server.CreateEntityEvent(wall);
} }
})); }));
commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that stalls each file transfer packet by the specified duration.", (string[] args) => commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that makes all file transfers take at least the specified duration.", (string[] args) =>
{ {
float seconds = 0.0f; float seconds = 0.0f;
if (args.Length > 0) if (args.Length > 0)
{ {
float.TryParse(args[0], out seconds); float.TryParse(args[0], out seconds);
} }
GameMain.Server.FileSender.StallPacketsTime = seconds; GameMain.Server.FileSender.ForceMinimumFileTransferDuration = seconds;
NewMessage("Set file transfer stall time to " + seconds); NewMessage("Set file transfer stall time to " + seconds);
})); }));
#endif #endif
} }
public static void ServerRead(IReadMessage inc, Client sender)
{
string consoleCommand = inc.ReadString();
float cursorX = inc.ReadSingle();
float cursorY = inc.ReadSingle();
if (rateLimiter.IsLimitReached(sender)) { return; }
ExecuteClientCommand(sender, new Vector2(cursorX, cursorY), consoleCommand);
}
public static void ExecuteClientCommand(Client client, Vector2 cursorWorldPos, string command) public static void ExecuteClientCommand(Client client, Vector2 cursorWorldPos, string command)
{ {
if (GameMain.Server == null) return; if (GameMain.Server == null) return;
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Text;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.Networking; using Barotrauma.Networking;
@@ -11,25 +12,16 @@ namespace Barotrauma
{ {
internal partial class MedicalClinic internal partial class MedicalClinic
{ {
private enum RateLimitResult // allow 20 requests per 5 seconds, announce to chat if the limit is reached
{ private readonly RateLimiter rateLimiter = new(
OK, maxRequests: RateLimitMaxRequests,
LimitReached expiryInSeconds: RateLimitExpiry,
} punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce));
private struct RateLimitInfo
{
public int Requests;
public const int MaxRequests = 10;
public DateTimeOffset Expiry;
}
private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry); private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry);
private readonly List<AfflictionSubscriber> afflictionSubscribers = new(); private readonly List<AfflictionSubscriber> afflictionSubscribers = new();
private readonly Dictionary<Client, RateLimitInfo> rateLimits = new();
public void ServerRead(IReadMessage inc, Client sender) public void ServerRead(IReadMessage inc, Client sender)
{ {
NetworkHeader header = (NetworkHeader)inc.ReadByte(); NetworkHeader header = (NetworkHeader)inc.ReadByte();
@@ -65,7 +57,7 @@ namespace Barotrauma
private void ProcessNewAddition(IReadMessage inc, Client client) private void ProcessNewAddition(IReadMessage inc, Client client)
{ {
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } if (rateLimiter.IsLimitReached(client)) { return; }
NetCrewMember newCrewMember = INetSerializableStruct.Read<NetCrewMember>(inc); NetCrewMember newCrewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
InsertPendingCrewMember(newCrewMember); InsertPendingCrewMember(newCrewMember);
@@ -74,7 +66,7 @@ namespace Barotrauma
private void ProcessAddEverything(Client client) private void ProcessAddEverything(Client client)
{ {
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } if (rateLimiter.IsLimitReached(client)) { return; }
AddEverythingToPending(); AddEverythingToPending();
ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client); ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client);
} }
@@ -92,7 +84,7 @@ namespace Barotrauma
private void ProcessNewRemoval(IReadMessage inc, Client client) private void ProcessNewRemoval(IReadMessage inc, Client client)
{ {
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } if (rateLimiter.IsLimitReached(client)) { return; }
NetRemovedAffliction removed = INetSerializableStruct.Read<NetRemovedAffliction>(inc); NetRemovedAffliction removed = INetSerializableStruct.Read<NetRemovedAffliction>(inc);
RemovePendingAffliction(removed.CrewMember, removed.Affliction); RemovePendingAffliction(removed.CrewMember, removed.Affliction);
@@ -101,14 +93,14 @@ namespace Barotrauma
private void ProcessRequestedPending(Client client) private void ProcessRequestedPending(Client client)
{ {
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } if (rateLimiter.IsLimitReached(client)) { return; }
ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client); ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.REQUEST_PENDING, DeliveryMethod.Reliable, targetClient: client);
} }
private void ProcessHealing(Client client) private void ProcessHealing(Client client)
{ {
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } if (rateLimiter.IsLimitReached(client)) { return; }
HealRequestResult result = HealAllPending(client: client); HealRequestResult result = HealAllPending(client: client);
ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client); ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client);
@@ -116,7 +108,7 @@ namespace Barotrauma
private void ProcessClearing(Client client) private void ProcessClearing(Client client)
{ {
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } if (rateLimiter.IsLimitReached(client)) { return; }
if (!PendingHeals.Any()) { return; } if (!PendingHeals.Any()) { return; }
@@ -126,7 +118,7 @@ namespace Barotrauma
private void ProcessRequestedAfflictions(IReadMessage inc, Client client) private void ProcessRequestedAfflictions(IReadMessage inc, Client client)
{ {
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; } if (rateLimiter.IsLimitReached(client)) { return; }
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc); NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
@@ -135,6 +127,17 @@ namespace Barotrauma
ImmutableArray<NetAffliction> pendingAfflictions = ImmutableArray<NetAffliction>.Empty; ImmutableArray<NetAffliction> pendingAfflictions = ImmutableArray<NetAffliction>.Empty;
int infoId = 0; int infoId = 0;
if (foundInfo is null)
{
StringBuilder sb = new();
foreach (CharacterInfo character in GetCrewCharacters())
{
sb.AppendLine($" - {character.DisplayName} ({character.ID})");
}
DebugConsole.ThrowError($"Could not find the requested crew member with ID {crewMember.CharacterInfoID}.\n{sb}");
}
if (foundInfo is { Character.CharacterHealth: { } health }) if (foundInfo is { Character.CharacterHealth: { } health })
{ {
pendingAfflictions = GetAllAfflictions(health); pendingAfflictions = GetAllAfflictions(health);
@@ -158,32 +161,6 @@ namespace Barotrauma
ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client); ServerSend(writeCrewMember, NetworkHeader.REQUEST_AFFLICTIONS, DeliveryMethod.Unreliable, client);
} }
private RateLimitResult CheckRateLimit(Client client)
{
if (rateLimits.TryGetValue(client, out RateLimitInfo rateLimitInfo))
{
if (rateLimitInfo.Expiry < DateTimeOffset.Now)
{
rateLimitInfo.Expiry = DateTimeOffset.Now.AddSeconds(5);
rateLimitInfo.Requests = 1;
}
else
{
if (rateLimitInfo.Requests > RateLimitInfo.MaxRequests) { return RateLimitResult.LimitReached; }
rateLimitInfo.Requests++;
}
rateLimits[client] = rateLimitInfo;
}
else
{
rateLimits.Add(client, new RateLimitInfo { Requests = 1, Expiry = DateTimeOffset.Now.AddSeconds(5) });
}
return RateLimitResult.OK;
}
private IWriteMessage StartSending() private IWriteMessage StartSending()
{ {
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
@@ -4,6 +4,7 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
@@ -135,6 +136,8 @@ namespace Barotrauma
} }
} }
EnsureItemsInBothHands(c.Character);
CreateNetworkEvent(); CreateNetworkEvent();
foreach (Inventory prevInventory in prevItemInventories.Distinct()) foreach (Inventory prevInventory in prevItemInventories.Distinct())
{ {
@@ -174,6 +177,33 @@ namespace Barotrauma
} }
} }
private void EnsureItemsInBothHands(Character character)
{
if (this is not CharacterInventory charInv) { return; }
int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand),
rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand);
if (IsSlotIndexOutOfBound(leftHandSlot) || IsSlotIndexOutOfBound(rightHandSlot)) { return; }
TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot);
TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot);
void TryPutInOppositeHandSlot(int originalSlot, int otherHandSlot)
{
const InvSlotType bothHandSlot = InvSlotType.LeftHand | InvSlotType.RightHand;
foreach (Item it in slots[originalSlot].Items)
{
if (it.AllowedSlots.None(static s => s.HasFlag(bothHandSlot)) || slots[otherHandSlot].Contains(it)) { continue; }
TryPutItem(it, otherHandSlot, true, true, character, false);
}
}
bool IsSlotIndexOutOfBound(int index) => index < 0 || index >= slots.Length;
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
{ {
SharedWrite(msg, extraData); SharedWrite(msg, extraData);
@@ -65,7 +65,8 @@ namespace Barotrauma
msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0); msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
itemContainer.Inventory.ServerEventWrite(msg, c); itemContainer.Inventory.ServerEventWrite(msg, c);
break; break;
case ItemStatusEventData _: case ItemStatusEventData statusEvent:
msg.WriteBoolean(statusEvent.LoadingRound);
msg.WriteSingle(condition); msg.WriteSingle(condition);
break; break;
case AssignCampaignInteractionEventData _: case AssignCampaignInteractionEventData _:
@@ -102,9 +102,9 @@ namespace Barotrauma.Networking
similarity *= 0.25f; similarity *= 0.25f;
} }
bool isOwner = GameMain.Server.OwnerConnection != null && c.Connection == GameMain.Server.OwnerConnection; bool isSpamExempt = RateLimiter.IsExempt(c);
if (similarity + c.ChatSpamSpeed > 5.0f && !isOwner) if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt)
{ {
GameMain.Server.KarmaManager.OnSpamFilterTriggered(c); GameMain.Server.KarmaManager.OnSpamFilterTriggered(c);
@@ -125,7 +125,7 @@ namespace Barotrauma.Networking
c.ChatSpamSpeed += similarity + 0.5f; c.ChatSpamSpeed += similarity + 0.5f;
if (c.ChatSpamTimer > 0.0f && !isOwner) if (c.ChatSpamTimer > 0.0f && !isSpamExempt)
{ {
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null); ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null);
c.ChatSpamTimer = 10.0f; c.ChatSpamTimer = 10.0f;
@@ -109,8 +109,6 @@ namespace Barotrauma.Networking
const int MaxTransferCount = 16; const int MaxTransferCount = 16;
const int MaxTransferCountPerRecipient = 5; const int MaxTransferCountPerRecipient = 5;
public static TimeSpan MaxTransferDuration = new TimeSpan(0, 2, 0);
public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver); public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver);
public FileTransferDelegate OnStarted; public FileTransferDelegate OnStarted;
public FileTransferDelegate OnEnded; public FileTransferDelegate OnEnded;
@@ -121,8 +119,9 @@ namespace Barotrauma.Networking
private readonly ServerPeer peer; private readonly ServerPeer peer;
public static DateTime StartTime;
#if DEBUG #if DEBUG
public float StallPacketsTime { get; set; } public float ForceMinimumFileTransferDuration { get; set; }
#endif #endif
public IReadOnlyList<FileTransferOut> ActiveTransfers => activeTransfers; public IReadOnlyList<FileTransferOut> ActiveTransfers => activeTransfers;
@@ -172,6 +171,8 @@ namespace Barotrauma.Networking
return null; return null;
} }
StartTime = DateTime.Now;
OnStarted(transfer); OnStarted(transfer);
GameMain.Server.LastClientListUpdateID++; GameMain.Server.LastClientListUpdateID++;
@@ -259,7 +260,18 @@ namespace Barotrauma.Networking
for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++) for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++)
{ {
long remaining = transfer.Data.Length - transfer.SentOffset; long remaining = transfer.Data.Length - transfer.SentOffset;
int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining); #if DEBUG
bool stalling = false;
float elapsedTime = (float)(DateTime.Now - StartTime).TotalSeconds;
if (elapsedTime < ForceMinimumFileTransferDuration)
{
int remainingChunks = (int)Math.Max(remaining / chunkLen, 1);
transfer.WaitTimer =
Math.Max(transfer.WaitTimer, (ForceMinimumFileTransferDuration - elapsedTime) / remainingChunks);
if (remainingChunks <= 1) { break; }
}
#endif
int sendByteCount = remaining > chunkLen ? chunkLen : (int)remaining;
message = new WriteOnlyMessage(); message = new WriteOnlyMessage();
message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER); message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER);
@@ -293,11 +305,10 @@ namespace Barotrauma.Networking
//this gets reset when packet loss or disorder sets in //this gets reset when packet loss or disorder sets in
transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate, transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate,
transfer.PacketsPerUpdate + 0.05f); transfer.PacketsPerUpdate + 0.05f);
}
#if DEBUG #if DEBUG
transfer.WaitTimer = Math.Max(transfer.WaitTimer, StallPacketsTime); if (stalling) { break; }
#endif #endif
}
} }
catch (Exception e) catch (Exception e)
@@ -330,7 +341,7 @@ namespace Barotrauma.Networking
{ {
byte transferId = inc.ReadByte(); byte transferId = inc.ReadByte();
var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId);
if (matchingTransfer != null) CancelTransfer(matchingTransfer); if (matchingTransfer != null) { CancelTransfer(matchingTransfer); }
return; return;
} }
else if (messageType == FileTransferMessageType.Data) else if (messageType == FileTransferMessageType.Data)
@@ -359,6 +370,7 @@ namespace Barotrauma.Networking
if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length) if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length)
{ {
matchingTransfer.Status = FileTransferStatus.Finished; matchingTransfer.Status = FileTransferStatus.Finished;
DebugConsole.Log($"Finished sending file \"{matchingTransfer.FilePath}\" to \"{client.Name}\". Took {DateTime.Now - StartTime}");
} }
} }
return; return;
@@ -303,7 +303,7 @@ namespace Barotrauma.Networking
} }
else else
{ {
var defaultPerms = PermissionPreset.List.Find(p => p.Name == "None"); var defaultPerms = PermissionPreset.List.Find(p => p.Identifier == "None");
if (defaultPerms != null) if (defaultPerms != null)
{ {
newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands); newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands);
@@ -332,9 +332,8 @@ namespace Barotrauma.Networking
public void Update(float deltaTime) public void Update(float deltaTime)
{ {
#if CLIENT dosProtection.Update(deltaTime);
if (ShowNetStats) { netStats.Update(deltaTime); }
#endif
if (!started) { return; } if (!started) { return; }
if (ChildServerRelay.HasShutDown) if (ChildServerRelay.HasShutDown)
@@ -387,10 +386,10 @@ namespace Barotrauma.Networking
Voting.Update(deltaTime); Voting.Update(deltaTime);
bool isCrewDead = bool isCrewDead =
connectedClients.All(c => c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated); connectedClients.All(c => !c.UsingFreeCam && (c.Character == null || c.Character.IsDead || c.Character.IsIncapacitated));
bool subAtLevelEnd = false; bool subAtLevelEnd = false;
if (Submarine.MainSub != null && !(GameMain.GameSession.GameMode is PvPMode)) if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode)
{ {
if (Level.Loaded?.EndOutpost != null) if (Level.Loaded?.EndOutpost != null)
{ {
@@ -692,10 +691,14 @@ namespace Barotrauma.Networking
} }
} }
private readonly DoSProtection dosProtection = new();
private void ReadDataMessage(NetworkConnection sender, IReadMessage inc) private void ReadDataMessage(NetworkConnection sender, IReadMessage inc)
{ {
var connectedClient = connectedClients.Find(c => c.Connection == sender); var connectedClient = connectedClients.Find(c => c.Connection == sender);
using var _ = dosProtection.Start(connectedClient);
ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte(); ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte();
switch (header) switch (header)
{ {
@@ -772,9 +775,12 @@ namespace Barotrauma.Networking
string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName); string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName);
if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
{ {
ServerSettings.CampaignSettings = settings; using (dosProtection.Pause(connectedClient))
ServerSettings.SaveSettings(); {
MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); ServerSettings.CampaignSettings = settings;
ServerSettings.SaveSettings();
MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings);
}
} }
} }
} }
@@ -784,11 +790,14 @@ namespace Barotrauma.Networking
if (GameStarted) if (GameStarted)
{ {
SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox); SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox);
return; break;
} }
if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
{ {
MultiPlayerCampaign.LoadCampaign(saveName); using (dosProtection.Pause(connectedClient))
{
MultiPlayerCampaign.LoadCampaign(saveName);
}
} }
} }
break; break;
@@ -1085,7 +1094,7 @@ namespace Barotrauma.Networking
ChatMessage.ServerRead(inc, c); ChatMessage.ServerRead(inc, c);
break; break;
case ClientNetSegment.Vote: case ClientNetSegment.Vote:
Voting.ServerRead(inc, c); Voting.ServerRead(inc, c, dosProtection);
break; break;
default: default:
return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes; return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
@@ -1246,7 +1255,7 @@ namespace Barotrauma.Networking
entityEventManager.Read(inc, c); entityEventManager.Read(inc, c);
break; break;
case ClientNetSegment.Vote: case ClientNetSegment.Vote:
Voting.ServerRead(inc, c); Voting.ServerRead(inc, c, dosProtection);
break; break;
case ClientNetSegment.SpectatingPos: case ClientNetSegment.SpectatingPos:
c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle()); c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle());
@@ -1406,19 +1415,23 @@ namespace Barotrauma.Networking
bool quitCampaign = inc.ReadBoolean(); bool quitCampaign = inc.ReadBoolean();
if (GameStarted) if (GameStarted)
{ {
Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage); using (dosProtection.Pause(sender))
if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
{ {
mpCampaign.SavePlayers(); Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage);
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine); if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
mpCampaign.UpdateStoreStock(); {
SaveUtil.SaveGame(GameMain.GameSession.SavePath); mpCampaign.SavePlayers();
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
mpCampaign.UpdateStoreStock();
GameMain.GameSession?.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
}
else
{
save = false;
}
EndGame(wasSaved: save);
} }
else
{
save = false;
}
EndGame(wasSaved: save);
} }
else if (mpCampaign != null) else if (mpCampaign != null)
{ {
@@ -1442,45 +1455,54 @@ namespace Barotrauma.Networking
} }
else if (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)) else if (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))
{ {
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath); using (dosProtection.Pause(sender))
{
MultiPlayerCampaign.LoadCampaign(GameMain.GameSession.SavePath);
}
} }
} }
else if (!GameStarted && !initiatedStartGame) else if (!GameStarted && !initiatedStartGame)
{ {
Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage); using (dosProtection.Pause(sender))
TryStartGame(); {
Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage);
TryStartGame();
}
} }
else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap))) else if (mpCampaign != null && (CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageCampaign) || CampaignMode.AllowedToManageCampaign(sender, ClientPermissions.ManageMap)))
{ {
var availableTransition = mpCampaign.GetAvailableTransition(out _, out _); using (dosProtection.Pause(sender))
//don't force location if we've teleported
bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation;
switch (availableTransition)
{ {
case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation: var availableTransition = mpCampaign.GetAvailableTransition(out _, out _);
if (forceLocation) //don't force location if we've teleported
{ bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation;
mpCampaign.Map.SelectLocation( switch (availableTransition)
mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation)); {
} case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation:
mpCampaign.LoadNewLevel(); if (forceLocation)
break; {
case CampaignMode.TransitionType.ProgressToNextEmptyLocation: mpCampaign.Map.SelectLocation(
if (forceLocation) mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation));
{ }
mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation)); mpCampaign.LoadNewLevel();
} break;
mpCampaign.LoadNewLevel(); case CampaignMode.TransitionType.ProgressToNextEmptyLocation:
break; if (forceLocation)
case CampaignMode.TransitionType.None: {
#if DEBUG || UNSTABLE mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation));
DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available."); }
#endif mpCampaign.LoadNewLevel();
return; break;
default: case CampaignMode.TransitionType.None:
Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage); #if DEBUG || UNSTABLE
mpCampaign.LoadNewLevel(); DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available.");
break; #endif
break;
default:
Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage);
mpCampaign.LoadNewLevel();
break;
}
} }
} }
} }
@@ -1520,11 +1542,7 @@ namespace Barotrauma.Networking
mpCampaign?.ServerRead(inc, sender); mpCampaign?.ServerRead(inc, sender);
break; break;
case ClientPermissions.ConsoleCommands: case ClientPermissions.ConsoleCommands:
{ DebugConsole.ServerRead(inc, sender);
string consoleCommand = inc.ReadString();
Vector2 clientCursorPos = new Vector2(inc.ReadSingle(), inc.ReadSingle());
DebugConsole.ExecuteClientCommand(sender, clientCursorPos, consoleCommand);
}
break; break;
case ClientPermissions.ManagePermissions: case ClientPermissions.ManagePermissions:
byte targetClientID = inc.ReadByte(); byte targetClientID = inc.ReadByte();
@@ -2582,16 +2600,20 @@ namespace Barotrauma.Networking
{ {
if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; } if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; }
const int MaxSaves = 255; using (dosProtection.Pause(client))
var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false);
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO);
msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves));
for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++)
{ {
msg.WriteNetSerializableStruct(saveInfos[i]); const int MaxSaves = 255;
var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false);
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO);
msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves));
for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++)
{
msg.WriteNetSerializableStruct(saveInfos[i]);
}
serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
} }
serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
return true; return true;
} }
@@ -3588,15 +3610,28 @@ namespace Barotrauma.Networking
} }
} }
private readonly RateLimiter charInfoRateLimiter = new(
maxRequests: 5,
expiryInSeconds: 10,
punishmentRules: new[]
{
(RateLimitAction.OnLimitReached, RateLimitPunishment.Announce),
(RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick)
});
private void UpdateCharacterInfo(IReadMessage message, Client sender) private void UpdateCharacterInfo(IReadMessage message, Client sender)
{ {
sender.SpectateOnly = message.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection); bool spectateOnly = message.ReadBoolean();
if (sender.SpectateOnly) message.ReadPadBits();
{
return;
}
string newName = message.ReadString(); sender.SpectateOnly = spectateOnly && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection);
if (sender.SpectateOnly) { return; }
var netInfo = INetSerializableStruct.Read<NetCharacterInfo>(message);
if (charInfoRateLimiter.IsLimitReached(sender)) { return; }
string newName = netInfo.NewName;
if (string.IsNullOrEmpty(newName)) if (string.IsNullOrEmpty(newName))
{ {
newName = sender.Name; newName = sender.Name;
@@ -3614,42 +3649,31 @@ namespace Barotrauma.Networking
} }
} }
int tagCount = message.ReadByte();
HashSet<Identifier> tagSet = new HashSet<Identifier>();
for (int i = 0; i < tagCount; i++)
{
tagSet.Add(message.ReadIdentifier());
}
int hairIndex = message.ReadByte();
int beardIndex = message.ReadByte();
int moustacheIndex = message.ReadByte();
int faceAttachmentIndex = message.ReadByte();
Color skinColor = message.ReadColorR8G8B8();
Color hairColor = message.ReadColorR8G8B8();
Color facialHairColor = message.ReadColorR8G8B8();
List<JobVariant> jobPreferences = new List<JobVariant>();
int count = message.ReadByte();
for (int i = 0; i < Math.Min(count, 3); i++)
{
string jobIdentifier = message.ReadString();
int variant = message.ReadByte();
if (JobPrefab.Prefabs.TryGet(jobIdentifier, out JobPrefab jobPrefab))
{
if (jobPrefab.HiddenJob) { continue; }
jobPreferences.Add(new JobVariant(jobPrefab, variant));
}
}
sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName); sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName);
sender.CharacterInfo.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
sender.CharacterInfo.Head.SkinColor = skinColor;
sender.CharacterInfo.Head.HairColor = hairColor;
sender.CharacterInfo.Head.FacialHairColor = facialHairColor;
if (jobPreferences.Count > 0) sender.CharacterInfo.RecreateHead(
tags: netInfo.Tags.ToImmutableHashSet(),
hairIndex: netInfo.HairIndex,
beardIndex: netInfo.BeardIndex,
moustacheIndex: netInfo.MoustacheIndex,
faceAttachmentIndex: netInfo.FaceAttachmentIndex);
sender.CharacterInfo.Head.SkinColor = netInfo.SkinColor;
sender.CharacterInfo.Head.HairColor = netInfo.HairColor;
sender.CharacterInfo.Head.FacialHairColor = netInfo.FacialHairColor;
if (netInfo.JobVariants.Length > 0)
{ {
sender.JobPreferences = jobPreferences; List<JobVariant> variants = new List<JobVariant>();
foreach (NetJobVariant jv in netInfo.JobVariants)
{
if (jv.ToJobVariant() is { } variant)
{
variants.Add(variant);
}
}
sender.JobPreferences = variants;
} }
} }
@@ -24,11 +24,28 @@ namespace Barotrauma.Networking
} }
protected readonly Callbacks callbacks; protected readonly Callbacks callbacks;
private readonly ImmutableArray<ContentPackage> contentPackages;
protected ServerPeer(Callbacks callbacks) protected ServerPeer(Callbacks callbacks)
{ {
this.callbacks = callbacks; this.callbacks = callbacks;
}
List<ContentPackage> contentPackageList = new List<ContentPackage>();
foreach (var cp in ContentPackageManager.EnabledPackages.All)
{
if (!cp.Files.Any()) { continue; }
if (!cp.HasMultiplayerSyncedContent && !cp.Files.All(f => f is SubmarineFile)) { continue; }
if (cp.UgcId.TryUnwrap(out var id1) &&
contentPackageList.FirstOrDefault(cp => cp.UgcId.TryUnwrap(out var id2) && id1.Equals(id2)) is ContentPackage existingPackage)
{
//there can be multiple enabled mods with the same UgcId if the player has e.g. created a local copy of a workshop mod
DebugConsole.AddWarning($"The content package \"{existingPackage.Name}\" ({existingPackage.Path}) has the same id as \"{cp.Name}\" ({cp.Path}). Ignoring the latter package.");
continue;
}
contentPackageList.Add(cp);
}
contentPackages = contentPackageList.ToImmutableArray();
}
public abstract void InitializeSteamServerCallbacks(); public abstract void InitializeSteamServerCallbacks();
@@ -250,9 +267,7 @@ namespace Barotrauma.Networking
structToSend = new ServerPeerContentPackageOrderPacket structToSend = new ServerPeerContentPackageOrderPacket
{ {
ServerName = GameMain.Server.ServerName, ServerName = GameMain.Server.ServerName,
ContentPackages = ContentPackageManager.EnabledPackages.All ContentPackages = contentPackages
.Where(cp => cp.Files.Any())
.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile))
.Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow))
.ToImmutableArray() .ToImmutableArray()
}; };
@@ -520,7 +520,7 @@ namespace Barotrauma.Networking
else else
{ {
string presetName = clientElement.GetAttributeString("preset", ""); string presetName = clientElement.GetAttributeString("preset", "");
PermissionPreset preset = PermissionPreset.List.Find(p => p.Name == presetName); PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName == presetName);
if (preset == null) if (preset == null)
{ {
DebugConsole.ThrowError("Failed to restore saved permissions to the client \"" + clientName + "\". Permission preset \"" + presetName + "\" not found."); DebugConsole.ThrowError("Failed to restore saved permissions to the client \"" + clientName + "\". Permission preset \"" + presetName + "\" not found.");
@@ -585,8 +585,7 @@ namespace Barotrauma.Networking
foreach (SavedClientPermission clientPermission in ClientPermissions) foreach (SavedClientPermission clientPermission in ClientPermissions)
{ {
var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands)); var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands));
#warning TODO: this is broken because of localization if (matchingPreset != null && matchingPreset.Identifier == "None")
if (matchingPreset != null && matchingPreset.Name == "None")
{ {
continue; continue;
} }
@@ -600,7 +599,7 @@ namespace Barotrauma.Networking
clientElement.Add(matchingPreset == null clientElement.Add(matchingPreset == null
? new XAttribute("permissions", clientPermission.Permissions.ToString()) ? new XAttribute("permissions", clientPermission.Permissions.ToString())
: new XAttribute("preset", matchingPreset.Name)); : new XAttribute("preset", matchingPreset.DisplayName));
if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands)) if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands))
{ {
@@ -225,7 +225,7 @@ namespace Barotrauma
} }
} }
public void ServerRead(IReadMessage inc, Client sender) public void ServerRead(IReadMessage inc, Client sender, DoSProtection dosProtection)
{ {
if (GameMain.Server == null || sender == null) { return; } if (GameMain.Server == null || sender == null) { return; }
@@ -337,7 +337,10 @@ namespace Barotrauma
inc.ReadPadBits(); inc.ReadPadBits();
GameMain.Server.UpdateVoteStatus(); using (dosProtection.Pause(sender))
{
GameMain.Server.UpdateVoteStatus();
}
} }
public void ServerWrite(IWriteMessage msg) public void ServerWrite(IWriteMessage msg)
@@ -158,7 +158,10 @@ namespace Barotrauma
sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language); sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language);
if (ContentPackageManager.EnabledPackages.All != null) if (ContentPackageManager.EnabledPackages.All != null)
{ {
sb.AppendLine("Selected content packages: " + (!ContentPackageManager.EnabledPackages.All.Any() ? "None" : string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => c.Name)))); sb.AppendLine("Selected content packages: " +
(!ContentPackageManager.EnabledPackages.All.Any() ?
"None" :
string.Join(", ", ContentPackageManager.EnabledPackages.All.Select(c => $"{c.Name} ({c.Hash?.ShortRepresentation ?? "unknown"})"))));
} }
sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed)); sb.AppendLine("Level seed: " + ((Level.Loaded == null) ? "no level loaded" : Level.Loaded.Seed));
sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")")); sb.AppendLine("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")"));
@@ -5,12 +5,13 @@ namespace Barotrauma.Steam
{ {
partial class SteamManager partial class SteamManager
{ {
private static void InitializeProjectSpecific() { IsInitialized = true; } private static void InitializeProjectSpecific() { }
private static bool IsInitializedProjectSpecific
=> Steamworks.SteamServer.IsValid;
public static bool CreateServer(Networking.GameServer server, bool isPublic) public static bool CreateServer(Networking.GameServer server, bool isPublic)
{ {
IsInitialized = true;
Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma") Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma")
{ {
GamePort = (ushort)server.Port, GamePort = (ushort)server.Port,
@@ -56,10 +57,15 @@ namespace Barotrauma.Steam
Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText); Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText);
Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString()); Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString());
Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString()); Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString());
Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); int index = 0;
Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); foreach (var contentPackage in contentPackages)
Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp {
=> cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : ""))); string ugcIdStr = contentPackage.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : string.Empty;
Steamworks.SteamServer.SetKey(
$"contentpackage{index}",
contentPackage.Name+","+ contentPackage.Hash.StringRepresentation + "," + ugcIdStr);
index++;
}
Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString()); Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString());
Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString());
Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString());
@@ -20,19 +20,6 @@ namespace Barotrauma
} }
public delegate void MessageSender(string message); public delegate void MessageSender(string message);
public void Greet(GameServer server, string codeWords, string codeResponse, MessageSender messageSender)
{
string greetingMessage = TextManager.FormatServerMessage(Mission.StartText,
("[codewords]", codeWords),
("[coderesponse]", codeResponse));
messageSender(greetingMessage);
Client traitorClient = server.ConnectedClients.Find(c => c.Character == Character);
Client ownerClient = server.ConnectedClients.Find(c => c.Connection == server.OwnerConnection);
if (traitorClient != ownerClient && ownerClient != null && ownerClient.Character == null)
{
GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText.Value, Mission.Identifier, TraitorMessageType.ServerMessageBox);
}
}
public void SendChatMessage(string serverText, Identifier iconIdentifier) public void SendChatMessage(string serverText, Identifier iconIdentifier)
{ {
@@ -224,10 +224,6 @@ namespace Barotrauma
{ {
pendingMessages.Add(traitor, new List<string>()); pendingMessages.Add(traitor, new List<string>());
} }
foreach (var traitor in Traitors.Values)
{
traitor.Greet(server, CodeWords, CodeResponse, message => pendingMessages[traitor].Add(message));
}
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier))); pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier)));
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier))); pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier)));
@@ -0,0 +1,232 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Barotrauma.Networking;
namespace Barotrauma
{
internal sealed class DoSProtection
{
/// <summary>
/// A struct that executes an action when it's created and another one when it's disposed.
/// </summary>
public readonly ref struct DoSAction
{
private readonly Client sender;
private readonly Action<Client> end;
public DoSAction(Client sender, Action<Client> start, Action<Client> end)
{
this.sender = sender;
this.end = end;
start(sender);
}
public void Dispose()
{
end(sender);
}
}
private sealed class OffenseData
{
/// <summary>
/// Timer that keeps track of how long it takes to process a packet.
/// </summary>
public readonly Stopwatch Stopwatch = new();
/// <summary>
/// Amount of strikes the client has received for causing the server to slow down.
/// </summary>
public int Strikes;
/// <summary>
/// How many packets have been sent in the last minute.
/// </summary>
public int PacketCount;
/// <summary>
/// Resets the strikes and packet count.
/// </summary>
public void ResetStrikes()
{
Strikes = 0;
PacketCount = 0;
}
/// <summary>
/// Resets the timer.
/// </summary>
public void ResetTimer() => Stopwatch.Reset();
}
private readonly Dictionary<Client, OffenseData> clients = new();
private float stopwatchResetTimer,
strikesResetTimer;
private const int StopwatchResetInterval = 1,
StrikesResetInterval = 60,
StrikeThreshold = 6;
/// <summary>
/// Called when the server receives a packet to start logging how much time it takes to process.
/// </summary>
/// <param name="client">The client to start a timer for.</param>
/// <returns>Nothing useful. Required for the "using" keyword.</returns>
/// <remarks>
/// Calling stop is not required, the timer will be stopped automatically when the function it was started in returns.
/// </remarks>
/// <example>
/// <code>
/// public void ServerRead(IReadMessage msg, Client c)
/// {
/// // start the timer
/// using var _ = dosProtection.Start(connectedClient);
///
/// if (condition)
/// {
/// // the timer will be stopped here.
/// return;
/// }
///
/// ProcessMessage(msg);
/// // the timer will be stopped here.
/// }
/// </code>
/// </example>
public DoSAction Start(Client client) => new DoSAction(client, StartFor, EndFor);
/// <summary>
/// Temporary pauses the timer for the client.
/// Used when we know a packet is going to slow down the server but we don't want to count it as a strike.
/// For example when a client is starting a round.
/// </summary>
/// <param name="client">The client to pause the timer for.</param>
/// <returns>Nothing useful. Required for the "using" keyword.</returns>
/// <remarks>
/// Calling resume is not required, the timer will be resumed automatically when the using block ends.
/// </remarks>
/// <example>
/// <code>
/// using (dos.Pause(client))
/// {
/// // do something that will slow down the server
/// }
/// // the timer will be resumed here
/// </code>
/// </example>
public DoSAction Pause(Client client) => new DoSAction(client, PauseFor, ResumeFor);
private void StartFor(Client client)
{
if (!clients.ContainsKey(client))
{
clients.Add(client, new OffenseData());
}
clients[client].Stopwatch.Start();
}
private void EndFor(Client client)
{
if (GetData(client) is not { } data) { return; }
data.PacketCount++;
data.Stopwatch.Stop();
UpdateOffense(client, data);
}
// stops the clock but doesn't update offenses
private void PauseFor(Client client) => GetData(client)?.Stopwatch.Stop();
private void ResumeFor(Client client) => GetData(client)?.Stopwatch.Start();
private void UpdateOffense(Client client, OffenseData data)
{
if (GameMain.Server?.ServerSettings is not { } settings) { return; }
// client is sending too many packets, kick them
if (data.PacketCount > settings.MaxPacketAmount && settings.MaxPacketAmount > ServerSettings.PacketLimitMin)
{
AttemptKickClient(client, TextManager.Get("PacketLimitKicked"));
clients.Remove(client);
return;
}
// if the stopwatch has been running for an entire second without the Update() method resetting it (which it does every second) then something is wrong
if (data.Stopwatch.ElapsedMilliseconds < 100) { return; }
data.Strikes++;
data.ResetTimer();
GameServer.Log($"{NetworkMember.ClientLogName(client)} is causing the server to slow down.", ServerLog.MessageType.DoSProtection);
// too many strikes, get them out of here
if (data.Strikes < StrikeThreshold) { return; }
if (settings.EnableDoSProtection)
{
AttemptKickClient(client, TextManager.Get("DoSProtectionKicked"));
}
clients.Remove(client);
static void AttemptKickClient(Client client, LocalizedString reason)
{
// ReSharper disable once ConvertToConstant.Local
bool doesRateLimitAffectClient =
#if DEBUG
true; // for testing
#else
!RateLimiter.IsExempt(client);
#endif
if (!doesRateLimitAffectClient)
{
return;
}
GameMain.Server?.KickClient(client, reason.Value);
}
}
public void Update(float deltaTime)
{
stopwatchResetTimer += deltaTime;
strikesResetTimer += deltaTime;
// reset the stopwatch every second
if (stopwatchResetTimer > StopwatchResetInterval)
{
stopwatchResetTimer = 0;
foreach (OffenseData data in clients.Values)
{
data.ResetTimer();
}
}
// reset the strikes every minute
if (strikesResetTimer > StrikesResetInterval)
{
strikesResetTimer = 0;
foreach (var (client, data) in clients)
{
if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings)
{
if (data.PacketCount > settings.MaxPacketAmount * 0.9f)
{
GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending a lot of packets and almost got kicked! ({data.PacketCount}).", ServerLog.MessageType.DoSProtection);
}
}
data.ResetStrikes();
}
}
}
private OffenseData? GetData(Client client) => clients.TryGetValue(client, out OffenseData? data) ? data : null;
}
}
@@ -0,0 +1,135 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.Networking;
namespace Barotrauma
{
public enum RateLimitAction
{
Invalid,
OnLimitReached,
OnLimitDoubled,
}
public enum RateLimitPunishment
{
None, // just ignore
Announce, // announce to the server
Kick, // kick the player
Ban // ban the player
}
internal sealed class RateLimiter
{
private sealed record RateLimit(DateTimeOffset Expiry)
{
public int RequestAmount;
}
private readonly Dictionary<Client, RateLimit> rateLimits = new();
private readonly HashSet<Client> expiredRateLimits = new();
private readonly Dictionary<Client, DateTimeOffset> recentlyAnnouncedOffenders = new();
private readonly int maxRequests, expiryInSeconds;
private readonly ImmutableDictionary<RateLimitAction, RateLimitPunishment> punishments;
public RateLimiter(int maxRequests, int expiryInSeconds, params (RateLimitAction Action, RateLimitPunishment Punishment)[] punishmentRules)
{
this.maxRequests = maxRequests;
this.expiryInSeconds = expiryInSeconds;
punishments = punishmentRules.ToImmutableDictionary(
static pair => pair.Action,
static pair => pair.Punishment);
}
public bool IsLimitReached(Client client)
{
#if !DEBUG
if (IsExempt(client)) { return false; }
#endif
expiredRateLimits.Clear();
foreach (var (c, limit) in rateLimits)
{
if (limit.Expiry < DateTimeOffset.Now)
{
expiredRateLimits.Add(c);
}
}
foreach (Client c in expiredRateLimits)
{
rateLimits.Remove(c);
}
if (!rateLimits.TryGetValue(client, out RateLimit? rateLimit))
{
rateLimit = new RateLimit(DateTimeOffset.Now.AddSeconds(expiryInSeconds));
rateLimits.Add(client, rateLimit);
}
rateLimit.RequestAmount++;
if (rateLimit.RequestAmount > maxRequests)
{
ProcessPunishment(client, rateLimit.RequestAmount);
return true;
}
return false;
}
private void ProcessPunishment(Client client, int requests)
{
bool isDosProtectionEnabled = GameMain.Server is { ServerSettings.EnableDoSProtection: true };
foreach (var (action, punishment) in punishments)
{
switch (action)
{
case RateLimitAction.Invalid:
continue;
case RateLimitAction.OnLimitReached when requests >= maxRequests:
case RateLimitAction.OnLimitDoubled when requests >= maxRequests * 2:
switch (punishment)
{
case RateLimitPunishment.None:
continue;
case RateLimitPunishment.Announce:
AnnounceOffender(client);
break;
case RateLimitPunishment.Ban when isDosProtectionEnabled:
GameMain.Server?.BanClient(client, TextManager.Get("SpamFilterKicked").Value);
break;
case RateLimitPunishment.Kick when isDosProtectionEnabled:
GameMain.Server?.KickClient(client, TextManager.Get("SpamFilterKicked").Value);
break;
}
break;
}
}
}
private void AnnounceOffender(Client client)
{
if (recentlyAnnouncedOffenders.TryGetValue(client, out DateTimeOffset expiry))
{
if (expiry > DateTimeOffset.Now) { return; }
recentlyAnnouncedOffenders.Remove(client);
}
GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending too many packets!", ServerLog.MessageType.DoSProtection);
recentlyAnnouncedOffenders.Add(client, DateTimeOffset.Now.AddSeconds(expiryInSeconds));
}
public static bool IsExempt(Client client) =>
(GameMain.Server.OwnerConnection != null && client.Connection == GameMain.Server.OwnerConnection)
|| client.HasPermission(ClientPermissions.SpamImmunity);
}
}
@@ -6,14 +6,14 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.0.9.0</Version> <Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations> <Configurations>Debug;Release;Unstable</Configurations>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<WarningsAsErrors>;NU1605;CS0114;CS0108CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors> <WarningsAsErrors>;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765</WarningsAsErrors>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -8,7 +8,7 @@
<Preset <Preset
name="Moderator" name="Moderator"
description="Allowed to manage round settings, kick players and access server logs." description="Allowed to manage round settings, kick players and access server logs."
permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney,ManageBotTalents"> permissions="ManageRound,Kick,SelectSub,SelectMode,ManageCampaign,ConsoleCommands,ServerLog,ManageSettings,ManageMoney,ManageBotTalents,SpamImmunity">
<Command name="clientlist"/> <Command name="clientlist"/>
<Command name="readycheck"/> <Command name="readycheck"/>
<Command name="autorestart"/> <Command name="autorestart"/>
@@ -107,12 +107,26 @@ namespace Barotrauma
} }
} }
public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) => /// <summary>
steeringManager is IndoorsSteeringManager pathSteering && /// Is the current path valid, using the provided parameters.
pathSteering.CurrentPath != null && /// </summary>
(!requireUnfinished || !pathSteering.CurrentPath.Finished) && /// <param name="requireNonDirty"></param>
!pathSteering.CurrentPath.Unreachable && /// <param name="requireUnfinished"></param>
(!requireNonDirty || !pathSteering.IsPathDirty); /// <param name="nodePredicate"></param>
/// <returns>When <paramref name="nodePredicate"/> is defined, returns false if any of the nodes fails to match the predicate.</returns>
public bool HasValidPath(bool requireNonDirty = true, bool requireUnfinished = true, Func<WayPoint, bool> nodePredicate = null)
{
if (SteeringManager is not IndoorsSteeringManager pathSteering) { return false; }
if (pathSteering.CurrentPath == null) { return false; }
if (pathSteering.CurrentPath.Unreachable) { return false; }
if (requireUnfinished && pathSteering.CurrentPath.Finished) { return false; }
if (requireNonDirty && pathSteering.IsPathDirty) { return false; }
if (nodePredicate != null)
{
return pathSteering.CurrentPath.Nodes.All(n => nodePredicate(n));
}
return true;
}
public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null; public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null;
public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable; public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable;
@@ -251,7 +265,7 @@ namespace Barotrauma
} }
private readonly HashSet<Item> unequippedItems = new HashSet<Item>(); private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false, IEnumerable<Identifier> targetTags = null)
{ {
var pickable = item.GetComponent<Pickable>(); var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; } if (pickable == null) { return false; }
@@ -265,23 +279,28 @@ namespace Barotrauma
} }
else else
{ {
var holdable = item.GetComponent<Holdable>(); // Not allowed to wear -> don't use the Wearable component even when it's found.
if (holdable != null) pickable = item.GetComponent<Holdable>();
{
pickable = holdable;
}
} }
if (item.ParentInventory is ItemInventory itemInventory) if (item.ParentInventory is ItemInventory itemInventory)
{ {
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; } if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
} }
if (equip) if (equip && pickable != null)
{ {
int targetSlot = -1; int targetSlot = -1;
//check if all the slots required by the item are free //check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots) foreach (InvSlotType slots in pickable.AllowedSlots)
{ {
if (slots.HasFlag(InvSlotType.Any)) { continue; } if (slots.HasFlag(InvSlotType.Any)) { continue; }
if (!wear)
{
if (slots != InvSlotType.RightHand && slots != InvSlotType.LeftHand && slots != (InvSlotType.RightHand | InvSlotType.LeftHand))
{
// Don't allow other than hand slots if not allowed to wear.
continue;
}
}
for (int i = 0; i < targetInventory.Capacity; i++) for (int i = 0; i < targetInventory.Capacity; i++)
{ {
if (targetInventory is CharacterInventory characterInventory) if (targetInventory is CharacterInventory characterInventory)
@@ -294,7 +313,7 @@ namespace Barotrauma
var otherItem = targetInventory.GetItemAt(i); var otherItem = targetInventory.GetItemAt(i);
if (otherItem == null) { continue; } if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful //try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot)) if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot))
{ {
if (storeUnequipped && targetInventory.Owner == Character) if (storeUnequipped && targetInventory.Owner == Character)
{ {
@@ -304,6 +323,11 @@ namespace Barotrauma
} }
if (dropOtherIfCannotMove) if (dropOtherIfCannotMove)
{ {
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
{
// Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory.
return false;
}
//if everything else fails, simply drop the existing item //if everything else fails, simply drop the existing item
otherItem.Drop(Character); otherItem.Drop(Character);
} }
@@ -314,7 +338,7 @@ namespace Barotrauma
} }
else else
{ {
return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot); return targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot);
} }
} }
@@ -339,7 +363,7 @@ namespace Barotrauma
if (avoidDroppingInSea && !character.IsInFriendlySub) if (avoidDroppingInSea && !character.IsInFriendlySub)
{ {
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it. // If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
{ {
if (unequipMax.HasValue && ++removed >= unequipMax) { return; } if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
continue; continue;
@@ -449,9 +473,10 @@ namespace Barotrauma
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition; Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared(); float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100); bool isClose = sqrDist < MathUtils.Pow2(100);
if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathNullOrUnreachable || IsCurrentPathFinished) if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished)
{ {
// Very close to the target, outside, or at the end of the path -> try to steer through the gap // Very close to the target, outside, or at the end of the path -> try to steer through the gap
Character.ReleaseSecondaryItem();
SteeringManager.Reset(); SteeringManager.Reset();
pathSteering?.ResetPath(); pathSteering?.ResetPath();
Vector2 dir = Vector2.Normalize(diff); Vector2 dir = Vector2.Normalize(diff);
@@ -480,7 +480,7 @@ namespace Barotrauma
if (SelectedAiTarget?.Entity != null || EscapeTarget != null) if (SelectedAiTarget?.Entity != null || EscapeTarget != null)
{ {
Entity t = SelectedAiTarget?.Entity ?? EscapeTarget; Entity t = SelectedAiTarget?.Entity ?? EscapeTarget;
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(requireNonDirty: true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath() ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left; Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left;
} }
else else
@@ -3934,7 +3934,7 @@ namespace Barotrauma
{ {
SteerAwayFromTheEnemy(); SteerAwayFromTheEnemy();
} }
else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true)) else if (canAttackDoors && HasValidPath())
{ {
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor; var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
@@ -1,16 +1,16 @@
using Barotrauma.Networking; using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma namespace Barotrauma
{ {
partial class HumanAIController : AIController partial class HumanAIController : AIController
{ {
public static bool debugai; public static bool DebugAI;
public static bool DisableCrewAI; public static bool DisableCrewAI;
private readonly AIObjectiveManager objectiveManager; private readonly AIObjectiveManager objectiveManager;
@@ -55,6 +55,7 @@ namespace Barotrauma
private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5; private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
private float obstacleRaycastTimer; private float obstacleRaycastTimer;
private bool isBlocked;
private readonly float enemyCheckInterval = 0.2f; private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 800; private readonly float enemySpotDistanceOutside = 800;
@@ -92,7 +93,10 @@ namespace Barotrauma
private readonly SteeringManager outsideSteering, insideSteering; private readonly SteeringManager outsideSteering, insideSteering;
public bool UseIndoorSteeringOutside { get; set; } = false; /// <summary>
/// Waypoints that are not linked to a sub (e.g. main path).
/// </summary>
public bool UseOutsideWaypoints { get; private set; }
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController; public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
@@ -225,14 +229,15 @@ namespace Barotrauma
IgnoredItems.Clear(); IgnoredItems.Clear();
} }
bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true) // Note: returns false when useTargetSub is 'true' and the target is outside (targetSub is 'null')
bool IsCloseEnoughToTarget(float threshold, bool targetSub = true)
{ {
Entity target = SelectedAiTarget?.Entity; Entity target = SelectedAiTarget?.Entity;
if (target == null) if (target == null)
{ {
return false; return false;
} }
if (useTargetSub) if (targetSub)
{ {
if (target.Submarine is Submarine sub) if (target.Submarine is Submarine sub)
{ {
@@ -244,62 +249,71 @@ namespace Barotrauma
return false; return false;
} }
} }
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2); return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow2(threshold);
} }
bool hasValidPath = HasValidPath(); bool isOutside = Character.Submarine == null;
if (isOutside)
if (Character.Submarine == null)
{ {
obstacleRaycastTimer -= deltaTime; obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0) if (obstacleRaycastTimer <= 0)
{ {
bool hasValidPath = HasValidPath();
isBlocked = false;
UseOutsideWaypoints = false;
obstacleRaycastTimer = obstacleRaycastIntervalLong; obstacleRaycastTimer = obstacleRaycastIntervalLong;
if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity is ISpatialEntity target && target.Submarine == null || !IsCloseEnoughToTarget(2000, useTargetSub: false)) ISpatialEntity spatialTarget = SelectedAiTarget?.Entity ?? ObjectiveManager.GetLastActiveObjective<AIObjectiveGoTo>()?.Target;
if (spatialTarget != null && (spatialTarget.Submarine == null || !IsCloseEnoughToTarget(2000, targetSub: false)))
{ {
// If the target is behind a level wall, switch to the pathing to get around the obstacles. // If the target is behind a level wall, switch to the pathing to get around the obstacles.
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity; IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
if (spatialTarget == null) Vector2 rayEnd = spatialTarget.SimPosition;
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
{ {
var gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>(); rayEnd += targetSub.SimPosition;
spatialTarget = gotoObjective?.Target; ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
} }
if (spatialTarget == null) var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
isBlocked = obstacle != null;
// Don't use outside waypoints when blocked by a sub, because we should use the waypoints linked to the sub instead.
UseOutsideWaypoints = isBlocked && (obstacle.UserData is not Submarine sub || sub.Info.IsRuin);
bool resetPath = false;
if (UseOutsideWaypoints)
{ {
UseIndoorSteeringOutside = false; bool isUsingInsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine != null || n.Ruin != null);
if (isUsingInsideWaypoints)
{
resetPath = true;
}
} }
else else
{ {
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null; bool isUsingOutsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine == null && n.Ruin == null);
Vector2 rayEnd = spatialTarget.SimPosition; if (isUsingOutsideWaypoints)
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
{ {
rayEnd += targetSub.SimPosition; resetPath = true;
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
} }
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); }
UseIndoorSteeringOutside = obstacle != null; if (resetPath)
{
PathSteering.ResetPath();
} }
} }
else else if (hasValidPath)
{ {
UseIndoorSteeringOutside = false; obstacleRaycastTimer = obstacleRaycastIntervalShort;
if (hasValidPath) // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
{ {
obstacleRaycastTimer = obstacleRaycastIntervalShort; if (connectedSub == Submarine.MainSub) { continue; }
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). Vector2 rayStart = SimPosition - connectedSub.SimPosition;
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{ {
if (connectedSub == Submarine.MainSub) { continue; } PathSteering.CurrentPath.Unreachable = true;
Vector2 rayStart = SimPosition - connectedSub.SimPosition; break;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
PathSteering.CurrentPath.Unreachable = true;
break;
}
} }
} }
} }
@@ -307,10 +321,11 @@ namespace Barotrauma
} }
else else
{ {
UseIndoorSteeringOutside = false; UseOutsideWaypoints = false;
isBlocked = false;
} }
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID)) if (isOutside || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
{ {
// Spot enemies while staying outside or inside an enemy ship. // Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -352,12 +367,13 @@ namespace Barotrauma
} }
} }
} }
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer)) if (useInsideSteering)
{ {
if (steeringManager != insideSteering) if (steeringManager != insideSteering)
{ {
insideSteering.Reset(); insideSteering.Reset();
PathSteering.ResetPath();
steeringManager = insideSteering; steeringManager = insideSteering;
} }
if (IsCloseEnoughToTarget(maxSteeringBuffer)) if (IsCloseEnoughToTarget(maxSteeringBuffer))
@@ -395,6 +411,8 @@ namespace Barotrauma
} }
objectiveManager.UpdateObjectives(deltaTime); objectiveManager.UpdateObjectives(deltaTime);
UpdateDragged(deltaTime);
if (reportProblemsTimer > 0) if (reportProblemsTimer > 0)
{ {
reportProblemsTimer -= deltaTime; reportProblemsTimer -= deltaTime;
@@ -433,10 +451,19 @@ namespace Barotrauma
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{ {
ReportProblems(); ReportProblems();
}
else
{
// Allows bots to heal targets autonomously while swimming outside of the sub.
if (AIObjectiveRescueAll.IsValidTarget(Character, Character))
{
AddTargets<AIObjectiveRescueAll, Character>(Character, Character);
}
} }
reportProblemsTimer = reportProblemsInterval; reportProblemsTimer = reportProblemsInterval;
} }
UpdateSpeaking(); SpeakAboutIssues();
UnequipUnnecessaryItems(); UnequipUnnecessaryItems();
reactTimer = GetReactionTime(); reactTimer = GetReactionTime();
} }
@@ -904,17 +931,67 @@ namespace Barotrauma
} }
})) }))
{ {
suitableContainer = targetContainer; if (targetContainer != null &&
return true; character.AIController is HumanAIController humanAI &&
humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull != null).Unreachable)
{
ignoredItems.Add(targetContainer);
itemIndex = 0;
return false;
}
else
{
suitableContainer = targetContainer;
return true;
}
} }
return false; return false;
} }
private float draggedTimer;
private float refuseDraggingTimer;
/// <summary>
/// The bot breaks free if being dragged by a human player from another team for longer than this
/// </summary>
private const float RefuseDraggingThresholdHigh = 10.0f;
/// <summary>
/// If the RefuseDraggingDuration is active (the bot recently broke free of being dragged), the bot breaks free much faster
/// </summary>
private const float RefuseDraggingThresholdLow = 0.5f;
private const float RefuseDraggingDuration = 30.0f;
private void UpdateDragged(float deltaTime)
{
if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; }
if (Character.IsEscorted) { return; }
if (Character.LockHands) { return; }
//don't allow player characters who aren't in the same team to drag us for more than x seconds
if (Character.SelectedBy == null ||
!Character.SelectedBy.IsPlayer ||
Character.SelectedBy.TeamID == Character.TeamID)
{
refuseDraggingTimer -= deltaTime;
return;
}
draggedTimer += deltaTime;
if (draggedTimer > RefuseDraggingThresholdHigh ||
(refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow))
{
draggedTimer = 0.0f;
refuseDraggingTimer = RefuseDraggingDuration;
Character.SelectedBy.DeselectCharacter();
Character.Speak(TextManager.Get("dialogrefusedragging").Value, delay: 0.5f, identifier: "refusedragging".ToIdentifier(), minDurationBetweenSimilar: 5.0f);
}
}
protected void ReportProblems() protected void ReportProblems()
{ {
Order newOrder = null; Order newOrder = null;
Hull targetHull = null; Hull targetHull = null;
bool speak = Character.SpeechImpediment < 100; // for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
if (Character.CurrentHull != null) if (Character.CurrentHull != null)
{ {
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>(); bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
@@ -1013,25 +1090,21 @@ namespace Barotrauma
} }
if (newOrder != null && speak) if (newOrder != null && speak)
{ {
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented if (Character.TeamID == CharacterTeamType.FriendlyNPC)
if (!Character.IsEscorted)
{ {
if (Character.TeamID == CharacterTeamType.FriendlyNPC) Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
{ identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default, minDurationBetweenSimilar: 60.0f);
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), }
minDurationBetweenSimilar: 60.0f); else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
} {
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
#if SERVER #if SERVER
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority) .WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull) .WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character)); .WithOrderGiver(Character), "", null, Character));
#endif #endif
}
} }
} }
} }
@@ -1062,21 +1135,33 @@ namespace Barotrauma
} }
} }
private void UpdateSpeaking() private void SpeakAboutIssues()
{ {
if (!Character.IsOnPlayerTeam) { return; } if (!Character.IsOnPlayerTeam) { return; }
if (Character.SpeechImpediment >= 100) { return; } if (Character.SpeechImpediment >= 100) { return; }
if (Character.Oxygen < 20.0f) float minDelay = 0.5f, maxDelay = 2f;
if (Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold)
{ {
Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f); string msgId = "DialogLowOxygen";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
} }
if (Character.Bleeding > 2.0f) if (Character.Bleeding > 2.0f && !Character.IsMedic)
{ {
Character.Speak(TextManager.Get("DialogBleeding").Value, null, Rand.Range(0.5f, 5.0f), "bleeding".ToIdentifier(), 30.0f); string msgId = "DialogBleeding";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
} }
if (Character.PressureTimer > 50.0f && Character.CurrentHull?.DisplayName != null) if ((Character.CurrentHull == null || Character.CurrentHull.LethalPressure > 0) && !Character.IsProtectedFromPressure)
{ {
Character.Speak(TextManager.GetWithVariable("DialogPressure", "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, null, Rand.Range(0.5f, 5.0f), "pressure".ToIdentifier(), 30.0f); if (Character.PressureProtection > 0)
{
string msgId = "DialogInsufficientPressureProtection";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
else if (Character.CurrentHull?.DisplayName != null)
{
string msgId = "DialogPressure";
Character.Speak(TextManager.GetWithVariable(msgId, "[roomname]", Character.CurrentHull.DisplayName, FormatCapitals.Yes).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
}
} }
} }
@@ -1205,7 +1290,7 @@ namespace Barotrauma
bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null; bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
if (isAccidental) if (isAccidental)
{ {
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold) if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold))
{ {
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
} }
@@ -1371,7 +1456,7 @@ namespace Barotrauma
} }
else else
{ {
if (humanAI.ObjectiveManager.GetActiveObjective<AIObjectiveCombat>()?.Enemy == attacker) if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
{ {
// Already targeting the attacker -> treat as a more serious threat. // Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2; cumulativeDamage *= 2;
@@ -1556,7 +1641,7 @@ namespace Barotrauma
hull.LethalPressure > 0 || hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{ {
needsSuit = !Character.IsProtectedFromPressure; needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
return needsAir || needsSuit; return needsAir || needsSuit;
} }
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
@@ -1656,7 +1741,7 @@ namespace Barotrauma
if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer) if (GameMain.GameSession?.Campaign?.Map?.CurrentLocation?.Reputation != null && character.IsPlayer)
{ {
var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage; var reputationLoss = damageAmount * Reputation.ReputationLossPerWallDamage;
GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss); GameMain.GameSession.Campaign.Map.CurrentLocation.Reputation.AddReputation(-reputationLoss, Reputation.MaxReputationLossFromWallDamage);
} }
if (accumulatedDamage <= WarningThreshold) { return; } if (accumulatedDamage <= WarningThreshold) { return; }
@@ -1812,7 +1897,7 @@ namespace Barotrauma
/// </summary> /// </summary>
public static void PropagateHullSafety(Character character, Hull hull) public static void PropagateHullSafety(Character character, Hull hull)
{ {
DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull)); DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
} }
private void RefreshHullSafety(Hull hull) private void RefreshHullSafety(Hull hull)
@@ -1885,7 +1970,7 @@ namespace Barotrauma
private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2> private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{ {
bool targetAdded = false; bool targetAdded = false;
DoForEachCrewMember(caller, humanAI => DoForEachBot(caller, humanAI =>
{ {
if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; } if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
var objective = humanAI.ObjectiveManager.GetObjective<T1>(); var objective = humanAI.ObjectiveManager.GetObjective<T1>();
@@ -1902,7 +1987,7 @@ namespace Barotrauma
public static void RemoveTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2> public static void RemoveTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{ {
DoForEachCrewMember(caller, humanAI => DoForEachBot(caller, humanAI =>
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target)); humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
} }
@@ -2024,6 +2109,10 @@ namespace Barotrauma
public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null) public float GetHullSafety(Hull hull, Character character, IEnumerable<Hull> visibleHulls = null)
{ {
if (hull == null)
{
return CalculateHullSafety(hull, character, visibleHulls);
}
if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety)) if (!knownHulls.TryGetValue(hull, out HullSafety hullSafety))
{ {
hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls)); hullSafety = new HullSafety(CalculateHullSafety(hull, character, visibleHulls));
@@ -2038,6 +2127,10 @@ namespace Barotrauma
public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false) public static float GetHullSafety(Hull hull, IEnumerable<Hull> visibleHulls, Character character, bool ignoreWater = false, bool ignoreOxygen = false, bool ignoreFire = false, bool ignoreEnemies = false)
{ {
if (hull == null)
{
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
}
HullSafety hullSafety; HullSafety hullSafety;
if (character.AIController is HumanAIController controller) if (character.AIController is HumanAIController controller)
{ {
@@ -2069,103 +2162,137 @@ namespace Barotrauma
if (other.IsPet) if (other.IsPet)
{ {
// Hostile NPCs are hostile to all pets, unless they are in the same team. // Hostile NPCs are hostile to all pets, unless they are in the same team.
if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; } return sameTeam || me.TeamID != CharacterTeamType.None;
} }
else else
{ {
if (!me.IsSameSpeciesOrGroup(other)) { return false; } if (!me.IsSameSpeciesOrGroup(other)) { return false; }
} }
if (GameMain.GameSession?.GameMode is CampaignMode campaign) if (GameMain.GameSession?.GameMode is CampaignMode)
{ {
if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) || if ((me.TeamID == CharacterTeamType.FriendlyNPC && other.TeamID == CharacterTeamType.Team1) ||
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC)) (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
{ {
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other; Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
Identifier npcFaction = npc.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty; //NPCs that allow some campaign interaction are not turned hostile by low reputation
if (npcFaction.IsEmpty) if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
if (npc.AIController is HumanAIController npcAI)
{ {
//if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost return !npcAI.IsInHostileFaction();
npcFaction = currentLocationFaction;
}
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
}
} }
} }
} }
return true; return true;
} }
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious; public bool IsInHostileFaction()
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
{ {
if (character == null) { return false; } if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; }
foreach (var c in Character.CharacterList) if (Character.IsEscorted) { return false; }
Identifier npcFaction = Character.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
if (npcFaction.IsEmpty)
{ {
if (FilterCrewMember(character, c)) //if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost
{ npcFaction = currentLocationFaction;
if (!predicate(c.AIController as HumanAIController))
{
return false;
}
}
} }
return true; if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
}
public static bool IsTrueForAnyCrewMember(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{ {
if (FilterCrewMember(character, c)) if (campaign.CurrentLocation is { IsFactionHostile: true })
{ {
if (predicate(c.AIController as HumanAIController)) return true;
{
return true;
}
} }
} }
return false; return false;
} }
public static int CountCrew(Character character, Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious;
public static bool IsTrueForAllBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (!IsBotInTheCrew(character, c)) { continue; }
if (!predicate(c.AIController as HumanAIController))
{
return false;
}
}
return true;
}
public static bool IsTrueForAnyBotInTheCrew(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (!IsBotInTheCrew(character, c)) { continue; }
if (predicate(c.AIController as HumanAIController))
{
return true;
}
}
return false;
}
public static int CountBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate = null)
{ {
if (character == null) { return 0; } if (character == null) { return 0; }
int count = 0; int count = 0;
foreach (var other in Character.CharacterList) foreach (var other in Character.CharacterList)
{ {
if (onlyActive && !IsActive(other)) if (!IsBotInTheCrew(character, other)) { continue; }
if (predicate == null || predicate(other.AIController as HumanAIController))
{ {
continue; count++;
}
if (onlyBots && other.IsPlayer)
{
continue;
}
if (FilterCrewMember(character, other))
{
if (predicate == null || predicate(other.AIController as HumanAIController))
{
count++;
}
} }
} }
return count; return count;
} }
public static void DoForEachCrewMember(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity) /// <summary>
/// Including the player characters in the same team.
/// </summary>
public bool IsTrueForAnyCrewMember(Func<Character, bool> predicate, bool onlyActive = true, bool onlyConnectedSubs = false)
{
foreach (var c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (onlyActive && c.IsIncapacitated) { continue; }
if (onlyConnectedSubs)
{
if (Character.Submarine == null)
{
if (c.Submarine != null)
{
return false;
}
}
else if (c.Submarine != Character.Submarine && !Character.Submarine.GetConnectedSubs().Contains(c.Submarine))
{
return false;
}
}
if (predicate(c))
{
return true;
}
}
return false;
}
private static void DoForEachBot(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
{ {
if (character == null) { return; } if (character == null) { return; }
foreach (var c in Character.CharacterList) foreach (var c in Character.CharacterList)
{ {
if (FilterCrewMember(character, c) && CheckReportRange(character, c, range)) if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
{ {
action(c.AIController as HumanAIController); action(c.AIController as HumanAIController);
} }
@@ -2185,7 +2312,7 @@ namespace Barotrauma
} }
} }
private static bool FilterCrewMember(Character self, Character other) => other != null && !other.IsDead && !other.Removed && other.AIController is HumanAIController humanAi && humanAi.IsFriendly(self); private static bool IsBotInTheCrew(Character self, Character other) => IsActive(other) && other.TeamID == self.TeamID && !other.IsIncapacitated && other.IsBot && other.AIController is HumanAIController;
public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter) public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
{ {
@@ -2230,10 +2357,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToOperateThis(Character.AIController); bool isOrder = IsOrderedToOperateThis(Character.AIController);
foreach (Character c in Character.CharacterList) foreach (Character c in Character.CharacterList)
{ {
if (!IsActive(c)) { continue; }
if (c == Character) { continue; } if (c == Character) { continue; }
if (c.Removed) { continue; }
if (c.TeamID != Character.TeamID) { continue; } if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
if (c.IsPlayer) if (c.IsPlayer)
{ {
if (c.SelectedItem == target.Item) if (c.SelectedItem == target.Item)
@@ -2301,9 +2427,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController); bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
foreach (var c in Character.CharacterList) foreach (var c in Character.CharacterList)
{ {
if (!IsActive(c)) { continue; }
if (c == Character) { continue; } if (c == Character) { continue; }
if (c.TeamID != Character.TeamID) { continue; } if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
other = c; other = c;
if (c.IsPlayer) if (c.IsPlayer)
{ {
@@ -2317,7 +2443,7 @@ namespace Barotrauma
{ {
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>(); var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
if (repairItemsObjective == null) { continue; } if (repairItemsObjective == null) { continue; }
if (!(repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is AIObjectiveRepairItem activeObjective) || activeObjective.Item != target) if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
{ {
// Not targeting the same item. // Not targeting the same item.
continue; continue;
@@ -2352,11 +2478,10 @@ namespace Barotrauma
} }
#region Wrappers #region Wrappers
public bool IsFriendly(Character other) => IsFriendly(Character, other); public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam);
public void DoForEachCrewMember(Action<HumanAIController> action) => DoForEachCrewMember(Character, action); public bool IsTrueForAnyBotInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAnyBotInTheCrew(Character, predicate);
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate); public bool IsTrueForAllBotsInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAllBotsInTheCrew(Character, predicate);
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate); public int CountBotsInTheCrew(Func<HumanAIController, bool> predicate = null) => CountBotsInTheCrew(Character, predicate);
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
#endregion #endregion
} }
} }
@@ -180,7 +180,7 @@ namespace Barotrauma
private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true) private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{ {
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished; bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null;
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f) if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
{ {
Vector2 targetDiff = target - currentTarget; Vector2 targetDiff = target - currentTarget;
@@ -205,6 +205,24 @@ namespace Barotrauma
if (needsNewPath || findPathTimer < -1.0f) if (needsNewPath || findPathTimer < -1.0f)
{ {
IsPathDirty = true; IsPathDirty = true;
if (!needsNewPath && findPathTimer < -1)
{
if (character.Submarine != null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
{
// Not moving -> need a new path.
needsNewPath = true;
}
if (character.Submarine == null && currentPath?.CurrentNode is WayPoint wp && wp.CurrentHull != null)
{
// Current node inside, while we are outside
// -> Check that the current node is not too far (can happen e.g. if someone controls the character in the meanwhile)
float maxDist = 200;
if (Vector2.DistanceSquared(character.WorldPosition, wp.WorldPosition) > maxDist * maxDist)
{
needsNewPath = true;
}
}
}
if (findPathTimer < 0) if (findPathTimer < 0)
{ {
SkipCurrentPathNodes(); SkipCurrentPathNodes();
@@ -213,7 +231,7 @@ namespace Barotrauma
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure; pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0; bool useNewPath = needsNewPath;
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{ {
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset). // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
@@ -387,49 +405,57 @@ namespace Barotrauma
} }
if (character.IsClimbing && useLadders) if (character.IsClimbing && useLadders)
{ {
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; if (currentLadder == null && nextLadder != null)
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
{ {
//climbing ladders -> don't move horizontally // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
diff.X = 0.0f; NextNode(!doorsChecked);
} }
//at the same height as the waypoint else
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
{ {
float heightFromFloor = character.AnimController.GetHeightFromFloor(); bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{ {
character.StopClimbing(); //climbing ladders -> don't move horizontally
diff.X = 0.0f;
} }
else if (nextLadder != null && !nextLadderSameAsCurrent) //at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
{ {
// Try to change the ladder (hatches between two submarines) float heightFromFloor = character.AnimController.GetHeightFromFloor();
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) // We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{ {
if (nextLadder.Item.TryInteract(character, forceSelectKey: true)) character.StopClimbing();
}
else if (nextLadder != null && !nextLadderSameAsCurrent)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
{ {
NextNode(!doorsChecked); if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
{
NextNode(!doorsChecked);
}
} }
} }
if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)
{
NextNode(!doorsChecked);
}
} }
if (!currentPath.IsAtEndNode && (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10)) else if (nextLadder != null)
{ {
NextNode(!doorsChecked); if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
} {
} //if the current node is below the character and the next one is above (or vice versa)
else if (nextLadder != null) //and both are on ladders, we can skip directly to the next one
{ //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
//if the current node is below the character and the next one is above (or vice versa) NextNode(!doorsChecked);
//and both are on ladders, we can skip directly to the next one }
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
NextNode(!doorsChecked);
} }
} }
return ConvertUnits.ToSimUnits(diff); return ConvertUnits.ToSimUnits(diff);
@@ -486,7 +512,7 @@ namespace Barotrauma
} }
} }
float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2); float targetDistance = Math.Max(colliderSize.X / 2 * margin, minWidth / 2);
if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && (door == null || door.CanBeTraversed)) if (horizontalDistance < targetDistance && !isTargetTooHigh && !isTargetTooLow && currentLadder == null && (door == null || door.CanBeTraversed))
{ {
NextNode(!doorsChecked); NextNode(!doorsChecked);
} }
@@ -122,7 +122,7 @@ namespace Barotrauma
foreach (Affliction affliction in afflictions) foreach (Affliction affliction in afflictions)
{ {
var currentEffect = affliction.GetActiveEffect(); var currentEffect = affliction.GetActiveEffect();
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag.Value) && !currentFlags.Contains(currentEffect.DialogFlag)) if (currentEffect is { DialogFlag.IsEmpty: false } && !currentFlags.Contains(currentEffect.DialogFlag))
{ {
currentFlags.Add(currentEffect.DialogFlag); currentFlags.Add(currentEffect.DialogFlag);
} }
@@ -506,15 +506,29 @@ namespace Barotrauma
} }
} }
protected static bool CanEquip(Character character, Item item) protected static bool CanEquip(Character character, Item item, bool allowWearing)
{ {
bool canEquip = item != null; if (item == null) { return false; }
if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any)) bool canEquip = false;
if (item.AllowedSlots.Contains(InvSlotType.Any))
{
if (character.Inventory.IsAnySlotAvailable(item))
{
canEquip = true;
}
}
if (!canEquip)
{ {
canEquip = false;
var inv = character.Inventory; var inv = character.Inventory;
foreach (var allowedSlot in item.AllowedSlots) foreach (var allowedSlot in item.AllowedSlots)
{ {
if (!allowWearing)
{
if (!allowedSlot.HasFlag(InvSlotType.RightHand) && !allowedSlot.HasFlag(InvSlotType.LeftHand))
{
continue;
}
}
foreach (var slotType in inv.SlotTypes) foreach (var slotType in inv.SlotTypes)
{ {
if (!allowedSlot.HasFlag(slotType)) { continue; } if (!allowedSlot.HasFlag(slotType)) { continue; }
@@ -530,18 +544,9 @@ namespace Barotrauma
} }
} }
} }
return canEquip; return canEquip && character.Inventory.CanBePut(item);
}
protected bool CheckItemIdentifiersOrTags(Item item, ImmutableHashSet<Identifier> identifiersOrTags)
{
if (identifiersOrTags.Contains(item.Prefab.Identifier)) { return true; }
foreach (var identifier in identifiersOrTags)
{
if (item.HasTag(identifier)) { return true; }
}
return false;
} }
protected bool CanEquip(Item item) => CanEquip(character, item); protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing);
} }
} }
@@ -64,7 +64,6 @@ namespace Barotrauma
if (subObjectives.Any()) { return; } if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer)) if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{ {
itemIndex = 0;
if (suitableContainer != null) if (suitableContainer != null)
{ {
bool equip = item.GetComponent<Holdable>() != null || bool equip = item.GetComponent<Holdable>() != null ||
@@ -112,10 +111,7 @@ namespace Barotrauma
Abandon = true; Abandon = true;
} }
} }
else objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
} }
protected override bool CheckObjectiveSpecific() protected override bool CheckObjectiveSpecific()
@@ -121,7 +121,7 @@ namespace Barotrauma
{ {
return true; return true;
} }
return CanEquip(character, item); return CanEquip(character, item, allowWearing: false);
} }
public override void OnDeselected() public override void OnDeselected()
@@ -170,13 +170,33 @@ namespace Barotrauma
return Priority; return Priority;
} }
} }
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f); if (TargetEliminated)
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
if (Priority > 0)
{ {
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character)) Priority = 0;
}
else
{
// 91-100
float minPriority = AIObjectiveManager.EmergencyObjectivePriority + 1;
float maxPriority = AIObjectiveManager.MaxObjectivePriority;
float priorityScale = maxPriority - minPriority;
float xDist = Math.Abs(character.WorldPosition.X - Enemy.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Enemy.WorldPosition.Y);
if (HumanAIController.VisibleHulls.Contains(Enemy.CurrentHull))
{ {
Priority = 0; xDist /= 2;
yDist /= 2;
}
float distanceFactor = MathUtils.InverseLerp(3000, 0, xDist + yDist * 5);
float devotion = CumulatedDevotion / 100;
float additionalPriority = MathHelper.Lerp(0, priorityScale, Math.Clamp(devotion + distanceFactor, 0, 1));
Priority = Math.Min((minPriority + additionalPriority) * PriorityModifier, maxPriority);
if (Priority > 0)
{
if (EnemyAIController.IsLatchedToSomeoneElse(Enemy, character))
{
Priority = 0;
}
} }
} }
return Priority; return Priority;
@@ -312,12 +332,10 @@ namespace Barotrauma
} }
else else
{ {
AskHelp();
Retreat(deltaTime); Retreat(deltaTime);
} }
break; break;
case CombatMode.Retreat: case CombatMode.Retreat:
AskHelp();
Retreat(deltaTime); Retreat(deltaTime);
break; break;
default: default:
@@ -352,7 +370,7 @@ namespace Barotrauma
Weapon = null; Weapon = null;
continue; continue;
} }
if (WeaponComponent.IsNotEmpty(character)) if (!WeaponComponent.IsEmpty(character))
{ {
// All good, the weapon is loaded // All good, the weapon is loaded
break; break;
@@ -470,7 +488,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component // Not in the inventory anymore or cannot find the weapon component
return false; return false;
} }
if (!WeaponComponent.IsNotEmpty(character)) if (WeaponComponent.IsEmpty(character))
{ {
// Try reloading (and seek ammo) // Try reloading (and seek ammo)
if (!Reload(seekAmmo)) if (!Reload(seekAmmo))
@@ -541,7 +559,7 @@ namespace Barotrauma
priority /= 2; priority /= 2;
} }
} }
if (!weapon.IsNotEmpty(character)) if (weapon.IsEmpty(character))
{ {
if (weapon is RangedWeapon && !isAllowedToSeekWeapons) if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{ {
@@ -554,7 +572,6 @@ namespace Barotrauma
priority /= 2; priority /= 2;
} }
} }
if (Enemy.Params.Health.StunImmunity) if (Enemy.Params.Health.StunImmunity)
{ {
if (weapon.Item.HasTag("stunner")) if (weapon.Item.HasTag("stunner"))
@@ -750,7 +767,7 @@ namespace Barotrauma
private bool Equip() private bool Equip()
{ {
if (character.LockHands) { return false; } if (character.LockHands) { return false; }
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false)) if (WeaponComponent.IsEmpty(character))
{ {
return false; return false;
} }
@@ -783,6 +800,10 @@ namespace Barotrauma
private void Retreat(float deltaTime) private void Retreat(float deltaTime)
{ {
if (!Enemy.IsHuman)
{
SpeakRetreating();
}
RemoveFollowTarget(); RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunitionObjective); RemoveSubObjective(ref seekAmmunitionObjective);
if (retreatObjective != null && retreatObjective.Target != retreatTarget) if (retreatObjective != null && retreatObjective.Target != retreatTarget)
@@ -793,6 +814,7 @@ namespace Barotrauma
{ {
// Swim away // Swim away
SteeringManager.Reset(); SteeringManager.Reset();
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition)); SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2); SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
return; return;
@@ -819,7 +841,8 @@ namespace Barotrauma
{ {
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager) TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
{ {
UsePathingOutside = false UsePathingOutside = false,
SpeakIfFails = false
}, },
onAbandon: () => onAbandon: () =>
{ {
@@ -861,6 +884,7 @@ namespace Barotrauma
{ {
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range)) if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
{ {
character.ReleaseSecondaryItem();
// Swim towards the target // Swim towards the target
SteeringManager.Reset(); SteeringManager.Reset();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10); SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10);
@@ -882,7 +906,8 @@ namespace Barotrauma
UsePathingOutside = false, UsePathingOutside = false,
IgnoreIfTargetDead = true, IgnoreIfTargetDead = true,
TargetName = Enemy.DisplayName, TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false AlwaysUseEuclideanDistance = false,
SpeakIfFails = false
}, },
onAbandon: () => onAbandon: () =>
{ {
@@ -966,7 +991,7 @@ namespace Barotrauma
item.GetComponent<RangedWeapon>() != null) item.GetComponent<RangedWeapon>() != null)
{ {
item.Drop(character); item.Drop(character);
character.Inventory.TryPutItem(item, character, CharacterInventory.anySlot); character.Inventory.TryPutItem(item, character, CharacterInventory.AnySlot);
} }
} }
} }
@@ -1028,54 +1053,43 @@ namespace Barotrauma
if (Weapon.OwnInventory == null) { return true; } if (Weapon.OwnInventory == null) { return true; }
// Eject empty ammo // Eject empty ammo
HumanAIController.UnequipEmptyItems(Weapon); HumanAIController.UnequipEmptyItems(Weapon);
RelatedItem item = null;
Item ammunition = null;
ImmutableHashSet<Identifier> ammunitionIdentifiers = null; ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained)) if (WeaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
{ {
foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained]) foreach (RelatedItem requiredItem in WeaponComponent.requiredItems[RelatedItem.RelationType.Contained])
{ {
ammunition = Weapon.OwnInventory.AllItems.FirstOrDefault(it => it.Condition > 0 && requiredItem.MatchesItem(it)); if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
if (ammunition != null)
{
// Ammunition still remaining
return true;
}
item = requiredItem;
ammunitionIdentifiers = requiredItem.Identifiers; ammunitionIdentifiers = requiredItem.Identifiers;
break;
} }
} }
else if (WeaponComponent is MeleeWeapon meleeWeapon) else if (WeaponComponent is MeleeWeapon meleeWeapon)
{ {
ammunitionIdentifiers = meleeWeapon.PreferredContainedItems; ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
} }
// No ammo // No ammo
if (ammunition == null) if (ammunitionIdentifiers != null)
{ {
if (ammunitionIdentifiers != null) // Try reload ammunition from inventory
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
Item ammunition = character.Inventory.FindItem(i => i.HasIdentifierOrTags(ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
{ {
// Try reload ammunition from inventory var container = Weapon.GetComponent<ItemContainer>();
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio"); if (!container.Inventory.TryPutItem(ammunition, user: character))
ammunition = character.Inventory.FindItem(i => CheckItemIdentifiersOrTags(i, ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
{ {
var container = Weapon.GetComponent<ItemContainer>(); if (ammunition.ParentInventory == character.Inventory)
if (!container.Inventory.TryPutItem(ammunition, null))
{ {
if (ammunition.ParentInventory == character.Inventory) ammunition.Drop(character);
{
ammunition.Drop(character);
}
} }
} }
} }
} }
if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false)) if (!WeaponComponent.IsEmpty(character))
{ {
return true; return true;
} }
else if (ammunition == null && !HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null) else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{ {
SeekAmmunition(ammunitionIdentifiers); SeekAmmunition(ammunitionIdentifiers);
} }
@@ -1270,7 +1284,7 @@ namespace Barotrauma
} }
private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30); private void SpeakNoWeapons() => Speak("dialogcombatnoweapons".ToIdentifier(), delay: 0, minDuration: 30);
private void AskHelp() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20); private void SpeakRetreating() => Speak("dialogcombatretreating".ToIdentifier(), delay: Rand.Range(0f, 1f), minDuration: 20);
private void Speak(Identifier textIdentifier, float delay, float minDuration) private void Speak(Identifier textIdentifier, float delay, float minDuration)
{ {
@@ -109,7 +109,7 @@ namespace Barotrauma
private bool CheckItem(Item item) private bool CheckItem(Item item)
{ {
return CheckItemIdentifiersOrTags(item, itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character); return item.HasIdentifierOrTags(itemIdentifiers) && item.ConditionPercentage >= ConditionLevel && item.HasAccess(character);
} }
protected override void Act(float deltaTime) protected override void Act(float deltaTime)
@@ -156,15 +156,15 @@ namespace Barotrauma
Inventory originalInventory = ItemToContain.ParentInventory; Inventory originalInventory = ItemToContain.ParentInventory;
var slots = originalInventory?.FindIndices(ItemToContain); var slots = originalInventory?.FindIndices(ItemToContain);
static bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain) bool TryPutItem(Inventory inventory, int? targetSlot, Item itemToContain)
{ {
if (targetSlot.HasValue) if (targetSlot.HasValue)
{ {
return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: null); return inventory.TryPutItem(itemToContain, targetSlot.Value, allowSwapping: false, allowCombine: false, user: character);
} }
else else
{ {
return inventory.TryPutItem(itemToContain, user: null); return inventory.TryPutItem(itemToContain, user: character);
} }
} }
@@ -202,7 +202,7 @@ namespace Barotrauma
ItemToContain == null || ItemToContain.Removed || ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(), SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.DefaultReach) endNodeFilter = n => Vector2.DistanceSquared(n.Waypoint.WorldPosition, container.Item.WorldPosition) <= MathUtils.Pow2(AIObjectiveGetItem.MaxReach)
}, },
onAbandon: () => Abandon = true, onAbandon: () => Abandon = true,
onCompleted: () => RemoveSubObjective(ref goToObjective)); onCompleted: () => RemoveSubObjective(ref goToObjective));
@@ -66,7 +66,7 @@ namespace Barotrauma
else else
{ {
float devotion = CumulatedDevotion / 100; float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, 100, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1)); Priority = MathHelper.Lerp(0, AIObjectiveManager.MaxObjectivePriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
} }
} }
return Priority; return Priority;
@@ -28,7 +28,7 @@ namespace Barotrauma
if (character.IsSecurity) { return 100; } if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; } if (objectiveManager.IsOrder(this)) { return 100; }
// If there's any security officers onboard, leave fighting for them. // If there's any security officers onboard, leave fighting for them.
return HumanAIController.IsTrueForAnyCrewMember(c => c.Character.IsSecurity && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine) ? 0 : 100; return HumanAIController.IsTrueForAnyCrewMember(c => c.IsSecurity, onlyActive: true, onlyConnectedSubs: true) ? 0 : 100;
} }
protected override AIObjective ObjectiveConstructor(Character target) protected override AIObjective ObjectiveConstructor(Character target)
@@ -37,8 +37,7 @@ namespace Barotrauma
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier); var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign) if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{ {
var reputation = campaign.Map?.CurrentLocation?.Reputation; if (campaign.CurrentLocation is { IsFactionHostile: true })
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{ {
combatObjective.holdFireCondition = () => combatObjective.holdFireCondition = () =>
{ {
@@ -66,7 +65,13 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; } if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { return false; } if (HumanAIController.IsFriendly(character, target)) { return false; }
if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; } if (!character.Submarine.IsConnectedTo(target.Submarine)) { return false; }
if (!targetCharactersInOtherSubs && character.Submarine.TeamID != target.Submarine.TeamID && character.Submarine.TeamID != character.OriginalTeamID) { return false; } if (!targetCharactersInOtherSubs)
{
if (character.Submarine.TeamID != target.Submarine.TeamID && character.OriginalTeamID != target.Submarine.TeamID)
{
return false;
}
}
if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; } if (target.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) { return false; }
if (target.IsArrested) { return false; } if (target.IsArrested) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; } if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }
@@ -79,7 +79,7 @@ namespace Barotrauma
{ {
if (mask != targetItem) if (mask != targetItem)
{ {
character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot); character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
} }
} }
} }
@@ -52,17 +52,14 @@ namespace Barotrauma
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) || objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() || objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0)) objectiveManager.Objectives.Any(o => o is AIObjectiveCombat && o.Priority > 0))
&& ((character.IsImmuneToPressure && !character.IsLowInOxygen)|| HumanAIController.HasDivingSuit(character)) ? 0 : 100; && ((!character.IsLowInOxygen && character.IsImmuneToPressure)|| HumanAIController.HasDivingSuit(character)) ? 0 : AIObjectiveManager.EmergencyObjectivePriority - 10;
} }
else else
{ {
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) || if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
(HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) && NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))))
{ {
Priority = 100; Priority = AIObjectiveManager.MaxObjectivePriority;
} }
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) && else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID)) character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
@@ -75,11 +72,11 @@ namespace Barotrauma
{ {
Priority = 0; Priority = 0;
} }
Priority = MathHelper.Clamp(Priority, 0, 100); Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted) if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{ {
// Boost the priority while seeking the diving gear // Boost the priority while seeking the diving gear
Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.HighestOrderPriority + 20, 100)); Priority = Math.Max(Priority, Math.Min(AIObjectiveManager.EmergencyObjectivePriority - 1, AIObjectiveManager.MaxObjectivePriority));
} }
} }
return Priority; return Priority;
@@ -111,7 +108,7 @@ namespace Barotrauma
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD) if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{ {
Priority -= priorityDecrease * deltaTime; Priority -= priorityDecrease * deltaTime;
if (currenthullSafety >= 100) if (currenthullSafety >= 100 && !character.IsLowInOxygen)
{ {
// Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock. // Reduce the priority to zero so that the bot can get switch to other objectives immediately, e.g. when entering the airlock.
Priority = 0; Priority = 0;
@@ -122,7 +119,7 @@ namespace Barotrauma
float dangerFactor = (100 - currenthullSafety) / 100; float dangerFactor = (100 - currenthullSafety) / 100;
Priority += dangerFactor * priorityIncrease * deltaTime; Priority += dangerFactor * priorityIncrease * deltaTime;
} }
Priority = MathHelper.Clamp(Priority, 0, 100); Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
} }
} }
@@ -138,7 +135,7 @@ namespace Barotrauma
{ {
if (resetPriority) { return; } if (resetPriority) { return; }
var currentHull = character.CurrentHull; var currentHull = character.CurrentHull;
bool dangerousPressure = !character.IsProtectedFromPressure && (currentHull == null || currentHull.LethalPressure > 0); bool dangerousPressure = (currentHull == null || currentHull.LethalPressure > 0) && !character.IsProtectedFromPressure;
bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false); bool shouldActOnSuffocation = character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false);
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull)) if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{ {
@@ -200,16 +197,11 @@ namespace Barotrauma
UpdateSimpleEscape(deltaTime); UpdateSimpleEscape(deltaTime);
return; return;
} }
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f); searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull; previousSafeHull = currentSafeHull;
currentSafeHull = potentialSafeHull; currentSafeHull = potentialSafeHull;
cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull);
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _); currentSafeHull ??= previousSafeHull;
if (currentSafeHull == null)
{
currentSafeHull = previousSafeHull;
}
if (currentSafeHull != null && currentSafeHull != currentHull) if (currentSafeHull != null && currentSafeHull != currentHull)
{ {
if (goToObjective?.Target != currentSafeHull) if (goToObjective?.Target != currentSafeHull)
@@ -219,6 +211,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective, TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true) constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{ {
SpeakIfFails = false,
AllowGoingOutside = AllowGoingOutside =
character.IsProtectedFromPressure || character.IsProtectedFromPressure ||
character.CurrentHull == null || character.CurrentHull == null ||
@@ -300,6 +293,7 @@ namespace Barotrauma
//only move if we haven't reached the edge of the room //only move if we haven't reached the edge of the room
if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right) if (escapeVel.X < 0 && character.Position.X > left || escapeVel.X > 0 && character.Position.X < right)
{ {
character.ReleaseSecondaryItem();
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel); character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
} }
else else
@@ -349,7 +343,6 @@ namespace Barotrauma
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; } if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; } if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; } if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
//sort the hulls based on distance and which sub they're in //sort the hulls based on distance and which sub they're in
//tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily //tends to make the method much faster, because we find a potential hull earlier and can discard further-away hulls more easily
//(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive //(for instance, an NPC in an outpost might otherwise go through all the hulls in the main sub first and do tons of expensive
@@ -493,5 +486,18 @@ namespace Barotrauma
cannotFindDivingGear = false; cannotFindDivingGear = false;
cannotFindSafeHull = false; cannotFindSafeHull = false;
} }
private bool NeedMoreDivingGear(Hull targetHull, float minOxygen = 0)
{
if (!HumanAIController.NeedsDivingGear(targetHull, out bool needsSuit)) { return false; }
if (needsSuit)
{
return !HumanAIController.HasDivingSuit(character, minOxygen);
}
else
{
return !HumanAIController.HasDivingGear(character, minOxygen);
}
}
} }
} }
@@ -37,40 +37,56 @@ namespace Barotrauma
{ {
Priority = 0; Priority = 0;
Abandon = true; Abandon = true;
return Priority;
} }
else if (HumanAIController.IsTrueForAnyCrewMember( float coopMultiplier = 1;
other => other != HumanAIController && foreach (var c in Character.CharacterList)
other.Character.IsBot &&
other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks &&
fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak)))
{ {
Priority = 0; if (!HumanAIController.IsActive(c)) { continue; }
if (c.TeamID != character.TeamID) { continue; }
if (c == character) { continue; }
if (c.IsPlayer) { continue; }
if (c.AIController is HumanAIController otherAI )
{
if (otherAI.ObjectiveManager.GetFirstActiveObjective<AIObjectiveFixLeak>() is AIObjectiveFixLeak fixLeak)
{
if (fixLeak.Leak == Leak)
{
// Ignore leaks that others are already targeting
Priority = 0;
return Priority;
}
if (fixLeak.Leak.FlowTargetHull == Leak.FlowTargetHull)
{
// Reduce the priority of leaks that others should be targeting
coopMultiplier = 0.1f;
break;
}
}
}
}
float reduction = isPriority ? 1 : 2;
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
if (operateObjective != null && objectiveManager.GetFirstActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
{
// Prioritize leaks that we are already fixing
Priority = maxPriority;
} }
else else
{ {
float reduction = isPriority ? 1 : 2; float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction; float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
if (operateObjective != null && objectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this) // Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull))
{ {
// Prioritize leaks that we are already fixing // Double the distance when the leak can be accessed from the current hull.
Priority = maxPriority; distanceFactor *= 2;
}
else
{
float xDist = Math.Abs(character.WorldPosition.X - Leak.WorldPosition.X);
float yDist = Math.Abs(character.WorldPosition.Y - Leak.WorldPosition.Y);
// Vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally).
// If the target is close, ignore the distance factor alltogether so that we keep fixing the leaks that are nearby.
float distanceFactor = isPriority || xDist < 200 && yDist < 100 ? 1 : MathHelper.Lerp(1, 0.1f, MathUtils.InverseLerp(0, 3000, xDist + yDist * 3.0f));
if (Leak.linkedTo.Any(e => e is Hull h && h == character.CurrentHull))
{
// Double the distance when the leak can be accessed from the current hull.
distanceFactor *= 2;
}
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier), 0, 1));
} }
float severity = isPriority ? 1 : AIObjectiveFixLeaks.GetLeakSeverity(Leak) / 100;
float devotion = CumulatedDevotion / 100;
Priority = MathHelper.Lerp(0, maxPriority, MathHelper.Clamp(devotion + (severity * distanceFactor * PriorityModifier * coopMultiplier), 0, 1));
} }
return Priority; return Priority;
} }
@@ -38,9 +38,9 @@ namespace Barotrauma
protected override float TargetEvaluation() protected override float TargetEvaluation()
{ {
int totalLeaks = Targets.Count(); int totalLeaks = Targets.Count;
if (totalLeaks == 0) { return 0; } if (totalLeaks == 0) { return 0; }
int otherFixers = HumanAIController.CountCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && !c.Character.IsIncapacitated && c.Character.Submarine == character.Submarine, onlyBots: true); int otherFixers = HumanAIController.CountBotsInTheCrew(c => c != HumanAIController && c.ObjectiveManager.IsCurrentObjective<AIObjectiveFixLeaks>() && c.Character.Submarine == character.Submarine);
bool anyFixers = otherFixers > 0; bool anyFixers = otherFixers > 0;
if (objectiveManager.IsOrder(this)) if (objectiveManager.IsOrder(this))
{ {
@@ -52,7 +52,7 @@ namespace Barotrauma
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom); int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks; int leaks = totalLeaks - secondaryLeaks;
float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1; float ratio = leaks == 0 ? 1 : anyFixers ? leaks / (float)otherFixers : 1;
if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountCrew(onlyBots: true) > 0.75f)) if (anyFixers && (ratio <= 1 || otherFixers > 5 || otherFixers / (float)HumanAIController.CountBotsInTheCrew() > 0.75f))
{ {
// Enough fixers // Enough fixers
return 0; return 0;
@@ -1,9 +1,11 @@
using Barotrauma.Items.Components; using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Diagnostics;
namespace Barotrauma namespace Barotrauma
{ {
@@ -31,14 +33,15 @@ namespace Barotrauma
private ISpatialEntity moveToTarget; private ISpatialEntity moveToTarget;
private bool isDoneSeeking; private bool isDoneSeeking;
public Item TargetItem => targetItem; public Item TargetItem => targetItem;
private int currSearchIndex; private int currentSearchIndex;
public ImmutableHashSet<Identifier> ignoredContainerIdentifiers; public ImmutableHashSet<Identifier> ignoredContainerIdentifiers;
public ImmutableHashSet<Identifier> ignoredIdentifiersOrTags; public ImmutableHashSet<Identifier> ignoredIdentifiersOrTags;
private AIObjectiveGoTo goToObjective; private AIObjectiveGoTo goToObjective;
private float currItemPriority; private float currItemPriority;
private readonly bool checkInventory; private readonly bool checkInventory;
public static float DefaultReach = 100; public const float DefaultReach = 100;
public const float MaxReach = 150;
public bool AllowToFindDivingGear { get; set; } = true; public bool AllowToFindDivingGear { get; set; } = true;
public bool MustBeSpecificItem { get; set; } public bool MustBeSpecificItem { get; set; }
@@ -76,7 +79,7 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1) public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) : base(character, objectiveManager, priorityModifier)
{ {
currSearchIndex = -1; currentSearchIndex = 0;
Equip = equip; Equip = equip;
originalTarget = targetItem; originalTarget = targetItem;
this.targetItem = targetItem; this.targetItem = targetItem;
@@ -89,7 +92,7 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, IEnumerable<Identifier> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false) public AIObjectiveGetItem(Character character, IEnumerable<Identifier> identifiersOrTags, AIObjectiveManager objectiveManager, bool equip = true, bool checkInventory = true, float priorityModifier = 1, bool spawnItemIfNotFound = false)
: base(character, objectiveManager, priorityModifier) : base(character, objectiveManager, priorityModifier)
{ {
currSearchIndex = -1; currentSearchIndex = 0;
Equip = equip; Equip = equip;
this.spawnItemIfNotFound = spawnItemIfNotFound; this.spawnItemIfNotFound = spawnItemIfNotFound;
this.checkInventory = checkInventory; this.checkInventory = checkInventory;
@@ -125,7 +128,7 @@ namespace Barotrauma
public static Func<PathNode, bool> CreateEndNodeFilter(ISpatialEntity targetEntity) public static Func<PathNode, bool> CreateEndNodeFilter(ISpatialEntity targetEntity)
{ {
return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(DefaultReach); return n => (n.Waypoint.Ladders == null || n.Waypoint.IsInWater) && Vector2.DistanceSquared(n.Waypoint.WorldPosition, targetEntity.WorldPosition) <= MathUtils.Pow2(MaxReach);
} }
private bool CheckInventory() private bool CheckInventory()
@@ -246,7 +249,7 @@ namespace Barotrauma
else else
{ {
character.SelectCharacter(c); character.SelectCharacter(c);
canInteract = character.CanInteractWith(c, maxDist: DefaultReach); canInteract = character.CanInteractWith(c);
character.DeselectCharacter(); character.DeselectCharacter();
} }
} }
@@ -268,7 +271,7 @@ namespace Barotrauma
Inventory itemInventory = targetItem.ParentInventory; Inventory itemInventory = targetItem.ParentInventory;
var slots = itemInventory?.FindIndices(targetItem); var slots = itemInventory?.FindIndices(targetItem);
if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true)) if (HumanAIController.TakeItem(targetItem, character.Inventory, Equip, Wear, storeUnequipped: true, targetTags: IdentifiersOrTags))
{ {
if (TakeWholeStack && slots != null) if (TakeWholeStack && slots != null)
{ {
@@ -298,8 +301,11 @@ namespace Barotrauma
if (!Equip) if (!Equip)
{ {
// Try equipping and wearing the item // Try equipping and wearing the item
Wear = true;
Equip = true; Equip = true;
if (!objectiveManager.HasActiveObjective<AIObjectiveCleanupItem>() && !objectiveManager.HasActiveObjective<AIObjectiveLoadItem>())
{
Wear = true;
}
return; return;
} }
#if DEBUG #if DEBUG
@@ -342,6 +348,10 @@ namespace Barotrauma
} }
} }
private Stopwatch sw;
private Stopwatch StopWatch => sw ??= new Stopwatch();
private readonly List<(Item item, float priority)> itemCandidates = new List<(Item, float)>();
private List<Item> itemList;
private void FindTargetItem() private void FindTargetItem()
{ {
if (IdentifiersOrTags == null) if (IdentifiersOrTags == null)
@@ -349,13 +359,16 @@ namespace Barotrauma
if (targetItem == null) if (targetItem == null)
{ {
#if DEBUG #if DEBUG
DebugConsole.NewMessage($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.", Color.Red); DebugConsole.AddWarning($"{character.Name}: Cannot find an item, because neither identifiers nor item was defined.");
#endif #endif
Abandon = true; Abandon = true;
} }
return; return;
} }
if (HumanAIController.DebugAI)
{
StopWatch.Restart();
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100); float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
if (!CheckPathForEachItem) if (!CheckPathForEachItem)
{ {
@@ -366,12 +379,22 @@ namespace Barotrauma
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective); CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
} }
bool checkPath = CheckPathForEachItem; bool checkPath = CheckPathForEachItem;
bool hasCalledPathFinder = false; // Reset if the character has switched subs.
int itemsPerFrame = (int)priority; if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true))
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
{ {
currSearchIndex++; currentSearchIndex = 0;
var item = Item.ItemList[currSearchIndex]; }
if (currentSearchIndex == 0)
{
itemCandidates.Clear();
itemList = character.Submarine.GetItems(alsoFromConnectedSubs: true);
}
int itemsPerFrame = (int)MathHelper.Lerp(30, 300, MathUtils.InverseLerp(10, 100, priority));
int checkedItems = 0;
for (int i = 0; i < itemsPerFrame && currentSearchIndex < itemList.Count; i++, currentSearchIndex++)
{
checkedItems++;
var item = itemList[currentSearchIndex];
Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine; Submarine itemSub = item.Submarine ?? item.ParentInventory?.Owner?.Submarine;
if (itemSub == null) { continue; } if (itemSub == null) { continue; }
Submarine mySub = character.Submarine; Submarine mySub = character.Submarine;
@@ -395,8 +418,6 @@ namespace Barotrauma
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; } if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
} }
} }
// Don't allow going into another sub, unless it's connected and of the same team and type.
if (!character.Submarine.IsEntityFoundOnThisSub(item, includingConnectedSubs: true)) { continue; }
if (character.IsItemTakenBySomeoneElse(item)) { continue; } if (character.IsItemTakenBySomeoneElse(item)) { continue; }
if (item.ParentInventory is ItemInventory itemInventory) if (item.ParentInventory is ItemInventory itemInventory)
{ {
@@ -411,11 +432,14 @@ namespace Barotrauma
if (rootInventoryOwner is Item ownerItem) if (rootInventoryOwner is Item ownerItem)
{ {
if (!ownerItem.IsInteractable(character)) { continue; } if (!ownerItem.IsInteractable(character)) { continue; }
if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; } if (ownerItem != item)
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
if (ownerItem != item.Container)
{ {
itemPriority *= 0.1f; if (!(ownerItem.GetComponent<ItemContainer>()?.HasRequiredItems(character, addMessage: false) ?? true)) { continue; }
//the item is inside an item inside an item (e.g. fuel tank in a welding tool in a cabinet -> reduce priority to prefer items that aren't inside a tool)
if (ownerItem != item.Container)
{
itemPriority *= 0.1f;
}
} }
} }
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition; Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
@@ -463,22 +487,69 @@ namespace Barotrauma
{ {
itemPriority *= item.Condition / item.MaxCondition; itemPriority *= item.Condition / item.MaxCondition;
} }
if (checkPath)
{
itemCandidates.Add((item, itemPriority));
}
// Ignore if the item has a lower priority than the currently selected one // Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; } if (itemPriority < currItemPriority) { continue; }
if (!hasCalledPathFinder && PathSteering != null && checkPath) if (EvaluateCombatPriority && itemPriority <= 0)
{ {
hasCalledPathFinder = true; // Not good enough
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null); continue;
if (path.Unreachable) { continue; }
} }
currItemPriority = itemPriority; currItemPriority = itemPriority;
targetItem = item; targetItem = item;
moveToTarget = rootInventoryOwner ?? item; moveToTarget = rootInventoryOwner ?? item;
} }
if (currSearchIndex >= Item.ItemList.Count - 1) if (currentSearchIndex >= itemList.Count - 1)
{ {
isDoneSeeking = true; isDoneSeeking = true;
if (targetItem == null) }
if (checkedItems > 0)
{
if (isDoneSeeking && itemCandidates.Any())
{
itemCandidates.Sort((x, y) => y.priority.CompareTo(x.priority));
}
if (HumanAIController.DebugAI && targetItem != null && StopWatch.ElapsedMilliseconds > 2)
{
var msg = $"Went through {checkedItems} of total {itemList.Count} items. Found item {targetItem.Name} in {StopWatch.ElapsedMilliseconds} ms. Completed: {isDoneSeeking}";
if (StopWatch.ElapsedMilliseconds > 5)
{
DebugConsole.ThrowError(msg);
}
else
{
// An occasional warning now and then can be ignored, but multiple at the same time might indicate a performance issue.
DebugConsole.AddWarning(msg);
}
}
}
if (isDoneSeeking)
{
if (PathSteering == null)
{
itemCandidates.Clear();
}
if (itemCandidates.Any())
{
if (itemCandidates.FirstOrDefault() is { } itemCandidate)
{
var path = PathSteering.PathFinder.FindPath(character.SimPosition, itemCandidate.item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable)
{
// Remove the invalid candidates and continue on the next frame.
itemCandidates.Remove(itemCandidate);
}
else
{
// The path was valid -> we are done.
itemCandidates.Clear();
}
}
}
if (targetItem == null && itemCandidates.None())
{ {
if (spawnItemIfNotFound) if (spawnItemIfNotFound)
{ {
@@ -569,11 +640,11 @@ namespace Barotrauma
{ {
if (!item.HasAccess(character)) { return false; } if (!item.HasAccess(character)) { return false; }
if (ignoredItems.Contains(item)) { return false; }; if (ignoredItems.Contains(item)) { return false; };
if (ignoredIdentifiersOrTags != null && CheckItemIdentifiersOrTags(item, ignoredIdentifiersOrTags)) { return false; } if (ignoredIdentifiersOrTags != null && item.HasIdentifierOrTags(ignoredIdentifiersOrTags)) { return false; }
if (item.Condition < TargetCondition) { return false; } if (item.Condition < TargetCondition) { return false; }
if (ItemFilter != null && !ItemFilter(item)) { return false; } if (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; } if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; }
return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf)); return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
} }
public override void Reset() public override void Reset()
@@ -591,7 +662,7 @@ namespace Barotrauma
targetItem = originalTarget; targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner(); moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false; isDoneSeeking = false;
currSearchIndex = 0; currentSearchIndex = 0;
currItemPriority = 0; currItemPriority = 0;
} }
@@ -257,7 +257,7 @@ namespace Barotrauma
} }
} }
} }
else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false)) else if (HumanAIController.HasValidPath(requireUnfinished: false))
{ {
waitUntilPathUnreachable = pathWaitingTime; waitUntilPathUnreachable = pathWaitingTime;
} }
@@ -364,7 +364,8 @@ namespace Barotrauma
else else
{ {
bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null; bool isRuins = character.Submarine?.Info.IsRuin != null || Target.Submarine?.Info.IsRuin != null;
if (!isRuins || !HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: true)) bool isEitherOneInside = isInside || Target.Submarine != null;
if (isEitherOneInside && (!isRuins || !HumanAIController.HasValidPath()))
{ {
SeekGaps(maxGapDistance); SeekGaps(maxGapDistance);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f); seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
@@ -388,6 +389,10 @@ namespace Barotrauma
} }
} }
} }
else
{
TargetGap = null;
}
} }
} }
else else
@@ -437,8 +442,8 @@ namespace Barotrauma
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand); var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand); var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull = bool handsFull =
(leftHandItem != null && character.Inventory.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) || (leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1); (rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
if (!handsFull) if (!handsFull)
{ {
bool hasBattery = false; bool hasBattery = false;
@@ -473,7 +478,7 @@ namespace Barotrauma
// Try to switch batteries // Try to switch batteries
if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false)) if (HumanAIController.HasItem(character, batteryTag, out IEnumerable<Item> batteries, conditionPercentage: 1, recursive: false))
{ {
scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.anySlot)); scooter.ContainedItems.ForEachMod(emptyBattery => character.Inventory.TryPutItem(emptyBattery, character, CharacterInventory.AnySlot));
if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character)) if (!scooter.Combine(batteries.OrderByDescending(b => b.Condition).First(), character))
{ {
useScooter = false; useScooter = false;
@@ -488,7 +493,7 @@ namespace Barotrauma
if (!useScooter) if (!useScooter)
{ {
// Unequip // Unequip
character.Inventory.TryPutItem(scooter, character, CharacterInventory.anySlot); character.Inventory.TryPutItem(scooter, character, CharacterInventory.AnySlot);
} }
} }
} }
@@ -511,13 +516,20 @@ namespace Barotrauma
{ {
nodeFilter = n => n.Waypoint.CurrentHull != null; nodeFilter = n => n.Waypoint.CurrentHull != null;
} }
else if (!isInside && HumanAIController.UseIndoorSteeringOutside) else if (!isInside)
{ {
nodeFilter = n => n.Waypoint.Submarine == null; if (HumanAIController.UseOutsideWaypoints)
{
nodeFilter = n => n.Waypoint.Submarine == null;
}
else
{
nodeFilter = n => n.Waypoint.Submarine != null || n.Waypoint.Ruin != null;
}
} }
if (!isInside && !UsePathingOutside) if (!isInside && !UsePathingOutside)
{ {
character.ReleaseSecondaryItem();
PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10); PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater) if (character.AnimController.InWater)
{ {
@@ -540,6 +552,7 @@ namespace Barotrauma
} }
else else
{ {
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition)); SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater) if (character.AnimController.InWater)
{ {
@@ -560,6 +573,7 @@ namespace Barotrauma
} }
else else
{ {
character.ReleaseSecondaryItem();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10); SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater) if (character.AnimController.InWater)
{ {
@@ -573,6 +587,7 @@ namespace Barotrauma
{ {
if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; } if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; }
SteeringManager.Reset(); SteeringManager.Reset();
character.ReleaseSecondaryItem();
character.CursorPosition = targetWorldPos; character.CursorPosition = targetWorldPos;
if (character.Submarine != null) if (character.Submarine != null)
{ {
@@ -624,7 +639,7 @@ namespace Barotrauma
} }
else if (target is Character c) else if (target is Character c)
{ {
return c.CurrentHull; return c.CurrentHull ?? c.AnimController.CurrentHull;
} }
else if (target is Structure structure) else if (target is Structure structure)
{ {
@@ -772,6 +787,14 @@ namespace Barotrauma
{ {
StopMovement(); StopMovement();
HumanAIController.FaceTarget(Target); HumanAIController.FaceTarget(Target);
if (Target is WayPoint { Ladders: null })
{
// Release ladders when ordered to wait at a spawnpoint.
// This is a special case specifically meant for NPCs that spawn in outposts with a wait order.
// Otherwise they might keep holding to the ladders when the target is just next to it.
// Releasing too early should be handled inside the IsCloseEnough property.
character.ReleaseSecondaryItem();
}
base.OnCompleted(); base.OnCompleted();
} }
@@ -781,6 +804,10 @@ namespace Barotrauma
findDivingGear = null; findDivingGear = null;
seekGapsTimer = 0; seekGapsTimer = 0;
TargetGap = null; TargetGap = null;
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
pathSteering.ResetPath();
}
} }
} }
} }
@@ -163,15 +163,22 @@ namespace Barotrauma
character.SelectedItem = null; character.SelectedItem = null;
CleanupItems(deltaTime); if (!character.IsClimbing)
{
CleanupItems(deltaTime);
}
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null) if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
{ {
TargetHull = character.CurrentHull; TargetHull = character.CurrentHull;
} }
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull))); bool currentTargetIsInvalid =
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid) currentTarget == null ||
IsForbidden(currentTarget) ||
(PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid && !HumanAIController.UnsafeHulls.Contains(TargetHull))
{ {
currentTarget = TargetHull; currentTarget = TargetHull;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing; bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
@@ -305,12 +312,8 @@ namespace Barotrauma
{ {
if (character.IsClimbing) if (character.IsClimbing)
{ {
if (character.AnimController.GetHeightFromFloor() < 0.1f) PathSteering.Reset();
{ character.StopClimbing();
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedSecondaryItem = null;
}
return;
} }
var currentHull = character.CurrentHull; var currentHull = character.CurrentHull;
if (!character.AnimController.InWater && currentHull != null) if (!character.AnimController.InWater && currentHull != null)
@@ -362,6 +365,7 @@ namespace Barotrauma
} }
else else
{ {
character.ReleaseSecondaryItem();
PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff)); PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
} }
return; return;
@@ -24,7 +24,7 @@ namespace Barotrauma
private ImmutableHashSet<Identifier> ValidContainableItemIdentifiers { get; } private ImmutableHashSet<Identifier> ValidContainableItemIdentifiers { get; }
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>(); private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
private int itemIndex = 0; private int itemIndex;
private AIObjectiveDecontainItem decontainObjective; private AIObjectiveDecontainItem decontainObjective;
private readonly HashSet<Item> ignoredItems = new HashSet<Item>(); private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
private Item targetItem; private Item targetItem;
@@ -219,9 +219,8 @@ namespace Barotrauma
return Priority; return Priority;
} }
public override void Update(float deltaTime) protected override void Act(float deltaTime)
{ {
base.Update(deltaTime);
if (targetItem == null) if (targetItem == null)
{ {
if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority)) if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority))
@@ -233,6 +232,7 @@ namespace Barotrauma
} }
targetItem = item; targetItem = item;
} }
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
float GetPriority(Item item) float GetPriority(Item item)
{ {
try try
@@ -256,11 +256,7 @@ namespace Barotrauma
} }
} }
} }
} else
protected override void Act(float deltaTime)
{
if (targetItem != null)
{ {
if(decontainObjective == null && !IsValidContainable(targetItem)) if(decontainObjective == null && !IsValidContainable(targetItem))
{ {
@@ -290,10 +286,6 @@ namespace Barotrauma
Reset(); Reset();
}); });
} }
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
} }
private bool IsValidContainable(Item item) private bool IsValidContainable(Item item)
@@ -310,7 +302,7 @@ namespace Barotrauma
if (parentItem.HasTag("donttakeitems")) { return false; } if (parentItem.HasTag("donttakeitems")) { return false; }
} }
if (!item.HasAccess(character)) { return false; } if (!item.HasAccess(character)) { return false; }
if (!character.HasItem(item) && !CanEquip(item)) { return false; } if (!character.HasItem(item) && !CanEquip(item, allowWearing: false)) { return false; }
if (!ItemContainer.CanBeContained(item)) { return false; } if (!ItemContainer.CanBeContained(item)) { return false; }
if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; } if (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; }
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full) if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full)
@@ -63,7 +63,7 @@ namespace Barotrauma
{ {
if (item == null || item.Removed) { return false; } if (item == null || item.Removed) { return false; }
if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; } if (targetContainerTags.HasValue && !OrderPrefab.TargetItemsMatchItem(targetContainerTags.Value, item)) { return false; }
if (!(item.GetComponent<ItemContainer>() is ItemContainer container)) { return false; } if ((item.GetComponent<ItemContainer>() is not ItemContainer container)) { return false; }
if (container.Inventory == null) { return false; } if (container.Inventory == null) { return false; }
if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; } if (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; }
if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; } if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; }
@@ -89,12 +89,7 @@ namespace Barotrauma
var target = objective.Key; var target = objective.Key;
if (!Targets.Contains(target)) if (!Targets.Contains(target))
{ {
var subObjective = objective.Value; subObjectives.Remove(objective.Value);
if (CurrentSubObjective == subObjective)
{
CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted;
}
subObjectives.Remove(subObjective);
} }
} }
SyncRemovedObjectives(Objectives, GetList()); SyncRemovedObjectives(Objectives, GetList());
@@ -157,6 +152,11 @@ namespace Barotrauma
else else
{ {
float max = AIObjectiveManager.LowestOrderPriority - 1; float max = AIObjectiveManager.LowestOrderPriority - 1;
if (this is AIObjectiveRescueAll rescueObjective && rescueObjective.Targets.Contains(character))
{
// Allow higher prio
max = AIObjectiveManager.EmergencyObjectivePriority;
}
float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1); float value = MathHelper.Clamp((CumulatedDevotion + (targetValue * PriorityModifier)) / 100, 0, 1);
Priority = MathHelper.Lerp(0, max, value); Priority = MathHelper.Lerp(0, max, value);
} }
@@ -20,6 +20,8 @@ namespace Barotrauma
MaxValue = 2 MaxValue = 2
} }
public const float MaxObjectivePriority = 100;
public const float EmergencyObjectivePriority = 90;
public const float HighestOrderPriority = 70; public const float HighestOrderPriority = 70;
public const float LowestOrderPriority = 60; public const float LowestOrderPriority = 60;
public const float RunPriority = 50; public const float RunPriority = 50;
@@ -125,11 +127,21 @@ namespace Barotrauma
{ {
CoroutineManager.StopCoroutines(delayedObjective.Value); CoroutineManager.StopCoroutines(delayedObjective.Value);
} }
var prevIdleObjective = GetObjective<AIObjectiveIdle>();
DelayedObjectives.Clear(); DelayedObjectives.Clear();
Objectives.Clear(); Objectives.Clear();
FailedAutonomousObjectives = false; FailedAutonomousObjectives = false;
AddObjective(new AIObjectiveFindSafety(character, this)); AddObjective(new AIObjectiveFindSafety(character, this));
AddObjective(new AIObjectiveIdle(character, this)); var newIdleObjective = new AIObjectiveIdle(character, this);
if (prevIdleObjective != null)
{
newIdleObjective.TargetHull = prevIdleObjective.TargetHull;
newIdleObjective.Behavior = prevIdleObjective.Behavior;
prevIdleObjective.PreferredOutpostModuleTypes.ForEach(t => newIdleObjective.PreferredOutpostModuleTypes.Add(t));
}
AddObjective(newIdleObjective);
int objectiveCount = Objectives.Count; int objectiveCount = Objectives.Count;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives) foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{ {
@@ -410,7 +422,7 @@ namespace Barotrauma
newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier) newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{ {
CloseEnough = Rand.Range(80f, 100f), CloseEnough = Rand.Range(80f, 100f),
CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver), onlyBots: true) * Rand.Range(0.8f, 1f), 4), CloseEnoughMultiplier = Math.Min(1 + HumanAIController.CountBotsInTheCrew(c => c.ObjectiveManager.HasOrder<AIObjectiveGoTo>(o => o.Target == order.OrderGiver)) * Rand.Range(0.8f, 1f), 4),
ExtraDistanceOutsideSub = 100, ExtraDistanceOutsideSub = 100,
ExtraDistanceWhileSwimming = 100, ExtraDistanceWhileSwimming = 100,
AllowGoingOutside = true, AllowGoingOutside = true,
@@ -633,10 +645,11 @@ namespace Barotrauma
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective(); public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T; public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
/// <summary> public T GetLastActiveObjective<T>() where T : AIObjective
/// Returns the last active objective of the specific type. => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T; public T GetFirstActiveObjective<T>() where T : AIObjective
=> CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).FirstOrDefault(so => so is T) as T;
/// <summary> /// <summary>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently. /// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.
@@ -122,7 +122,7 @@ namespace Barotrauma
else if (!isOrder) else if (!isOrder)
{ {
var steering = component?.Item.GetComponent<Steering>(); var steering = component?.Item.GetComponent<Steering>();
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain))) if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true)))
{ {
// Ignore if already set to autopilot or if there's a captain onboard // Ignore if already set to autopilot or if there's a captain onboard
Priority = 0; Priority = 0;
@@ -204,7 +204,7 @@ namespace Barotrauma
} }
if (operateTarget != null) if (operateTarget != null)
{ {
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget)) if (HumanAIController.IsTrueForAnyBotInTheCrew(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
{ {
// Another crew member is already targeting this entity (leak). // Another crew member is already targeting this entity (leak).
Abandon = true; Abandon = true;

Some files were not shown because too many files have changed in this diff Show More