Merge branch 'master' of https://github.com/Regalis11/Barotrauma into develop

This commit is contained in:
EvilFactory
2023-05-10 11:55:46 -03:00
273 changed files with 5739 additions and 2421 deletions

View File

@@ -54,8 +54,7 @@ body:
label: Version
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
options:
- v1.0.9.0
- Unstable (v1.1.4.0)
- v1.0.13.1
- Other
validations:
required: true

View File

@@ -10,7 +10,7 @@ namespace Barotrauma
public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
{
if (Character == Character.Controlled) { return; }
if (!debugai) { return; }
if (!DebugAI) { return; }
Vector2 pos = Character.WorldPosition;
pos.Y = -pos.Y;
Vector2 textOffset = new Vector2(-40, -160);

View File

@@ -19,6 +19,17 @@ namespace Barotrauma
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;
while (timer < time)
{
@@ -26,15 +37,16 @@ namespace Barotrauma
float m = MathHelper.Lerp(1, Config.DeadEntityColorMultiplier, MathUtils.InverseLerp(0, time, timer));
foreach (var item in thalamusItems)
{
if (item.Color.A == 0) { continue; }
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);
}
}
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);
}
yield return CoroutineStatus.Running;

View File

@@ -6,11 +6,17 @@ namespace Barotrauma
{
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; }
/// <summary>
/// Sound to play when the attack deals damage.
/// </summary>
private RoundSound sound;
/// <summary>
/// Particle emitter to use when the attack deals damage.
/// </summary>
private ParticleEmitter particleEmitter;
partial void InitProjSpecific(ContentXElement element)

View File

@@ -582,7 +582,7 @@ namespace Barotrauma
float closestItemDistance = Math.Max(aimAssistAmount, 2.0f);
foreach (MapEntity entity in entityList)
{
if (!(entity is Item item))
if (entity is not Item item)
{
continue;
}

View File

@@ -153,6 +153,7 @@ namespace Barotrauma
private static readonly List<BossProgressBar> bossProgressBars = new List<BossProgressBar>();
private static readonly Dictionary<Identifier, LocalizedString> cachedHudTexts = new Dictionary<Identifier, LocalizedString>();
private static LanguageIdentifier cachedHudTextLanguage = LanguageIdentifier.None;
private static GUILayoutGroup bossHealthContainer;
@@ -202,10 +203,15 @@ namespace Barotrauma
public static LocalizedString GetCachedHudText(string textTag, InputType keyBind)
{
if (cachedHudTextLanguage != GameSettings.CurrentConfig.Language)
{
cachedHudTexts.Clear();
}
Identifier key = (textTag + keyBind).ToIdentifier();
if (cachedHudTexts.TryGetValue(key, out LocalizedString text)) { return text; }
text = TextManager.GetWithVariable(textTag, "[key]", GameSettings.CurrentConfig.KeyMap.KeyBindText(keyBind)).Value;
cachedHudTexts.Add(key, text);
cachedHudTextLanguage = GameSettings.CurrentConfig.Language;
return text;
}

View File

@@ -701,10 +701,11 @@ namespace Barotrauma
blurStrength = Math.Max(blurStrength, affliction.GetScreenBlurStrength());
radialDistortStrength = Math.Max(radialDistortStrength, affliction.GetRadialDistortStrength());
chromaticAberrationStrength = Math.Max(chromaticAberrationStrength, affliction.GetChromaticAberrationStrength());
float afflictionGrainStrength = affliction.GetScreenGrainStrength();
if (afflictionGrainStrength > 0.0f)
{
grainStrength = Math.Max(grainStrength, affliction.GetScreenGrainStrength());
grainStrength = Math.Max(grainStrength, afflictionGrainStrength);
Color afflictionGrainColor = affliction.GetActiveEffect()?.GrainColor ?? Color.White;
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)
{
var affliction = kvp.Key;
if (affliction.Prefab.AfflictionOverlay != null)
{
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));
}
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));
}
float damageOverlayAlpha = DamageOverlayTimer;

View File

@@ -125,9 +125,11 @@ namespace Barotrauma
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
int startIndex = modVersion.Length - 1;
while (char.IsDigit(modVersion[startIndex])) { startIndex--; }
while (startIndex > 0 && char.IsDigit(modVersion[startIndex])) { startIndex--; }
startIndex++;
if (startIndex >= modVersion.Length

View File

@@ -427,6 +427,10 @@ namespace Barotrauma
{
CheatsEnabled = true;
SteamAchievementManager.CheatsEnabled = true;
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
campaign.CheatsEnabled = true;
}
NewMessage("Enabled cheat commands.", Color.Red);
#if USE_STEAM
NewMessage("Steam achievements have been disabled during this play session.", Color.Red);
@@ -642,15 +646,29 @@ namespace Barotrauma
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; }
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) =>
{
if (Submarine.MainSub == null) { return; }
MapEntity.SelectedList.Clear();
MapEntity.ClearHighlightedEntities();
WikiImage.Create(Submarine.MainSub);
try
{
MapEntity.SelectedList.Clear();
MapEntity.ClearHighlightedEntities();
WikiImage.Create(Submarine.MainSub);
}
catch (Exception e)
{
DebugConsole.ThrowError("The command 'wikiimage_sub' failed.", e);
}
}));
AssignRelayToServer("kick", false);
@@ -1156,7 +1174,7 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = true;
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);
@@ -1258,8 +1276,8 @@ namespace Barotrauma
AssignOnExecute("debugai", (string[] args) =>
{
HumanAIController.debugai = !HumanAIController.debugai;
if (HumanAIController.debugai)
HumanAIController.DebugAI = !HumanAIController.DebugAI;
if (HumanAIController.DebugAI)
{
GameMain.DevMode = true;
GameMain.DebugDraw = true;
@@ -1274,7 +1292,7 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = true;
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);
@@ -2333,7 +2351,7 @@ namespace Barotrauma
{
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.Y * item.Prefab.Scale));
}
@@ -2869,7 +2887,7 @@ namespace Barotrauma
NewMessage("Valid ranks are:", Color.White);
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) =>
{
@@ -3355,6 +3373,11 @@ namespace Barotrauma
else
{
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"));
}
});

View File

@@ -517,6 +517,10 @@ namespace Barotrauma
GlyphData gd = GetGlyphData(charIndex);
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];
Vector2 drawOffset;
drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y;

View File

@@ -240,7 +240,7 @@ namespace Barotrauma
private void UpdatePending()
{
if (!(pendingHealList is { } healList)) { return; }
if (pendingHealList is not { } healList) { return; }
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"))
{
OnClicked = (_, _) =>
OnClicked = (button, _) =>
{
if (isWaitingForServer) { return true; }
button.Enabled = false;
isWaitingForServer = true;
medicalClinic.TreatAllButtonAction(OnReceived);
bool wasSuccessful = medicalClinic.TreatAllButtonAction(_ => ReEnableButton());
if (!wasSuccessful) { ReEnableButton(); }
void ReEnableButton()
{
isWaitingForServer = false;
button.Enabled = true;
}
return true;
}
};
crewHealList = new CrewHealList(crewList, parent, treatAllButton);
void OnReceived(MedicalClinic.CallbackOnlyRequest obj)
{
isWaitingForServer = false;
}
}
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)
{
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
};
@@ -585,8 +591,10 @@ namespace Barotrauma
OnClicked = (button, _) =>
{
button.Enabled = false;
medicalClinic.HealAllButtonAction(request =>
isWaitingForServer = true;
bool wasSuccessful = medicalClinic.HealAllButtonAction(request =>
{
isWaitingForServer = false;
switch (request.HealResult)
{
case MedicalClinic.HealRequestResult.InsufficientFunds:
@@ -600,6 +608,12 @@ namespace Barotrauma
button.Enabled = true;
ClosePopup();
});
if (!wasSuccessful)
{
isWaitingForServer = false;
button.Enabled = true;
}
ClosePopup();
return true;
}
@@ -610,11 +624,19 @@ namespace Barotrauma
ClickSound = GUISoundType.Cart,
OnClicked = (button, _) =>
{
if (isWaitingForServer) { return true; }
button.Enabled = false;
medicalClinic.ClearAllButtonAction(_ =>
isWaitingForServer = true;
bool wasSuccessful = medicalClinic.ClearAllButtonAction(_ => ReEnableButton());
if (!wasSuccessful) { ReEnableButton(); }
void ReEnableButton()
{
isWaitingForServer = false;
button.Enabled = true;
});
}
return true;
}
};
@@ -701,10 +723,15 @@ namespace Barotrauma
OnClicked = (button, _) =>
{
button.Enabled = false;
medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ =>
bool wasSuccessful = medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ =>
{
button.Enabled = true;
});
if (!wasSuccessful)
{
button.Enabled = true;
}
return true;
}
};
@@ -792,7 +819,13 @@ namespace Barotrauma
selectedCrewAfflictionList = popupAfflictionList;
isWaitingForServer = true;
medicalClinic.RequestAfflictions(info, OnReceived);
bool wasSuccessful = medicalClinic.RequestAfflictions(info, OnReceived);
if (!wasSuccessful)
{
isWaitingForServer = false;
ClosePopup();
}
void OnReceived(MedicalClinic.AfflictionRequest request)
{
@@ -800,6 +833,16 @@ namespace Barotrauma
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.TextColor = GUIStyle.Red;
return;
@@ -953,14 +996,20 @@ namespace Barotrauma
}
existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray();
ToggleElements(ElementState.Disabled, elementsToDisable);
medicalClinic.AddPendingButtonAction(existingMember, request =>
bool wasSuccessful = medicalClinic.AddPendingButtonAction(existingMember, request =>
{
if (request.Result == MedicalClinic.RequestResult.Timeout)
{
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
@@ -1090,9 +1139,8 @@ namespace Barotrauma
{
return result switch
{
MedicalClinic.RequestResult.Error => TextManager.Get("error"),
MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"),
_ => "What the hell did you just do" // this should never happen
_ => TextManager.Get("error")
};
}

View File

@@ -1084,18 +1084,19 @@ namespace Barotrauma
if (save)
{
GUI.SetSavingIndicatorState(true);
if (GameSession.Submarine != null && !GameSession.Submarine.Removed)
{
GameSession.SubmarineInfo = new SubmarineInfo(GameSession.Submarine);
}
// 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)
if (GameSession.Campaign is CampaignMode campaign)
{
spCampaign.UpdateStoreStock();
if (campaign is SinglePlayerCampaign spCampaign && Level.IsLoadedFriendlyOutpost)
{
spCampaign.UpdateStoreStock();
}
GameSession.EventManager?.RegisterEventHistory(registerFinishedOnly: true);
campaign.End();
}
SaveUtil.SaveGame(GameSession.SavePath);
}

View File

@@ -148,7 +148,10 @@ namespace Barotrauma
}
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);
LocalizedString buttonText = "";
switch (availableTransition)
@@ -159,12 +162,12 @@ namespace Barotrauma
{
string textTag = availableTransition == TransitionType.ProgressToNextLocation ? "EnterLocation" : "EnterEmptyLocation";
buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.EndLocation?.Name ?? "[ERROR]");
endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI;
allowEndingRound = !ForceMapUI && !ShowCampaignUI;
}
break;
case TransitionType.LeaveLocation:
buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]");
endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI;
allowEndingRound = !ForceMapUI && !ShowCampaignUI;
break;
case TransitionType.ReturnToPreviousLocation:
case TransitionType.ReturnToPreviousEmptyLocation:
@@ -172,32 +175,37 @@ namespace Barotrauma
{
string textTag = availableTransition == TransitionType.ReturnToPreviousLocation ? "EnterLocation" : "EnterEmptyLocation";
buttonText = TextManager.GetWithVariable(textTag, "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]");
endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI;
allowEndingRound = !ForceMapUI && !ShowCampaignUI;
}
break;
case TransitionType.None:
default:
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)))
bool inFriendlySub = Character.Controlled is { IsInFriendlySub: true };
if (Level.Loaded.Type == LevelData.LevelType.Outpost && !Level.Loaded.IsEndBiome &&
(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]");
endRoundButton.Visible = !ForceMapUI && !ShowCampaignUI;
allowEndingRound = !ForceMapUI && !ShowCampaignUI;
}
else
{
endRoundButton.Visible = false;
allowEndingRound = false;
}
break;
}
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 (!AllowedToManageCampaign(ClientPermissions.ManageMap))
@@ -215,7 +223,11 @@ namespace Barotrauma
prevCampaignUIAutoOpenType = availableTransition;
}
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;
}
@@ -328,6 +340,11 @@ namespace Barotrauma
CampaignUI.UpgradeStore?.RequestRefresh();
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()

View File

@@ -348,7 +348,7 @@ namespace Barotrauma
//--------------------------------------
//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)
{
if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; }
@@ -358,8 +358,12 @@ namespace Barotrauma
endTransition.Stop();
overlayColor = Color.Transparent;
if (DateTime.Now > timeOut) { GameMain.NetLobbyScreen.Select(); }
if (!(Screen.Selected is RoundSummaryScreen))
if (DateTime.Now > timeOut)
{
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)
{
@@ -947,7 +951,9 @@ namespace Barotrauma
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);
if (hiredCharacters.Any()) { CampaignUI.CrewManagement.ValidateHires(hiredCharacters); }

View File

@@ -17,7 +17,8 @@ namespace Barotrauma
{
Undecided,
Success,
Error,
CharacterInfoMissing,
CharacterNotFound,
Timeout
}
@@ -34,7 +35,9 @@ namespace Barotrauma
private readonly List<RequestAction<CallbackOnlyRequest>> addRequests = 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)
{
@@ -42,23 +45,26 @@ namespace Barotrauma
if (Screen.Selected is TestScreen)
{
onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray()));
return;
return true;
}
#endif
if (info is not { Character.CharacterHealth: { } health })
{
onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray<NetAffliction>.Empty));
return;
onReceived.Invoke(new AfflictionRequest(RequestResult.CharacterInfoMissing, ImmutableArray<NetAffliction>.Empty));
return true;
}
ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health).ToImmutableArray();
ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health);
onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions));
return;
return true;
}
afflictionRequests.Add(new RequestAction<AfflictionRequest>(onReceived, GetTimeout()));
SendAfflictionRequest(info);
return requestBucket.TryEnqueue(() =>
{
afflictionRequests.Add(new RequestAction<AfflictionRequest>(onReceived, GetTimeout()));
SendAfflictionRequest(info);
});
}
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
if (GameMain.IsSingleplayer) { return; }
pendingHealRequests.Add(new RequestAction<PendingRequest>(onReceived, GetTimeout()));
SendPendingRequest();
requestBucket.TryEnqueue(() =>
{
pendingHealRequests.Add(new RequestAction<PendingRequest>(onReceived, GetTimeout()));
SendPendingRequest();
});
}
public void Update(float deltaTime)
@@ -79,6 +88,7 @@ namespace Barotrauma
UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout);
requestBucket.Update(deltaTime);
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();
}
public void TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived)
public bool TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
AddEverythingToPending();
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
return true;
}
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
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)
{
@@ -171,33 +185,39 @@ namespace Barotrauma
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()));
ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
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)
{
ClearPendingHeals();
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
return true;
}
clearAllRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
clearAllRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable);
});
}
private void ClearRequstReceived()
private void ClearRequestReceived()
{
ClearPendingHeals();
if (TryDequeue(clearAllRequests, out var callback))
@@ -224,28 +244,31 @@ namespace Barotrauma
OnUpdate?.Invoke();
}
public void AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived)
public bool AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
InsertPendingCrewMember(crewMember);
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
return true;
}
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
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)
{
RemovePendingAffliction(crewMember, affliction);
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
return true;
}
INetSerializableStruct removedAffliction = new NetRemovedAffliction
@@ -254,11 +277,14 @@ namespace Barotrauma
Affliction = affliction
};
removeRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
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);
foreach (var crewMember in crewMembers)
@@ -300,7 +326,7 @@ namespace Barotrauma
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
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()));
}
}
@@ -336,7 +362,7 @@ namespace Barotrauma
IWriteMessage msg = StartSending();
msg.WriteByte((byte)header);
netStruct?.Write(msg);
GameMain.Client.ClientPeer?.Send(msg, deliveryMethod);
GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod);
}
public void ClientRead(IReadMessage inc)
@@ -356,7 +382,7 @@ namespace Barotrauma
PendingRequestReceived(inc);
break;
case NetworkHeader.ADD_PENDING:
NewAdditonReceived(inc, flag);
NewAdditionReceived(inc, flag);
break;
case NetworkHeader.REMOVE_PENDING:
NewRemovalReceived(inc, flag);
@@ -365,7 +391,7 @@ namespace Barotrauma
HealRequestReceived(inc);
break;
case NetworkHeader.CLEAR_PENDING:
ClearRequstReceived();
ClearRequestReceived();
break;
}
}

View File

@@ -40,7 +40,7 @@ namespace Barotrauma
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);
msgBox = new GUIMessageBox(readyCheckHeader, readyCheckBody(author), new[] { yesButton, noButton }, relativeSize, minSize, type: GUIMessageBox.Type.Vote) { UserData = PromptData, Draggable = true };

View File

@@ -213,10 +213,11 @@ namespace Barotrauma
};
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)
{
if (missionsToDisplay.Contains(mission)) { continue; }
if (!mission.Prefab.ShowInMenus) { continue; }
if (mission.Locations[0] == mission.Locations[1] ||
mission.Locations.Contains(campaignMode?.Map.SelectedLocation))
@@ -316,7 +317,7 @@ namespace Barotrauma
RichString reputationText = displayedMission.GetReputationRewardText();
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);

View File

@@ -88,6 +88,11 @@ namespace Barotrauma.Items.Components
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
UInt16 userID = msg.ReadUInt16();
if (userID != Entity.NullEntityID)
{
user = Entity.FindEntityByID(userID) as Character;
}
CurrPowerConsumption = powerConsumption;
charging = true;
timer = Duration;

View File

@@ -21,6 +21,9 @@ namespace Barotrauma.Items.Components
/// </summary>
private float lightColorMultiplier;
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The scale of the light sprite.")]
public float LightSpriteScale { get; set; }
public Vector2 DrawSize
{
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);
}
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);
}
}

View File

@@ -528,7 +528,7 @@ namespace Barotrauma.Items.Components
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);
if (suitableIngredients.Count() > 3) { toolTipText += "..."; }
if (requiredItem.UseCondition && requiredItem.MinCondition < 1.0f)
@@ -550,6 +550,8 @@ namespace Barotrauma.Items.Components
{
toolTipText = TextManager.GetWithVariable("displayname.emptyitem", "[itemname]", toolTipText);
}
toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖";
if (!requiredItemPrefab.Description.IsNullOrEmpty())
{
toolTipText += '\n' + requiredItemPrefab.Description;
@@ -594,7 +596,7 @@ namespace Barotrauma.Items.Components
if (tooltip != null)
{
GUIComponent.DrawToolTip(spriteBatch, tooltip.Tooltip, tooltip.TargetElement);
GUIComponent.DrawToolTip(spriteBatch, RichString.Rich(tooltip.Tooltip), tooltip.TargetElement);
tooltip = null;
}
}

View File

@@ -403,7 +403,8 @@ namespace Barotrauma.Items.Components
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.GetComponent<Pickable>() == null) { return false; }
@@ -702,6 +703,12 @@ namespace Barotrauma.Items.Components
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)
{
Vector2 textSize = GUIStyle.Font.MeasureString(noPowerTip);
@@ -1057,7 +1064,9 @@ namespace Barotrauma.Items.Components
waterVolume += linkedHull.WaterVolume;
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
{

View File

@@ -1311,7 +1311,6 @@ namespace Barotrauma.Items.Components
float worldPingRadiusSqr = worldPingRadius * worldPingRadius;
disruptedDirections.Clear();
if (Level.Loaded == null) { return; }
for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
{
@@ -1434,8 +1433,10 @@ namespace Barotrauma.Items.Components
if (connectedSubs.Contains(submarine)) { continue; }
}
Rectangle worldBorders = Submarine.MainSub.GetDockedBorders();
worldBorders.Location += Submarine.MainSub.WorldPosition.ToPoint();
//display the actual walls if the ping source is inside the sub (but not inside a hull, that's handled above)
//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))
{
CreateBlipsForSubmarineWalls(submarine, pingSource, transducerPos, pingRadius, prevPingRadius, range, passive);

View File

@@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components
User = Entity.FindEntityByID(userId) as Character;
Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle());
float rotation = msg.ReadSingle();
SpreadCounter = msg.ReadByte();
spreadIndex = msg.ReadByte();
if (User != null)
{
Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false);

View File

@@ -12,6 +12,8 @@ namespace Barotrauma.Items.Components
private readonly List<GUIComponent> uiElements = new List<GUIComponent>();
private GUILayoutGroup uiElementContainer;
private bool readingNetworkEvent;
private Point ElementMaxSize => new Point(uiElementContainer.Rect.Width, (int)(65 * GUI.yScale));
public override bool RecreateGUIOnResolutionChange => true;
@@ -100,7 +102,7 @@ namespace Barotrauma.Items.Components
{
ValueChanged(ni.UserData as CustomInterfaceElement, ni.FloatValue);
}
else
else if (!readingNetworkEvent)
{
item.CreateClientEvent(this);
}
@@ -126,7 +128,7 @@ namespace Barotrauma.Items.Components
{
ValueChanged(ni.UserData as CustomInterfaceElement, ni.IntValue);
}
else
else if (!readingNetworkEvent)
{
item.CreateClientEvent(this);
}
@@ -161,7 +163,7 @@ namespace Barotrauma.Items.Components
{
TickBoxToggled(tBox.UserData as CustomInterfaceElement, tBox.Selected);
}
else
else if (!readingNetworkEvent)
{
item.CreateClientEvent(this);
}
@@ -181,12 +183,12 @@ namespace Barotrauma.Items.Components
};
btn.OnClicked += (_, userdata) =>
{
CustomInterfaceElement btnElement = userdata as CustomInterfaceElement;;
CustomInterfaceElement btnElement = userdata as CustomInterfaceElement;
if (GameMain.Client == null)
{
ButtonClicked(btnElement);
}
else
else if (!readingNetworkEvent)
{
item.CreateClientEvent(this, new EventData(btnElement));
}
@@ -248,7 +250,7 @@ namespace Barotrauma.Items.Components
int visibleElementCount = 0;
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);
if (visible) { visibleElementCount++; }
if (uiElement.Visible != visible)
@@ -297,9 +299,10 @@ namespace Barotrauma.Items.Components
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()) :
customInterfaceElementList[elementIndex].Label;
TextManager.Get(label).Fallback(label);
}
uiElementContainer.Recalculate();
@@ -334,7 +337,9 @@ namespace Barotrauma.Items.Components
{
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)
{
@@ -386,45 +391,53 @@ namespace Barotrauma.Items.Components
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
for (int i = 0; i < customInterfaceElementList.Count; i++)
readingNetworkEvent = true;
try
{
var element = customInterfaceElementList[i];
if (element.HasPropertyName)
for (int i = 0; i < customInterfaceElementList.Count; i++)
{
string newValue = msg.ReadString();
if (!element.IsNumberInput)
var element = customInterfaceElementList[i];
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
{
switch (element.NumberType)
bool elementState = msg.ReadBoolean();
if (element.ContinuousSignal)
{
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;
((GUITickBox)uiElements[i]).Selected = elementState;
TickBoxToggled(element, elementState);
}
else if (elementState)
{
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;
}
}
}
}

View File

@@ -212,7 +212,7 @@ namespace Barotrauma.Items.Components
Sprite pingCircle = GUIStyle.UIThermalGlow.Value.Sprite;
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 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);

View File

@@ -1398,7 +1398,7 @@ namespace Barotrauma
}
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;
@@ -1411,13 +1411,14 @@ namespace Barotrauma
}
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;
case EventType.Status:
bool loadingRound = msg.ReadBoolean();
float newCondition = msg.ReadSingle();
SetCondition(newCondition, isNetworkEvent: true);
SetCondition(newCondition, isNetworkEvent: true, executeEffects: !loadingRound);
break;
case EventType.AssignCampaignInteraction:
CampaignInteractionType = (CampaignMode.InteractionType)msg.ReadByte();
@@ -1459,9 +1460,9 @@ namespace Barotrauma
byte length = msg.ReadByte();
for (int i = 0; i < length; i++)
{
var statIdentifier = INetSerializableStruct.Read<ItemStatManager.TalentStatIdentifier>(msg);
var statIdentifier = INetSerializableStruct.Read<TalentStatIdentifier>(msg);
var statValue = msg.ReadSingle();
StatManager.ApplyStat(statIdentifier, statValue);
StatManager.ApplyStatDirect(statIdentifier, statValue);
}
break;
case EventType.Upgrade:

View File

@@ -2,6 +2,7 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Linq;
namespace Barotrauma
{
@@ -320,5 +321,45 @@ namespace Barotrauma
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;
}
}
}

View File

@@ -240,7 +240,8 @@ namespace Barotrauma.Lights
range *
((Character.Controlled?.Submarine != null && light.ParentSub == Character.Controlled?.Submarine) ? 2.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

View File

@@ -377,6 +377,8 @@ namespace Barotrauma.Lights
public float Priority;
public float PriorityMultiplier = 1.0f;
private Vector2 lightTextureTargetSize;
public Vector2 LightTextureTargetSize

View File

@@ -537,7 +537,7 @@ namespace Barotrauma
float damage = msg.ReadRangedSingle(0.0f, 1.0f, 8) * MaxHealth;
if (!invalidMessage && i < Sections.Length)
{
SetDamage(i, damage);
SetDamage(i, damage, isNetworkEvent: true);
}
}
}

View File

@@ -75,7 +75,7 @@ namespace Barotrauma.Networking
float gain = 1.0f;
float noiseGain = 0.0f;
Vector3? position = null;
if (character != null)
if (character != null && !character.IsDead)
{
if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat)
{

View File

@@ -8,7 +8,6 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
@@ -16,6 +15,10 @@ namespace Barotrauma.Networking
{
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 IsServer => false;
@@ -76,6 +79,8 @@ namespace Barotrauma.Networking
Interrupted
}
private UInt16? debugStartGameCampaignSaveID;
private RoundInitStatus roundInitStatus = RoundInitStatus.NotStarted;
public bool RoundStarting => roundInitStatus == RoundInitStatus.Starting || roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize;
@@ -512,6 +517,7 @@ namespace Barotrauma.Networking
DisplayInLoadingScreens = true
};
Quit();
GUI.DisableHUD = false;
GameMain.ServerListScreen.Select();
return;
}
@@ -861,17 +867,24 @@ namespace Barotrauma.Networking
ContentFile file = ContentPackageManager.EnabledPackages.All
.Select(p =>
p.Files.FirstOrDefault(f => f.Path == filePath))
.FirstOrDefault(f => !(f is null));
.FirstOrDefault(f => f is not null);
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);
int subEqualityCheckValue = inc.ReadInt32();
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." +
" There may have been an error in receiving the up-to-date submarine file from the server.";
string errorMsg =
"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);
throw new Exception(errorMsg);
}
@@ -888,7 +901,7 @@ namespace Barotrauma.Networking
$"Mission equality check failed. Mission count doesn't match the server. " +
$"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
$"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);
throw new Exception(errorMsg);
}
@@ -901,7 +914,7 @@ namespace Barotrauma.Networking
$"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " +
$"Server: {string.Join(", ", serverMissionIdentifiers)}, " +
$"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);
throw new Exception(errorMsg);
}
@@ -924,7 +937,7 @@ namespace Barotrauma.Networking
", level value count: " + levelEqualityCheckValues.Count +
", seed: " + Level.Loaded.Seed +
", 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);
throw new Exception(errorMsg);
}
@@ -1324,6 +1337,8 @@ namespace Barotrauma.Networking
eventErrorWritten = false;
GameMain.NetLobbyScreen.StopWaitingForStartRound();
debugStartGameCampaignSaveID = null;
while (CoroutineManager.IsCoroutineRunning("EndGame"))
{
EndCinematic?.Stop();
@@ -1473,7 +1488,28 @@ namespace Barotrauma.Networking
roundInitStatus = RoundInitStatus.Interrupted;
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;
DebugConsole.ThrowError("Failed to start campaign round (campaign map not loaded yet).");
@@ -1482,30 +1518,14 @@ namespace Barotrauma.Networking
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);
LevelData levelData = nextLocationIndex > -1 ?
campaign.Map.Locations[nextLocationIndex].LevelData :
campaign.Map.Connections[nextConnectionIndex].LevelData;
debugStartGameCampaignSaveID = campaign.LastSaveID;
if (roundSummary != null)
{
loadTask = campaign.SelectSummaryScreen(roundSummary, levelData, mirrorLevel, null);
@@ -1699,7 +1719,7 @@ namespace Barotrauma.Networking
yield return CoroutineStatus.Success;
}
if (GameMain.GameSession != null) { GameMain.GameSession.EndRound(endMessage, traitorResults, transitionType); }
GameMain.GameSession?.EndRound(endMessage, traitorResults, transitionType);
ServerSettings.ServerDetailsChanged = true;
@@ -2591,31 +2611,24 @@ namespace Barotrauma.Networking
public void WriteCharacterInfo(IWriteMessage msg, string newName = null)
{
msg.WriteBoolean(characterInfo == null);
msg.WritePadBits();
if (characterInfo == null) { return; }
msg.WriteString(newName ?? string.Empty);
var head = characterInfo.Head;
msg.WriteByte((byte)characterInfo.Head.Preset.TagSet.Count);
foreach (Identifier tag in characterInfo.Head.Preset.TagSet)
{
msg.WriteIdentifier(tag);
}
msg.WriteByte((byte)characterInfo.Head.HairIndex);
msg.WriteByte((byte)characterInfo.Head.BeardIndex);
msg.WriteByte((byte)characterInfo.Head.MoustacheIndex);
msg.WriteByte((byte)characterInfo.Head.FaceAttachmentIndex);
msg.WriteColorR8G8B8(characterInfo.Head.SkinColor);
msg.WriteColorR8G8B8(characterInfo.Head.HairColor);
msg.WriteColorR8G8B8(characterInfo.Head.FacialHairColor);
var netInfo = new NetCharacterInfo(
NewName: newName ?? string.Empty,
Tags: head.Preset.TagSet.ToImmutableArray(),
HairIndex: (byte)head.HairIndex,
BeardIndex: (byte)head.BeardIndex,
MoustacheIndex: (byte)head.MoustacheIndex,
FaceAttachmentIndex: (byte)head.FaceAttachmentIndex,
SkinColor: head.SkinColor,
HairColor: head.HairColor,
FacialHairColor: head.FacialHairColor,
JobVariants: GameMain.NetLobbyScreen.JobPreferences.Select(NetJobVariant.FromJobVariant).ToImmutableArray());
var jobPreferences = GameMain.NetLobbyScreen.JobPreferences;
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);
}
msg.WriteNetSerializableStruct(netInfo);
}
public void Vote(VoteType voteType, object data)
@@ -2871,12 +2884,14 @@ namespace Barotrauma.Networking
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.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"));
return false;

View File

@@ -92,10 +92,10 @@ namespace Barotrauma.Networking
Name = GameMain.Client.Name,
OwnerKey = ownerKey,
SteamId = SteamManager.GetSteamId().Select(id => (AccountId)id),
SteamAuthTicket = steamAuthTicket switch
SteamAuthTicket = steamAuthTicket?.Data switch
{
null => Option<byte[]>.None(),
var ticket => Option<byte[]>.Some(ticket.Data)
var ticketData => Option<byte[]>.Some(ticketData)
},
GameVersion = GameMain.Version.ToString(),
Language = GameSettings.CurrentConfig.Language.Value

View File

@@ -111,8 +111,25 @@ namespace Barotrauma.Networking
? NetworkConnection.TimeoutThresholdInGame
: 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);
if (!packetHeader.IsServerMessage()) { return; }

View File

@@ -141,15 +141,31 @@ namespace Barotrauma.Networking
if (remotePeer.DisconnectTime != null) { return; }
var peerPacketHeaders = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
PacketHeader packetHeader = peerPacketHeaders.PacketHeader;
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
}
}
if (!remotePeer.Authenticated && !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep())
private void ProcessP2PData(ulong steamId, RemotePeer remotePeer, IReadMessage inc)
{
var (deliveryMethod, packetHeader, connectionInitialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (remotePeer is { Authenticated: false, Authenticating: false } && packetHeader.IsConnectionInitializationStep())
{
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)
{
remotePeer.Authenticating = true;
@@ -181,6 +197,7 @@ namespace Barotrauma.Networking
ForwardToServerProcess(outMsg);
}
}
public override void Update(float deltaTime)

View File

@@ -428,7 +428,7 @@ namespace Barotrauma.Networking
if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; }
Language = valueGetter("language")?.ToLanguageIdentifier() ?? LanguageIdentifier.None;
ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray();
ContentPackages = ExtractContentPackageInfo(ServerName, valueGetter).ToImmutableArray();
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? joinedHashes = valueGetter("contentpackagehash");
string? joinedWorkshopIds = valueGetter("contentpackageid");
@@ -448,9 +474,11 @@ namespace Barotrauma.Networking
#warning TODO: genericize
ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray();
if (contentPackageNames.Length != contentPackageHashes.Length
|| contentPackageHashes.Length != contentPackageIds.Length)
if (contentPackageNames.Length != contentPackageHashes.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>();
}

View File

@@ -35,7 +35,7 @@ namespace Barotrauma
}
private static Option<ServerInfo> InfoFromListEntry(Steamworks.Data.ServerInfo entry) =>
entry.Name.IsNullOrEmpty()
entry.Name.IsNullOrEmpty() || entry.Address is null
? Option<ServerInfo>.None()
: Option<ServerInfo>.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort))
{

View File

@@ -71,10 +71,10 @@ namespace Barotrauma
foreach (var lobby in lobbies)
{
string lobbyOwnerStr = lobby.GetData("lobbyowner");
string lobbyOwnerStr = lobby.GetData("lobbyowner") ?? "";
lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr);
string serverName = lobby.GetData("name");
string serverName = lobby.GetData("name") ?? "";
if (string.IsNullOrEmpty(serverName)) { continue; }
var ownerId = SteamId.Parse(lobbyOwnerStr);

View File

@@ -9,6 +9,9 @@ namespace Barotrauma.Networking
{
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
{
public GUIComponent GUIComponent;
@@ -28,7 +31,15 @@ namespace Barotrauma.Networking
if (GUIComponent == null) return null;
else if (GUIComponent is GUITickBox tickBox) return tickBox.Selected;
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 GUIDropDown dropdown) return dropdown.SelectedData;
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 GUIScrollBar scrollBar)
{
if (value.GetType() == typeof(int))
if (value is int i)
{
scrollBar.BarScrollValue = (int)value;
scrollBar.BarScrollValue = i;
}
else
{
@@ -941,11 +952,58 @@ namespace Barotrauma.Networking
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 --------------------------------------------------------------------------
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);
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));
foreach (string karmaPreset in GameMain.NetworkMember.KarmaManager.Presets.Keys)
{

View File

@@ -276,6 +276,8 @@ namespace Barotrauma.Networking
if (GameMain.Client?.Character != null)
{
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]);
}
//encode audio and enqueue it

View File

@@ -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.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 listenerPos = GameMain.SoundManager.ListenerPosition;

View File

@@ -169,7 +169,10 @@ namespace Barotrauma
sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language);
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("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")"));

View File

@@ -18,7 +18,7 @@ namespace Barotrauma
public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; }
private GUIButton nextButton;
private GUILayoutGroup characterInfoColumns;
private GUIListBox characterInfoColumns;
public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable<SubmarineInfo> submarines, IEnumerable<CampaignMode.SaveInfo> saveFiles = null)
: base(newGameContainer, loadGameContainer)
@@ -249,11 +249,7 @@ namespace Barotrauma
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform),
TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft);
characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true)
{
Stretch = true,
RelativeSpacing = 0.01f
};
characterInfoColumns = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true);
var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f),
secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true)
@@ -306,8 +302,8 @@ namespace Barotrauma
for (int i = 0; i < characterInfos.Count; i++)
{
var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f / characterInfos.Count, 1.0f),
characterInfoColumns.RectTransform));
var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(Math.Max(1.0f / characterInfos.Count, 0.33f), 1.0f),
characterInfoColumns.Content.RectTransform));
var (characterInfo, job) = characterInfos[i];

View File

@@ -202,18 +202,21 @@ namespace Barotrauma
case CampaignMode.InteractionType.PurchaseSub:
submarineSelection?.Update();
break;
case CampaignMode.InteractionType.Crew:
CrewManagement?.Update();
break;
case CampaignMode.InteractionType.Store:
Store?.Update(deltaTime);
break;
break;
case CampaignMode.InteractionType.MedicalClinic:
MedicalClinic?.Update(deltaTime);
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;
missionList.Enabled = false;
}
//locationInfoPanel?.UpdateAuto(1.0f);
}
public void SelectTab(CampaignMode.InteractionType tab, Character npc = null)

View File

@@ -343,7 +343,7 @@ namespace Barotrauma
editorContainer.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) },
genParams.Identifier.Value)
@@ -359,7 +359,7 @@ namespace Barotrauma
editorContainer.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) },
genParams.Name)
@@ -375,7 +375,7 @@ namespace Barotrauma
editorContainer.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) },
genParams.Name)
@@ -391,7 +391,7 @@ namespace Barotrauma
editorContainer.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) },
genParams.Name)

View File

@@ -2244,9 +2244,9 @@ namespace Barotrauma
List<ContextMenuOption> rankOptions = new List<ContextMenuOption>();
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") });
msgBox.Buttons[0].OnClicked = delegate
@@ -2350,7 +2350,7 @@ namespace Barotrauma
};
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);

View File

@@ -1,16 +1,16 @@
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Microsoft.Xna.Framework.Input;
using Barotrauma.IO;
using Barotrauma.Steam;
namespace Barotrauma
{
@@ -3549,10 +3549,45 @@ namespace Barotrauma
TextManager.Get("LoadingVanillaSubmarineHeader"),
TextManager.Get("LoadingVanillaSubmarineDesc"));
public void LoadSub(SubmarineInfo info)
public void LoadSub(SubmarineInfo info, bool checkIdConflicts = true)
{
Submarine.Unload();
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
{
selectedSub = new Submarine(info);
@@ -5266,7 +5301,7 @@ namespace Barotrauma
}
}
if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default)
if (PlayerInput.KeyHit(InputType.Use) && mode == Mode.Default)
{
if (dummyCharacter != null)
{
@@ -5357,6 +5392,16 @@ namespace Barotrauma
else
{
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)
{
selectables.ForEach(MapEntity.AddSelection);

View File

@@ -94,7 +94,7 @@ namespace Barotrauma.Steam
CanBeFocused = false
};
var itemTitle = new GUITextBlock(new RectTransform(Vector2.One, itemFrame.RectTransform),
text: item.Title);
text: item.Title ?? "");
var itemDownloadProgress
= new GUIProgressBar(new RectTransform((0.5f, 0.75f),
itemFrame.RectTransform, Anchor.CenterRight), 0.0f)

View File

@@ -105,7 +105,8 @@ namespace Barotrauma.Steam
currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
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("subselectionmode", serverSettings.SubSelectionMode.ToString());
currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString());

View File

@@ -16,6 +16,9 @@ namespace Barotrauma.Steam
private static readonly List<Identifier> initializationErrors = new List<Identifier>();
public static IReadOnlyList<Identifier> InitializationErrors => initializationErrors;
private static bool IsInitializedProjectSpecific
=> Steamworks.SteamClient.IsValid && Steamworks.SteamClient.IsLoggedOn;
private static void InitializeProjectSpecific()
{
if (IsInitialized) { return; }
@@ -23,7 +26,6 @@ namespace Barotrauma.Steam
try
{
Steamworks.SteamClient.Init(AppID, false);
IsInitialized = Steamworks.SteamClient.IsLoggedOn && Steamworks.SteamClient.IsValid;
if (IsInitialized)
{
@@ -43,13 +45,11 @@ namespace Barotrauma.Steam
}
catch (DllNotFoundException)
{
IsInitialized = false;
initializationErrors.Add("SteamDllNotFound".ToIdentifier());
}
catch (Exception e)
{
DebugConsole.ThrowError("SteamManager initialization threw an exception", e);
IsInitialized = false;
initializationErrors.Add("SteamClientInitFailed".ToIdentifier());
}

View File

@@ -111,7 +111,7 @@ namespace Barotrauma.Steam
{
await Task.Yield();
string thumbnailUrl = item.PreviewImageUrl;
string? thumbnailUrl = item.PreviewImageUrl;
if (thumbnailUrl.IsNullOrWhiteSpace()) { return null; }
var client = new RestClient(thumbnailUrl);
var request = new RestRequest(".", Method.GET);

View File

@@ -17,7 +17,7 @@ namespace Barotrauma.Steam
private string ExtractTitle(ItemOrPackage itemOrPackage)
=> itemOrPackage.TryGet(out ContentPackage package)
? package.Name
: ((Steamworks.Ugc.Item)itemOrPackage).Title;
: (((Steamworks.Ugc.Item)itemOrPackage).Title ?? "");
private void CreateWorkshopItemDetailContainer(
GUIFrame parent,
@@ -340,6 +340,8 @@ namespace Barotrauma.Steam
subscribeButton.OnClicked = (button, o) =>
{
if (!SteamManager.IsInitialized) { return false; }
if (!workshopItem.IsSubscribed)
{
workshopItem.Subscribe();
@@ -360,6 +362,8 @@ namespace Barotrauma.Steam
new RectTransform(Vector2.Zero, subscribeButton.RectTransform),
onUpdate: (deltaTime, component) =>
{
if (!SteamManager.IsInitialized) { return; }
if (subscribeButtonSprite.Style is { Identifier: { } styleId })
{
if (workshopItem.IsSubscribed && styleId != minusButton)
@@ -380,6 +384,8 @@ namespace Barotrauma.Steam
new RectTransform((1.22f, 1.22f), subscribeButtonSprite.RectTransform, Anchor.Center),
onDraw: (spriteBatch, component) =>
{
if (!SteamManager.IsInitialized) { return; }
bool visible = workshopItem.IsSubscribed
&& (workshopItem.IsDownloading
|| workshopItem.IsDownloadPending
@@ -407,6 +413,8 @@ namespace Barotrauma.Steam
},
onUpdate: (deltaTime, component) =>
{
if (!SteamManager.IsInitialized) { return; }
displayedDownloadAmount = Math.Min(
workshopItem.DownloadAmount,
MathHelper.Lerp(displayedDownloadAmount, workshopItem.DownloadAmount, 0.05f));
@@ -450,7 +458,7 @@ namespace Barotrauma.Steam
var title = new GUITextBlock(
new RectTransform(Vector2.One, itemLayout.RectTransform),
workshopItem.Title, font: GUIStyle.Font)
workshopItem.Title ?? "", font: GUIStyle.Font)
{
CanBeFocused = false
};
@@ -570,7 +578,7 @@ namespace Barotrauma.Steam
var titleAndAuthorLayout = new GUILayoutGroup(new RectTransform(Vector2.One, headerLayout.RectTransform));
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);
var author = workshopItem.Owner;
@@ -682,9 +690,9 @@ namespace Barotrauma.Steam
TaskPool.Add($"Request username for {author.Id}", author.RequestInfoAsync(), (t) =>
{
authorButton.Text = author.Name;
authorButton.Text = author.Name ?? "";
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);
});
@@ -769,7 +777,7 @@ namespace Barotrauma.Steam
var tagsLabel = new GUITextBlock(new RectTransform((1.0f, 0.12f), statsVertical0.RectTransform),
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
var descriptionListBox = new GUIListBox(new RectTransform((1.0f, 0.38f), verticalLayout.RectTransform));

View File

@@ -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;
}
}
}

View File

@@ -142,11 +142,17 @@ namespace Barotrauma
GameMain.Instance.GraphicsDevice.SetRenderTarget(rt);
GameMain.Instance.GraphicsDevice.Clear(Color.Transparent);
spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, null, null, null, null, transform);
Submarine.Draw(spriteBatch);
Submarine.DrawFront(spriteBatch);
Submarine.DrawDamageable(spriteBatch, null);
spriteBatch.End();
DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => e is Structure s && (e.SpriteDepth >= 0.9f || s.Prefab.BackgroundSprite != null)));
DrawBatch(() => Submarine.DrawBack(spriteBatch, true, e => (e is not Structure || e.SpriteDepth < 0.9f)));
DrawBatch(() => Submarine.DrawDamageable(spriteBatch, null, editing: true));
DrawBatch(() => Submarine.DrawFront(spriteBatch, editing: true));
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.Viewport = prevViewport;

View File

@@ -11,15 +11,14 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.0.9.0</Version>
<Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations>
<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>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<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 Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -11,16 +11,14 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.0.9.0</Version>
<Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations>
<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>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<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 Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -11,7 +11,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.0.9.0</Version>
<Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
@@ -19,9 +19,7 @@
<Configurations>Debug;Release;Unstable</Configurations>
<InvariantGlobalization>true</InvariantGlobalization>
<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>
<DocumentationFile>Doc\BuildDocClient.xml</DocumentationFile>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<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 Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

View File

@@ -11,14 +11,14 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.0.9.0</Version>
<Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations>
<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>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>

View File

@@ -11,14 +11,14 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.0.9.0</Version>
<Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations>
<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>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>

View File

@@ -14,6 +14,15 @@ namespace Barotrauma
{
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
{
/// <summary>
@@ -609,12 +618,12 @@ namespace Barotrauma
NewMessage("Valid ranks are:", Color.White);
foreach (PermissionPreset permissionPreset in PermissionPreset.List)
{
NewMessage(" - " + permissionPreset.Name, Color.White);
NewMessage(" - " + permissionPreset.DisplayName, Color.White);
}
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)
{
ThrowError("Rank \"" + rank + "\" not found.");
@@ -623,7 +632,7 @@ namespace Barotrauma
client.SetPermissions(preset.Permissions, preset.PermittedCommands);
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);
});
@@ -2028,6 +2037,7 @@ namespace Barotrauma
"freecam",
(Client client, Vector2 cursorWorldPos, string[] args) =>
{
client.UsingFreeCam = true;
GameMain.Server.SetClientCharacter(client, null);
client.SpectateOnly = true;
}
@@ -2141,7 +2151,7 @@ namespace Barotrauma
}
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)
{
GameMain.Server.SendConsoleMessage("Rank \"" + rank + "\" not found.", senderClient, Color.Red);
@@ -2150,8 +2160,8 @@ namespace Barotrauma
client.SetPermissions(preset.Permissions, preset.PermittedCommands);
GameMain.Server.UpdateClientPermissions(client);
GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.Name}\" to {client.Name}.", senderClient);
NewMessage(senderClient.Name + " granted the rank \"" + preset.Name + "\" to " + client.Name + ".", Color.White);
GameMain.Server.SendConsoleMessage($"Assigned the rank \"{preset.DisplayName}\" to {client.Name}.", senderClient);
NewMessage(senderClient.Name + " granted the rank \"" + preset.DisplayName + "\" to " + client.Name + ".", Color.White);
}
);
@@ -2545,26 +2555,37 @@ namespace Barotrauma
foreach (Item item in Item.ItemList)
{
item.TryCreateServerEventSpam();
item.CreateStatusEvent();
item.CreateStatusEvent(loadingRound: false);
}
foreach (Structure wall in Structure.WallList)
{
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;
if (args.Length > 0)
{
float.TryParse(args[0], out seconds);
}
GameMain.Server.FileSender.StallPacketsTime = seconds;
GameMain.Server.FileSender.ForceMinimumFileTransferDuration = seconds;
NewMessage("Set file transfer stall time to " + seconds);
}));
#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)
{
if (GameMain.Server == null) return;

View File

@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using Barotrauma.Extensions;
using Barotrauma.Networking;
@@ -11,25 +12,16 @@ namespace Barotrauma
{
internal partial class MedicalClinic
{
private enum RateLimitResult
{
OK,
LimitReached
}
private struct RateLimitInfo
{
public int Requests;
public const int MaxRequests = 10;
public DateTimeOffset Expiry;
}
// allow 20 requests per 5 seconds, announce to chat if the limit is reached
private readonly RateLimiter rateLimiter = new(
maxRequests: RateLimitMaxRequests,
expiryInSeconds: RateLimitExpiry,
punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce));
private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry);
private readonly List<AfflictionSubscriber> afflictionSubscribers = new();
private readonly Dictionary<Client, RateLimitInfo> rateLimits = new();
public void ServerRead(IReadMessage inc, Client sender)
{
NetworkHeader header = (NetworkHeader)inc.ReadByte();
@@ -65,7 +57,7 @@ namespace Barotrauma
private void ProcessNewAddition(IReadMessage inc, Client client)
{
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
if (rateLimiter.IsLimitReached(client)) { return; }
NetCrewMember newCrewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
InsertPendingCrewMember(newCrewMember);
@@ -74,7 +66,7 @@ namespace Barotrauma
private void ProcessAddEverything(Client client)
{
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
if (rateLimiter.IsLimitReached(client)) { return; }
AddEverythingToPending();
ServerSend(PendingHeals.ToNetCollection(), NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable, reponseClient: client);
}
@@ -92,7 +84,7 @@ namespace Barotrauma
private void ProcessNewRemoval(IReadMessage inc, Client client)
{
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
if (rateLimiter.IsLimitReached(client)) { return; }
NetRemovedAffliction removed = INetSerializableStruct.Read<NetRemovedAffliction>(inc);
RemovePendingAffliction(removed.CrewMember, removed.Affliction);
@@ -101,14 +93,14 @@ namespace Barotrauma
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);
}
private void ProcessHealing(Client client)
{
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
if (rateLimiter.IsLimitReached(client)) { return; }
HealRequestResult result = HealAllPending(client: client);
ServerSend(new NetHealRequest { Result = result }, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable, reponseClient: client);
@@ -116,7 +108,7 @@ namespace Barotrauma
private void ProcessClearing(Client client)
{
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
if (rateLimiter.IsLimitReached(client)) { return; }
if (!PendingHeals.Any()) { return; }
@@ -126,7 +118,7 @@ namespace Barotrauma
private void ProcessRequestedAfflictions(IReadMessage inc, Client client)
{
if (CheckRateLimit(client) == RateLimitResult.LimitReached) { return; }
if (rateLimiter.IsLimitReached(client)) { return; }
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
@@ -135,6 +127,17 @@ namespace Barotrauma
ImmutableArray<NetAffliction> pendingAfflictions = ImmutableArray<NetAffliction>.Empty;
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 })
{
pendingAfflictions = GetAllAfflictions(health);
@@ -158,32 +161,6 @@ namespace Barotrauma
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()
{
IWriteMessage msg = new WriteOnlyMessage();

View File

@@ -4,6 +4,7 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma
{
@@ -135,6 +136,8 @@ namespace Barotrauma
}
}
EnsureItemsInBothHands(c.Character);
CreateNetworkEvent();
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)
{
SharedWrite(msg, extraData);

View File

@@ -65,7 +65,8 @@ namespace Barotrauma
msg.WriteUInt16(GameMain.Server.EntityEventManager.Events.Last()?.ID ?? (ushort)0);
itemContainer.Inventory.ServerEventWrite(msg, c);
break;
case ItemStatusEventData _:
case ItemStatusEventData statusEvent:
msg.WriteBoolean(statusEvent.LoadingRound);
msg.WriteSingle(condition);
break;
case AssignCampaignInteractionEventData _:

View File

@@ -103,9 +103,9 @@ namespace Barotrauma.Networking
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 && !GameMain.LuaCs.Game.disableSpamFilter)
if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt)
{
GameMain.Server.KarmaManager.OnSpamFilterTriggered(c);
@@ -126,7 +126,7 @@ namespace Barotrauma.Networking
c.ChatSpamSpeed += similarity + 0.5f;
if (c.ChatSpamTimer > 0.0f && !isOwner && !GameMain.LuaCs.Game.disableSpamFilter)
if (c.ChatSpamTimer > 0.0f && !isSpamExempt)
{
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null);
c.ChatSpamTimer = 10.0f;

View File

@@ -108,9 +108,7 @@ namespace Barotrauma.Networking
const int MaxTransferCount = 16;
const int MaxTransferCountPerRecipient = 5;
public static TimeSpan MaxTransferDuration = new TimeSpan(0, 2, 0);
public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver);
public FileTransferDelegate OnStarted;
public FileTransferDelegate OnEnded;
@@ -121,8 +119,9 @@ namespace Barotrauma.Networking
private readonly ServerPeer peer;
public static DateTime StartTime;
#if DEBUG
public float StallPacketsTime { get; set; }
public float ForceMinimumFileTransferDuration { get; set; }
#endif
public IReadOnlyList<FileTransferOut> ActiveTransfers => activeTransfers;
@@ -172,6 +171,8 @@ namespace Barotrauma.Networking
return null;
}
StartTime = DateTime.Now;
OnStarted(transfer);
GameMain.Server.LastClientListUpdateID++;
@@ -259,7 +260,18 @@ namespace Barotrauma.Networking
for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++)
{
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.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER);
@@ -293,11 +305,10 @@ namespace Barotrauma.Networking
//this gets reset when packet loss or disorder sets in
transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate,
transfer.PacketsPerUpdate + 0.05f);
}
#if DEBUG
transfer.WaitTimer = Math.Max(transfer.WaitTimer, StallPacketsTime);
if (stalling) { break; }
#endif
}
}
catch (Exception e)
@@ -330,7 +341,7 @@ namespace Barotrauma.Networking
{
byte transferId = inc.ReadByte();
var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId);
if (matchingTransfer != null) CancelTransfer(matchingTransfer);
if (matchingTransfer != null) { CancelTransfer(matchingTransfer); }
return;
}
else if (messageType == FileTransferMessageType.Data)
@@ -359,6 +370,7 @@ namespace Barotrauma.Networking
if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length)
{
matchingTransfer.Status = FileTransferStatus.Finished;
DebugConsole.Log($"Finished sending file \"{matchingTransfer.FilePath}\" to \"{client.Name}\". Took {DateTime.Now - StartTime}");
}
}
return;

View File

@@ -310,7 +310,7 @@ namespace Barotrauma.Networking
}
else
{
var defaultPerms = PermissionPreset.List.Find(p => p.Name == "None");
var defaultPerms = PermissionPreset.List.Find(p => p.Identifier == "None");
if (defaultPerms != null)
{
newClient.SetPermissions(defaultPerms.Permissions, defaultPerms.PermittedCommands);
@@ -339,9 +339,8 @@ namespace Barotrauma.Networking
public void Update(float deltaTime)
{
#if CLIENT
if (ShowNetStats) { netStats.Update(deltaTime); }
#endif
dosProtection.Update(deltaTime);
if (!started) { return; }
if (ChildServerRelay.HasShutDown)
@@ -397,10 +396,10 @@ namespace Barotrauma.Networking
Voting.Update(deltaTime);
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;
if (Submarine.MainSub != null && !(GameMain.GameSession.GameMode is PvPMode))
if (Submarine.MainSub != null && GameMain.GameSession.GameMode is not PvPMode)
{
if (Level.Loaded?.EndOutpost != null)
{
@@ -702,10 +701,14 @@ namespace Barotrauma.Networking
}
}
private readonly DoSProtection dosProtection = new();
private void ReadDataMessage(NetworkConnection sender, IReadMessage inc)
{
var connectedClient = connectedClients.Find(c => c.Connection == sender);
using var _ = dosProtection.Start(connectedClient);
ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte();
GameMain.LuaCs.Networking.NetMessageReceived(inc, header, connectedClient);
@@ -785,9 +788,12 @@ namespace Barotrauma.Networking
string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName);
if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
{
ServerSettings.CampaignSettings = settings;
ServerSettings.SaveSettings();
MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings);
using (dosProtection.Pause(connectedClient))
{
ServerSettings.CampaignSettings = settings;
ServerSettings.SaveSettings();
MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings);
}
}
}
}
@@ -797,11 +803,14 @@ namespace Barotrauma.Networking
if (GameStarted)
{
SendDirectChatMessage(TextManager.Get("CampaignStartFailedRoundRunning").Value, connectedClient, ChatMessageType.MessageBox);
return;
break;
}
if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
{
MultiPlayerCampaign.LoadCampaign(saveName);
{
using (dosProtection.Pause(connectedClient))
{
MultiPlayerCampaign.LoadCampaign(saveName);
}
}
}
break;
@@ -1098,7 +1107,7 @@ namespace Barotrauma.Networking
ChatMessage.ServerRead(inc, c);
break;
case ClientNetSegment.Vote:
Voting.ServerRead(inc, c);
Voting.ServerRead(inc, c, dosProtection);
break;
default:
return SegmentTableReader<ClientNetSegment>.BreakSegmentReading.Yes;
@@ -1259,7 +1268,7 @@ namespace Barotrauma.Networking
entityEventManager.Read(inc, c);
break;
case ClientNetSegment.Vote:
Voting.ServerRead(inc, c);
Voting.ServerRead(inc, c, dosProtection);
break;
case ClientNetSegment.SpectatingPos:
c.SpectatePos = new Vector2(inc.ReadSingle(), inc.ReadSingle());
@@ -1419,19 +1428,23 @@ namespace Barotrauma.Networking
bool quitCampaign = inc.ReadBoolean();
if (GameStarted)
{
Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage);
if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
using (dosProtection.Pause(sender))
{
mpCampaign.SavePlayers();
GameMain.GameSession.SubmarineInfo = new SubmarineInfo(GameMain.GameSession.Submarine);
mpCampaign.UpdateStoreStock();
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
Log($"Client \"{ClientLogName(sender)}\" ended the round.", ServerLog.MessageType.ServerMessage);
if (mpCampaign != null && Level.IsLoadedFriendlyOutpost && save)
{
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)
{
@@ -1455,45 +1468,54 @@ namespace Barotrauma.Networking
}
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)
{
Log("Client \"" + ClientLogName(sender) + "\" started the round.", ServerLog.MessageType.ServerMessage);
TryStartGame();
using (dosProtection.Pause(sender))
{
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)))
{
var availableTransition = mpCampaign.GetAvailableTransition(out _, out _);
//don't force location if we've teleported
bool forceLocation = !mpCampaign.Map.AllowDebugTeleport || mpCampaign.Map.CurrentLocation == Level.Loaded.StartLocation;
switch (availableTransition)
using (dosProtection.Pause(sender))
{
case CampaignMode.TransitionType.ReturnToPreviousEmptyLocation:
if (forceLocation)
{
mpCampaign.Map.SelectLocation(
mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation));
}
mpCampaign.LoadNewLevel();
break;
case CampaignMode.TransitionType.ProgressToNextEmptyLocation:
if (forceLocation)
{
mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation));
}
mpCampaign.LoadNewLevel();
break;
case CampaignMode.TransitionType.None:
#if DEBUG || UNSTABLE
DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available.");
#endif
return;
default:
Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage);
mpCampaign.LoadNewLevel();
break;
var availableTransition = mpCampaign.GetAvailableTransition(out _, out _);
//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:
if (forceLocation)
{
mpCampaign.Map.SelectLocation(
mpCampaign.Map.CurrentLocation.Connections.Find(c => c.LevelData == Level.Loaded?.LevelData).OtherLocation(mpCampaign.Map.CurrentLocation));
}
mpCampaign.LoadNewLevel();
break;
case CampaignMode.TransitionType.ProgressToNextEmptyLocation:
if (forceLocation)
{
mpCampaign.Map.SetLocation(mpCampaign.Map.Locations.IndexOf(Level.Loaded.EndLocation));
}
mpCampaign.LoadNewLevel();
break;
case CampaignMode.TransitionType.None:
#if DEBUG || UNSTABLE
DebugConsole.ThrowError($"Client \"{sender.Name}\" attempted to trigger a level transition. No transitions available.");
#endif
break;
default:
Log("Client \"" + ClientLogName(sender) + "\" ended the round.", ServerLog.MessageType.ServerMessage);
mpCampaign.LoadNewLevel();
break;
}
}
}
}
@@ -1533,11 +1555,7 @@ namespace Barotrauma.Networking
mpCampaign?.ServerRead(inc, sender);
break;
case ClientPermissions.ConsoleCommands:
{
string consoleCommand = inc.ReadString();
Vector2 clientCursorPos = new Vector2(inc.ReadSingle(), inc.ReadSingle());
DebugConsole.ExecuteClientCommand(sender, clientCursorPos, consoleCommand);
}
DebugConsole.ServerRead(inc, sender);
break;
case ClientPermissions.ManagePermissions:
byte targetClientID = inc.ReadByte();
@@ -2609,16 +2627,20 @@ namespace Barotrauma.Networking
{
if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; }
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++)
using (dosProtection.Pause(client))
{
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;
}
@@ -3641,15 +3663,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)
{
sender.SpectateOnly = message.ReadBoolean() && (ServerSettings.AllowSpectating || sender.Connection == OwnerConnection);
if (sender.SpectateOnly)
{
return;
}
bool spectateOnly = message.ReadBoolean();
message.ReadPadBits();
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))
{
newName = sender.Name;
@@ -3667,42 +3702,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.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;
}
}

View File

@@ -24,11 +24,28 @@ namespace Barotrauma.Networking
}
protected readonly Callbacks callbacks;
private readonly ImmutableArray<ContentPackage> contentPackages;
protected ServerPeer(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();
@@ -255,9 +272,7 @@ namespace Barotrauma.Networking
structToSend = new ServerPeerContentPackageOrderPacket
{
ServerName = GameMain.Server.ServerName,
ContentPackages = ContentPackageManager.EnabledPackages.All
.Where(cp => cp.Files.Any())
.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile))
ContentPackages = contentPackages
.Select(contentPackage => new ServerContentPackage(contentPackage, timeNow))
.ToImmutableArray()
};

View File

@@ -537,7 +537,7 @@ namespace Barotrauma.Networking
else
{
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)
{
DebugConsole.ThrowError("Failed to restore saved permissions to the client \"" + clientName + "\". Permission preset \"" + presetName + "\" not found.");
@@ -602,8 +602,7 @@ namespace Barotrauma.Networking
foreach (SavedClientPermission clientPermission in ClientPermissions)
{
var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands));
#warning TODO: this is broken because of localization
if (matchingPreset != null && matchingPreset.Name == "None")
if (matchingPreset != null && matchingPreset.Identifier == "None")
{
continue;
}
@@ -617,7 +616,7 @@ namespace Barotrauma.Networking
clientElement.Add(matchingPreset == null
? new XAttribute("permissions", clientPermission.Permissions.ToString())
: new XAttribute("preset", matchingPreset.Name));
: new XAttribute("preset", matchingPreset.DisplayName));
if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands))
{

View File

@@ -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; }
@@ -337,7 +337,10 @@ namespace Barotrauma
inc.ReadPadBits();
GameMain.Server.UpdateVoteStatus();
using (dosProtection.Pause(sender))
{
GameMain.Server.UpdateVoteStatus();
}
}
public void ServerWrite(IWriteMessage msg)

View File

@@ -162,7 +162,10 @@ namespace Barotrauma
sb.AppendLine("Language: " + GameSettings.CurrentConfig.Language);
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("Loaded submarine: " + ((Submarine.MainSub == null) ? "None" : Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash + ")"));

View File

@@ -5,12 +5,13 @@ namespace Barotrauma.Steam
{
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)
{
IsInitialized = true;
Steamworks.SteamServerInit options = new Steamworks.SteamServerInit("Barotrauma", "Barotrauma")
{
GamePort = (ushort)server.Port,
@@ -57,10 +58,15 @@ namespace Barotrauma.Steam
Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText);
Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString());
Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString());
Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation)));
Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp
=> cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : "")));
int index = 0;
foreach (var contentPackage in contentPackages)
{
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("subselectionmode", server.ServerSettings.SubSelectionMode.ToString());
Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString());

View File

@@ -20,19 +20,6 @@ namespace Barotrauma
}
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)
{

View File

@@ -231,11 +231,6 @@ namespace Barotrauma
{
pendingMessages.Add(traitor, new List<string>());
}
foreach (var traitor in Traitors.Values)
{
traitor.Greet(server, CodeWords, CodeResponse, message => pendingMessages[traitor].Add(message));
GameMain.LuaCs.Hook.Call("traitor.traitorAssigned", new object[] { traitor });
}
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier)));
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier)));

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -11,14 +11,14 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.0.9.0</Version>
<Version>1.0.13.1</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
<Configurations>Debug;Release;Unstable</Configurations>
<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>
<DocumentationFile>Doc\BuildDocServer.xml</DocumentationFile>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>

View File

@@ -8,7 +8,7 @@
<Preset
name="Moderator"
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="readycheck"/>
<Command name="autorestart"/>

View File

@@ -107,12 +107,26 @@ namespace Barotrauma
}
}
public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) =>
steeringManager is IndoorsSteeringManager pathSteering &&
pathSteering.CurrentPath != null &&
(!requireUnfinished || !pathSteering.CurrentPath.Finished) &&
!pathSteering.CurrentPath.Unreachable &&
(!requireNonDirty || !pathSteering.IsPathDirty);
/// <summary>
/// Is the current path valid, using the provided parameters.
/// </summary>
/// <param name="requireNonDirty"></param>
/// <param name="requireUnfinished"></param>
/// <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 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>();
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>();
if (pickable == null) { return false; }
@@ -265,23 +279,28 @@ namespace Barotrauma
}
else
{
var holdable = item.GetComponent<Holdable>();
if (holdable != null)
{
pickable = holdable;
}
// Not allowed to wear -> don't use the Wearable component even when it's found.
pickable = item.GetComponent<Holdable>();
}
if (item.ParentInventory is ItemInventory itemInventory)
{
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
}
if (equip)
if (equip && pickable != null)
{
int targetSlot = -1;
//check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots)
{
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++)
{
if (targetInventory is CharacterInventory characterInventory)
@@ -294,7 +313,7 @@ namespace Barotrauma
var otherItem = targetInventory.GetItemAt(i);
if (otherItem == null) { continue; }
//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)
{
@@ -304,6 +323,11 @@ namespace Barotrauma
}
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
otherItem.Drop(Character);
}
@@ -314,7 +338,7 @@ namespace Barotrauma
}
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 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; }
continue;
@@ -449,9 +473,10 @@ namespace Barotrauma
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared();
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
Character.ReleaseSecondaryItem();
SteeringManager.Reset();
pathSteering?.ResetPath();
Vector2 dir = Vector2.Normalize(diff);

View File

@@ -480,7 +480,7 @@ namespace Barotrauma
if (SelectedAiTarget?.Entity != null || EscapeTarget != null)
{
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;
}
else
@@ -3934,7 +3934,7 @@ namespace Barotrauma
{
SteerAwayFromTheEnemy();
}
else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true))
else if (canAttackDoors && HasValidPath())
{
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))

View File

@@ -1,16 +1,16 @@
using Barotrauma.Networking;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
namespace Barotrauma
{
partial class HumanAIController : AIController
{
public static bool debugai;
public static bool DebugAI;
public static bool DisableCrewAI;
private readonly AIObjectiveManager objectiveManager;
@@ -55,6 +55,7 @@ namespace Barotrauma
private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
private float obstacleRaycastTimer;
private bool isBlocked;
private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 800;
@@ -92,7 +93,10 @@ namespace Barotrauma
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 HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
@@ -225,14 +229,15 @@ namespace Barotrauma
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;
if (target == null)
{
return false;
}
if (useTargetSub)
if (targetSub)
{
if (target.Submarine is Submarine sub)
{
@@ -244,62 +249,71 @@ namespace Barotrauma
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();
if (Character.Submarine == null)
bool isOutside = Character.Submarine == null;
if (isOutside)
{
obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0)
{
bool hasValidPath = HasValidPath();
isBlocked = false;
UseOutsideWaypoints = false;
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.
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity;
if (spatialTarget == null)
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
Vector2 rayEnd = spatialTarget.SimPosition;
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
{
var gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>();
spatialTarget = gotoObjective?.Target;
rayEnd += targetSub.SimPosition;
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
{
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
Vector2 rayEnd = spatialTarget.SimPosition;
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
bool isUsingOutsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine == null && n.Ruin == null);
if (isUsingOutsideWaypoints)
{
rayEnd += targetSub.SimPosition;
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
resetPath = true;
}
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;
if (hasValidPath)
obstacleRaycastTimer = obstacleRaycastIntervalShort;
// 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;
// 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())
if (connectedSub == Submarine.MainSub) { continue; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
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; }
Vector2 rayStart = SimPosition - connectedSub.SimPosition;
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;
}
PathSteering.CurrentPath.Unreachable = true;
break;
}
}
}
@@ -307,10 +321,11 @@ namespace Barotrauma
}
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.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -352,12 +367,13 @@ namespace Barotrauma
}
}
}
if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer))
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
if (useInsideSteering)
{
if (steeringManager != insideSteering)
{
insideSteering.Reset();
PathSteering.ResetPath();
steeringManager = insideSteering;
}
if (IsCloseEnoughToTarget(maxSteeringBuffer))
@@ -395,6 +411,8 @@ namespace Barotrauma
}
objectiveManager.UpdateObjectives(deltaTime);
UpdateDragged(deltaTime);
if (reportProblemsTimer > 0)
{
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)
{
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;
}
UpdateSpeaking();
SpeakAboutIssues();
UnequipUnnecessaryItems();
reactTimer = GetReactionTime();
}
@@ -904,17 +931,67 @@ namespace Barotrauma
}
}))
{
suitableContainer = targetContainer;
return true;
if (targetContainer != null &&
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;
}
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()
{
Order newOrder = 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)
{
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
@@ -1013,25 +1090,21 @@ namespace Barotrauma
}
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.IsEscorted)
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
{
if (Character.TeamID == CharacterTeamType.FriendlyNPC)
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
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))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
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))
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
#if SERVER
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character));
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character));
#endif
}
}
}
}
@@ -1062,21 +1135,33 @@ namespace Barotrauma
}
}
private void UpdateSpeaking()
private void SpeakAboutIssues()
{
if (!Character.IsOnPlayerTeam) { 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;
if (isAccidental)
{
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold)
if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold))
{
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
}
@@ -1371,7 +1456,7 @@ namespace Barotrauma
}
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.
cumulativeDamage *= 2;
@@ -1556,7 +1641,7 @@ namespace Barotrauma
hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{
needsSuit = !Character.IsProtectedFromPressure;
needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
return needsAir || needsSuit;
}
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)
{
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; }
@@ -1812,7 +1897,7 @@ namespace Barotrauma
/// </summary>
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)
@@ -1885,7 +1970,7 @@ namespace Barotrauma
private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{
bool targetAdded = false;
DoForEachCrewMember(caller, humanAI =>
DoForEachBot(caller, humanAI =>
{
if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
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>
{
DoForEachCrewMember(caller, humanAI =>
DoForEachBot(caller, humanAI =>
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
}
@@ -2024,6 +2109,10 @@ namespace Barotrauma
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))
{
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)
{
if (hull == null)
{
return CalculateHullSafety(hull, visibleHulls, character, ignoreWater, ignoreOxygen, ignoreFire, ignoreEnemies);
}
HullSafety hullSafety;
if (character.AIController is HumanAIController controller)
{
@@ -2069,103 +2162,137 @@ namespace Barotrauma
if (other.IsPet)
{
// 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
{
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))
{
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
Identifier npcFaction = npc.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
if (npcFaction.IsEmpty)
//NPCs that allow some campaign interaction are not turned hostile by low reputation
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
npcFaction = currentLocationFaction;
}
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{
return false;
}
return !npcAI.IsInHostileFaction();
}
}
}
return true;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
public bool IsInHostileFaction()
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; }
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 (!predicate(c.AIController as HumanAIController))
{
return false;
}
}
//if faction identifier is not specified, assume the NPC is a member of the faction that owns the outpost
npcFaction = currentLocationFaction;
}
return true;
}
public static bool IsTrueForAnyCrewMember(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
if (FilterCrewMember(character, c))
if (campaign.CurrentLocation is { IsFactionHostile: true })
{
if (predicate(c.AIController as HumanAIController))
{
return true;
}
return true;
}
}
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; }
int count = 0;
foreach (var other in Character.CharacterList)
{
if (onlyActive && !IsActive(other))
if (!IsBotInTheCrew(character, other)) { continue; }
if (predicate == null || predicate(other.AIController as HumanAIController))
{
continue;
}
if (onlyBots && other.IsPlayer)
{
continue;
}
if (FilterCrewMember(character, other))
{
if (predicate == null || predicate(other.AIController as HumanAIController))
{
count++;
}
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; }
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);
}
@@ -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)
{
@@ -2230,10 +2357,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToOperateThis(Character.AIController);
foreach (Character c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c == Character) { continue; }
if (c.Removed) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
if (c.IsPlayer)
{
if (c.SelectedItem == target.Item)
@@ -2301,9 +2427,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
foreach (var c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c == Character) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
other = c;
if (c.IsPlayer)
{
@@ -2317,7 +2443,7 @@ namespace Barotrauma
{
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
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.
continue;
@@ -2352,11 +2478,10 @@ namespace Barotrauma
}
#region Wrappers
public bool IsFriendly(Character other) => IsFriendly(Character, other);
public void DoForEachCrewMember(Action<HumanAIController> action) => DoForEachCrewMember(Character, action);
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate);
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate);
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam);
public bool IsTrueForAnyBotInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAnyBotInTheCrew(Character, predicate);
public bool IsTrueForAllBotsInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAllBotsInTheCrew(Character, predicate);
public int CountBotsInTheCrew(Func<HumanAIController, bool> predicate = null) => CountBotsInTheCrew(Character, predicate);
#endregion
}
}

View File

@@ -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)
{
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)
{
Vector2 targetDiff = target - currentTarget;
@@ -205,6 +205,24 @@ namespace Barotrauma
if (needsNewPath || findPathTimer < -1.0f)
{
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)
{
SkipCurrentPathNodes();
@@ -213,7 +231,7 @@ namespace Barotrauma
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
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)
{
// 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)
{
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
if (currentLadder == null && nextLadder != null)
{
//climbing ladders -> don't move horizontally
diff.X = 0.0f;
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
NextNode(!doorsChecked);
}
//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)
else
{
float heightFromFloor = character.AnimController.GetHeightFromFloor();
// 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))
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent;
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.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)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
float heightFromFloor = character.AnimController.GetHeightFromFloor();
// 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);
}
}
else if (nextLadder != null)
{
//if the current node is below the character and the next one is above (or vice versa)
//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);
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)
//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
NextNode(!doorsChecked);
}
}
}
return ConvertUnits.ToSimUnits(diff);
@@ -486,7 +512,7 @@ namespace Barotrauma
}
}
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);
}

View File

@@ -122,7 +122,7 @@ namespace Barotrauma
foreach (Affliction affliction in afflictions)
{
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);
}

View File

@@ -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 (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any))
if (item == null) { return false; }
bool canEquip = false;
if (item.AllowedSlots.Contains(InvSlotType.Any))
{
if (character.Inventory.IsAnySlotAvailable(item))
{
canEquip = true;
}
}
if (!canEquip)
{
canEquip = false;
var inv = character.Inventory;
foreach (var allowedSlot in item.AllowedSlots)
{
if (!allowWearing)
{
if (!allowedSlot.HasFlag(InvSlotType.RightHand) && !allowedSlot.HasFlag(InvSlotType.LeftHand))
{
continue;
}
}
foreach (var slotType in inv.SlotTypes)
{
if (!allowedSlot.HasFlag(slotType)) { continue; }
@@ -530,18 +544,9 @@ namespace Barotrauma
}
}
}
return canEquip;
}
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;
return canEquip && character.Inventory.CanBePut(item);
}
protected bool CanEquip(Item item) => CanEquip(character, item);
protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing);
}
}

View File

@@ -64,7 +64,6 @@ namespace Barotrauma
if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{
itemIndex = 0;
if (suitableContainer != null)
{
bool equip = item.GetComponent<Holdable>() != null ||
@@ -112,10 +111,7 @@ namespace Barotrauma
Abandon = true;
}
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
protected override bool CheckObjectiveSpecific()

View File

@@ -121,7 +121,7 @@ namespace Barotrauma
{
return true;
}
return CanEquip(character, item);
return CanEquip(character, item, allowWearing: false);
}
public override void OnDeselected()

View File

@@ -170,13 +170,33 @@ namespace Barotrauma
return Priority;
}
}
float damageFactor = MathUtils.InverseLerp(0.0f, 5.0f, character.GetDamageDoneByAttacker(Enemy) / 100.0f);
Priority = TargetEliminated ? 0 : Math.Min((95 + damageFactor) * PriorityModifier, 100);
if (Priority > 0)
if (TargetEliminated)
{
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;
@@ -312,12 +332,10 @@ namespace Barotrauma
}
else
{
AskHelp();
Retreat(deltaTime);
}
break;
case CombatMode.Retreat:
AskHelp();
Retreat(deltaTime);
break;
default:
@@ -352,7 +370,7 @@ namespace Barotrauma
Weapon = null;
continue;
}
if (WeaponComponent.IsNotEmpty(character))
if (!WeaponComponent.IsEmpty(character))
{
// All good, the weapon is loaded
break;
@@ -470,7 +488,7 @@ namespace Barotrauma
// Not in the inventory anymore or cannot find the weapon component
return false;
}
if (!WeaponComponent.IsNotEmpty(character))
if (WeaponComponent.IsEmpty(character))
{
// Try reloading (and seek ammo)
if (!Reload(seekAmmo))
@@ -541,7 +559,7 @@ namespace Barotrauma
priority /= 2;
}
}
if (!weapon.IsNotEmpty(character))
if (weapon.IsEmpty(character))
{
if (weapon is RangedWeapon && !isAllowedToSeekWeapons)
{
@@ -554,7 +572,6 @@ namespace Barotrauma
priority /= 2;
}
}
if (Enemy.Params.Health.StunImmunity)
{
if (weapon.Item.HasTag("stunner"))
@@ -750,7 +767,7 @@ namespace Barotrauma
private bool Equip()
{
if (character.LockHands) { return false; }
if (!WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
if (WeaponComponent.IsEmpty(character))
{
return false;
}
@@ -783,6 +800,10 @@ namespace Barotrauma
private void Retreat(float deltaTime)
{
if (!Enemy.IsHuman)
{
SpeakRetreating();
}
RemoveFollowTarget();
RemoveSubObjective(ref seekAmmunitionObjective);
if (retreatObjective != null && retreatObjective.Target != retreatTarget)
@@ -793,6 +814,7 @@ namespace Barotrauma
{
// Swim away
SteeringManager.Reset();
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(character.WorldPosition - Enemy.WorldPosition));
SteeringManager.SteeringAvoid(deltaTime, 5, weight: 2);
return;
@@ -819,7 +841,8 @@ namespace Barotrauma
{
TryAddSubObjective(ref retreatObjective, () => new AIObjectiveGoTo(retreatTarget, character, objectiveManager)
{
UsePathingOutside = false
UsePathingOutside = false,
SpeakIfFails = false
},
onAbandon: () =>
{
@@ -861,6 +884,7 @@ namespace Barotrauma
{
if (sqrDistance > MathUtils.Pow2(meleeWeapon.Range))
{
character.ReleaseSecondaryItem();
// Swim towards the target
SteeringManager.Reset();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Enemy), weight: 10);
@@ -882,7 +906,8 @@ namespace Barotrauma
UsePathingOutside = false,
IgnoreIfTargetDead = true,
TargetName = Enemy.DisplayName,
AlwaysUseEuclideanDistance = false
AlwaysUseEuclideanDistance = false,
SpeakIfFails = false
},
onAbandon: () =>
{
@@ -966,7 +991,7 @@ namespace Barotrauma
item.GetComponent<RangedWeapon>() != null)
{
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; }
// Eject empty ammo
HumanAIController.UnequipEmptyItems(Weapon);
RelatedItem item = null;
Item ammunition = null;
ImmutableHashSet<Identifier> ammunitionIdentifiers = null;
if (WeaponComponent.requiredItems.ContainsKey(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 (ammunition != null)
{
// Ammunition still remaining
return true;
}
item = requiredItem;
if (Weapon.OwnInventory.AllItems.Any(it => it.Condition > 0 && requiredItem.MatchesItem(it))) { continue; }
ammunitionIdentifiers = requiredItem.Identifiers;
break;
}
}
else if (WeaponComponent is MeleeWeapon meleeWeapon)
{
ammunitionIdentifiers = meleeWeapon.PreferredContainedItems;
}
// 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
static bool IsInsideHeadset(Item i) => i.ParentInventory?.Owner is Item ownerItem && ownerItem.HasTag("mobileradio");
ammunition = character.Inventory.FindItem(i => CheckItemIdentifiersOrTags(i, ammunitionIdentifiers) && i.Condition > 0 && !IsInsideHeadset(i), recursive: true);
if (ammunition != null)
var container = Weapon.GetComponent<ItemContainer>();
if (!container.Inventory.TryPutItem(ammunition, user: character))
{
var container = Weapon.GetComponent<ItemContainer>();
if (!container.Inventory.TryPutItem(ammunition, null))
if (ammunition.ParentInventory == character.Inventory)
{
if (ammunition.ParentInventory == character.Inventory)
{
ammunition.Drop(character);
}
ammunition.Drop(character);
}
}
}
}
if (WeaponComponent.HasRequiredContainedItems(character, addMessage: false))
if (!WeaponComponent.IsEmpty(character))
{
return true;
}
else if (ammunition == null && !HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
else if (!HoldPosition && IsOffensiveOrArrest && seekAmmo && ammunitionIdentifiers != null)
{
SeekAmmunition(ammunitionIdentifiers);
}
@@ -1270,7 +1284,7 @@ namespace Barotrauma
}
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)
{

View File

@@ -109,7 +109,7 @@ namespace Barotrauma
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)
@@ -156,15 +156,15 @@ namespace Barotrauma
Inventory originalInventory = ItemToContain.ParentInventory;
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)
{
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
{
return inventory.TryPutItem(itemToContain, user: null);
return inventory.TryPutItem(itemToContain, user: character);
}
}
@@ -202,7 +202,7 @@ namespace Barotrauma
ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
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,
onCompleted: () => RemoveSubObjective(ref goToObjective));

View File

@@ -66,7 +66,7 @@ namespace Barotrauma
else
{
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;

View File

@@ -28,7 +28,7 @@ namespace Barotrauma
if (character.IsSecurity) { return 100; }
if (objectiveManager.IsOrder(this)) { return 100; }
// 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)
@@ -37,8 +37,7 @@ namespace Barotrauma
var combatObjective = new AIObjectiveCombat(character, target, combatMode, objectiveManager, PriorityModifier);
if (character.TeamID == CharacterTeamType.FriendlyNPC && target.TeamID == CharacterTeamType.Team1 && GameMain.GameSession?.GameMode is CampaignMode campaign)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
if (campaign.CurrentLocation is { IsFactionHostile: true })
{
combatObjective.holdFireCondition = () =>
{
@@ -66,7 +65,13 @@ namespace Barotrauma
if (target.CurrentHull == null) { return false; }
if (HumanAIController.IsFriendly(character, target)) { 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.IsArrested) { return false; }
if (EnemyAIController.IsLatchedToSomeoneElse(target, character)) { return false; }

View File

@@ -79,7 +79,7 @@ namespace Barotrauma
{
if (mask != targetItem)
{
character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot);
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
}
}
}

View File

@@ -52,17 +52,14 @@ namespace Barotrauma
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
objectiveManager.HasActiveObjective<AIObjectiveRescue>() ||
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
{
if ((character.IsLowInOxygen && !character.AnimController.HeadInWater && HumanAIController.HasDivingSuit(character, requireOxygenTank: false)) ||
(HumanAIController.NeedsDivingGear(character.CurrentHull, out bool needsSuit) &&
(needsSuit ?
!HumanAIController.HasDivingSuit(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)) :
!HumanAIController.HasDivingGear(character, conditionPercentage: AIObjectiveFindDivingGear.GetMinOxygen(character)))))
NeedMoreDivingGear(character.CurrentHull, AIObjectiveFindDivingGear.GetMinOxygen(character)))
{
Priority = 100;
Priority = AIObjectiveManager.MaxObjectivePriority;
}
else if ((objectiveManager.IsCurrentOrder<AIObjectiveGoTo>() || objectiveManager.IsCurrentOrder<AIObjectiveReturn>()) &&
character.Submarine != null && !character.IsOnFriendlyTeam(character.Submarine.TeamID))
@@ -75,11 +72,11 @@ namespace Barotrauma
{
Priority = 0;
}
Priority = MathHelper.Clamp(Priority, 0, 100);
Priority = MathHelper.Clamp(Priority, 0, AIObjectiveManager.MaxObjectivePriority);
if (divingGearObjective != null && !divingGearObjective.IsCompleted && divingGearObjective.CanBeCompleted)
{
// 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;
@@ -111,7 +108,7 @@ namespace Barotrauma
if (currenthullSafety > HumanAIController.HULL_SAFETY_THRESHOLD)
{
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.
Priority = 0;
@@ -122,7 +119,7 @@ namespace Barotrauma
float dangerFactor = (100 - currenthullSafety) / 100;
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; }
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);
if (!character.LockHands && (!dangerousPressure || shouldActOnSuffocation || cannotFindSafeHull))
{
@@ -200,16 +197,11 @@ namespace Barotrauma
UpdateSimpleEscape(deltaTime);
return;
}
searchHullTimer = SearchHullInterval * Rand.Range(0.9f, 1.1f);
previousSafeHull = currentSafeHull;
currentSafeHull = potentialSafeHull;
cannotFindSafeHull = currentSafeHull == null || HumanAIController.NeedsDivingGear(currentSafeHull, out _);
if (currentSafeHull == null)
{
currentSafeHull = previousSafeHull;
}
cannotFindSafeHull = currentSafeHull == null || NeedMoreDivingGear(currentSafeHull);
currentSafeHull ??= previousSafeHull;
if (currentSafeHull != null && currentSafeHull != currentHull)
{
if (goToObjective?.Target != currentSafeHull)
@@ -219,6 +211,7 @@ namespace Barotrauma
TryAddSubObjective(ref goToObjective,
constructor: () => new AIObjectiveGoTo(currentSafeHull, character, objectiveManager, getDivingGearIfNeeded: true)
{
SpeakIfFails = false,
AllowGoingOutside =
character.IsProtectedFromPressure ||
character.CurrentHull == null ||
@@ -300,6 +293,7 @@ namespace Barotrauma
//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)
{
character.ReleaseSecondaryItem();
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
}
else
@@ -349,7 +343,6 @@ namespace Barotrauma
if (ignoredHulls != null && ignoredHulls.Contains(hull)) { continue; }
if (HumanAIController.UnreachableHulls.Contains(hull)) { continue; }
if (connectedSubs != null && !connectedSubs.Contains(hull.Submarine)) { continue; }
//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
//(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;
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);
}
}
}
}

View File

@@ -37,40 +37,56 @@ namespace Barotrauma
{
Priority = 0;
Abandon = true;
return Priority;
}
else if (HumanAIController.IsTrueForAnyCrewMember(
other => other != HumanAIController &&
other.Character.IsBot &&
other.ObjectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks &&
fixLeaks.SubObjectives.Any(so => so is AIObjectiveFixLeak fixObjective && fixObjective.Leak == Leak)))
float coopMultiplier = 1;
foreach (var c in Character.CharacterList)
{
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
{
float reduction = isPriority ? 1 : 2;
float maxPriority = AIObjectiveManager.LowestOrderPriority - reduction;
if (operateObjective != null && objectiveManager.GetActiveObjective<AIObjectiveFixLeaks>() is AIObjectiveFixLeaks fixLeaks && fixLeaks.CurrentSubObjective == this)
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))
{
// Prioritize leaks that we are already fixing
Priority = maxPriority;
}
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));
// 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 * coopMultiplier), 0, 1));
}
return Priority;
}

View File

@@ -38,9 +38,9 @@ namespace Barotrauma
protected override float TargetEvaluation()
{
int totalLeaks = Targets.Count();
int totalLeaks = Targets.Count;
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;
if (objectiveManager.IsOrder(this))
{
@@ -52,7 +52,7 @@ namespace Barotrauma
int secondaryLeaks = Targets.Count(l => l.IsRoomToRoom);
int leaks = totalLeaks - secondaryLeaks;
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
return 0;

View File

@@ -1,9 +1,11 @@
using Barotrauma.Items.Components;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
namespace Barotrauma
{
@@ -31,14 +33,15 @@ namespace Barotrauma
private ISpatialEntity moveToTarget;
private bool isDoneSeeking;
public Item TargetItem => targetItem;
private int currSearchIndex;
private int currentSearchIndex;
public ImmutableHashSet<Identifier> ignoredContainerIdentifiers;
public ImmutableHashSet<Identifier> ignoredIdentifiersOrTags;
private AIObjectiveGoTo goToObjective;
private float currItemPriority;
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 MustBeSpecificItem { get; set; }
@@ -76,7 +79,7 @@ namespace Barotrauma
public AIObjectiveGetItem(Character character, Item targetItem, AIObjectiveManager objectiveManager, bool equip = true, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
currentSearchIndex = 0;
Equip = equip;
originalTarget = 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)
: base(character, objectiveManager, priorityModifier)
{
currSearchIndex = -1;
currentSearchIndex = 0;
Equip = equip;
this.spawnItemIfNotFound = spawnItemIfNotFound;
this.checkInventory = checkInventory;
@@ -125,7 +128,7 @@ namespace Barotrauma
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()
@@ -246,7 +249,7 @@ namespace Barotrauma
else
{
character.SelectCharacter(c);
canInteract = character.CanInteractWith(c, maxDist: DefaultReach);
canInteract = character.CanInteractWith(c);
character.DeselectCharacter();
}
}
@@ -268,7 +271,7 @@ namespace Barotrauma
Inventory itemInventory = targetItem.ParentInventory;
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)
{
@@ -298,8 +301,11 @@ namespace Barotrauma
if (!Equip)
{
// Try equipping and wearing the item
Wear = true;
Equip = true;
if (!objectiveManager.HasActiveObjective<AIObjectiveCleanupItem>() && !objectiveManager.HasActiveObjective<AIObjectiveLoadItem>())
{
Wear = true;
}
return;
}
#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()
{
if (IdentifiersOrTags == null)
@@ -349,13 +359,16 @@ namespace Barotrauma
if (targetItem == null)
{
#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
Abandon = true;
}
return;
}
if (HumanAIController.DebugAI)
{
StopWatch.Restart();
}
float priority = Math.Clamp(objectiveManager.GetCurrentPriority(), 10, 100);
if (!CheckPathForEachItem)
{
@@ -366,12 +379,22 @@ namespace Barotrauma
CheckPathForEachItem = priority >= AIObjectiveManager.LowestOrderPriority && (objectiveManager.IsCurrentOrder<AIObjectiveFixLeaks>() || objectiveManager.CurrentOrder is AIObjectiveGoTo gotoOrder && gotoOrder.IsFollowOrderObjective);
}
bool checkPath = CheckPathForEachItem;
bool hasCalledPathFinder = false;
int itemsPerFrame = (int)priority;
for (int i = 0; i < itemsPerFrame && currSearchIndex < Item.ItemList.Count - 1; i++)
// Reset if the character has switched subs.
if (itemList != null && !character.Submarine.IsEntityFoundOnThisSub(itemList.FirstOrDefault(), includingConnectedSubs: true))
{
currSearchIndex++;
var item = Item.ItemList[currSearchIndex];
currentSearchIndex = 0;
}
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;
if (itemSub == null) { continue; }
Submarine mySub = character.Submarine;
@@ -395,8 +418,6 @@ namespace Barotrauma
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 (item.ParentInventory is ItemInventory itemInventory)
{
@@ -411,11 +432,14 @@ namespace Barotrauma
if (rootInventoryOwner is Item ownerItem)
{
if (!ownerItem.IsInteractable(character)) { continue; }
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)
if (ownerItem != item)
{
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;
@@ -463,22 +487,69 @@ namespace Barotrauma
{
itemPriority *= item.Condition / item.MaxCondition;
}
if (checkPath)
{
itemCandidates.Add((item, itemPriority));
}
// Ignore if the item has a lower priority than the currently selected one
if (itemPriority < currItemPriority) { continue; }
if (!hasCalledPathFinder && PathSteering != null && checkPath)
if (EvaluateCombatPriority && itemPriority <= 0)
{
hasCalledPathFinder = true;
var path = PathSteering.PathFinder.FindPath(character.SimPosition, item.SimPosition, character.Submarine, errorMsgStr: $"AIObjectiveGetItem {character.DisplayName}", nodeFilter: node => node.Waypoint.CurrentHull != null);
if (path.Unreachable) { continue; }
// Not good enough
continue;
}
currItemPriority = itemPriority;
targetItem = item;
moveToTarget = rootInventoryOwner ?? item;
}
if (currSearchIndex >= Item.ItemList.Count - 1)
if (currentSearchIndex >= itemList.Count - 1)
{
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)
{
@@ -569,11 +640,11 @@ namespace Barotrauma
{
if (!item.HasAccess(character)) { 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 (ItemFilter != null && !ItemFilter(item)) { return false; }
if (RequireNonEmpty && item.Components.Any(i => !i.IsNotEmpty(character))) { return false; }
return CheckItemIdentifiersOrTags(item, IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
if (RequireNonEmpty && item.Components.Any(i => i.IsEmpty(character))) { return false; }
return item.HasIdentifierOrTags(IdentifiersOrTags) || (AllowVariants && !item.Prefab.VariantOf.IsEmpty && IdentifiersOrTags.Contains(item.Prefab.VariantOf));
}
public override void Reset()
@@ -591,7 +662,7 @@ namespace Barotrauma
targetItem = originalTarget;
moveToTarget = targetItem?.GetRootInventoryOwner();
isDoneSeeking = false;
currSearchIndex = 0;
currentSearchIndex = 0;
currItemPriority = 0;
}

View File

@@ -257,7 +257,7 @@ namespace Barotrauma
}
}
}
else if (HumanAIController.HasValidPath(requireNonDirty: true, requireUnfinished: false))
else if (HumanAIController.HasValidPath(requireUnfinished: false))
{
waitUntilPathUnreachable = pathWaitingTime;
}
@@ -364,7 +364,8 @@ namespace Barotrauma
else
{
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);
seekGapsTimer = seekGapsInterval * Rand.Range(0.1f, 1.1f);
@@ -388,6 +389,10 @@ namespace Barotrauma
}
}
}
else
{
TargetGap = null;
}
}
}
else
@@ -436,9 +441,9 @@ namespace Barotrauma
{
var leftHandItem = character.GetEquippedItem(slotType: InvSlotType.LeftHand);
var rightHandItem = character.GetEquippedItem(slotType: InvSlotType.RightHand);
bool handsFull =
(leftHandItem != null && character.Inventory.CheckIfAnySlotAvailable(leftHandItem, inWrongSlot: false) == -1) ||
(rightHandItem != null && character.Inventory.CheckIfAnySlotAvailable(rightHandItem, inWrongSlot: false) == -1);
bool handsFull =
(leftHandItem != null && !character.Inventory.IsAnySlotAvailable(leftHandItem)) ||
(rightHandItem != null && !character.Inventory.IsAnySlotAvailable(rightHandItem));
if (!handsFull)
{
bool hasBattery = false;
@@ -473,7 +478,7 @@ namespace Barotrauma
// Try to switch batteries
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))
{
useScooter = false;
@@ -488,7 +493,7 @@ namespace Barotrauma
if (!useScooter)
{
// 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;
}
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)
{
character.ReleaseSecondaryItem();
PathSteering.SteeringSeekSimple(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
@@ -540,6 +552,7 @@ namespace Barotrauma
}
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringManual(deltaTime, Vector2.Normalize(Target.WorldPosition - character.WorldPosition));
if (character.AnimController.InWater)
{
@@ -560,6 +573,7 @@ namespace Barotrauma
}
else
{
character.ReleaseSecondaryItem();
SteeringManager.SteeringSeek(character.GetRelativeSimPosition(Target), 10);
if (character.AnimController.InWater)
{
@@ -573,6 +587,7 @@ namespace Barotrauma
{
if (!character.HasEquippedItem("scooter".ToIdentifier())) { return; }
SteeringManager.Reset();
character.ReleaseSecondaryItem();
character.CursorPosition = targetWorldPos;
if (character.Submarine != null)
{
@@ -624,7 +639,7 @@ namespace Barotrauma
}
else if (target is Character c)
{
return c.CurrentHull;
return c.CurrentHull ?? c.AnimController.CurrentHull;
}
else if (target is Structure structure)
{
@@ -772,6 +787,14 @@ namespace Barotrauma
{
StopMovement();
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();
}
@@ -781,6 +804,10 @@ namespace Barotrauma
findDivingGear = null;
seekGapsTimer = 0;
TargetGap = null;
if (SteeringManager is IndoorsSteeringManager pathSteering)
{
pathSteering.ResetPath();
}
}
}
}

View File

@@ -163,15 +163,22 @@ namespace Barotrauma
character.SelectedItem = null;
CleanupItems(deltaTime);
if (!character.IsClimbing)
{
CleanupItems(deltaTime);
}
if (behavior == BehaviorType.StayInHull && TargetHull == null && character.CurrentHull != null)
{
TargetHull = character.CurrentHull;
}
bool currentTargetIsInvalid = currentTarget == null || IsForbidden(currentTarget) || (PathSteering.CurrentPath != null && PathSteering.CurrentPath.Nodes.Any(n => HumanAIController.UnsafeHulls.Contains(n.CurrentHull)));
if (behavior == BehaviorType.StayInHull && !currentTargetIsInvalid)
bool 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;
bool stayInHull = character.CurrentHull == currentTarget && IsSteeringFinished() && !character.IsClimbing;
@@ -305,12 +312,8 @@ namespace Barotrauma
{
if (character.IsClimbing)
{
if (character.AnimController.GetHeightFromFloor() < 0.1f)
{
character.AnimController.Anim = AnimController.Animation.None;
character.SelectedSecondaryItem = null;
}
return;
PathSteering.Reset();
character.StopClimbing();
}
var currentHull = character.CurrentHull;
if (!character.AnimController.InWater && currentHull != null)
@@ -362,6 +365,7 @@ namespace Barotrauma
}
else
{
character.ReleaseSecondaryItem();
PathSteering.SteeringManual(deltaTime, Vector2.Normalize(diff));
}
return;

View File

@@ -24,7 +24,7 @@ namespace Barotrauma
private ImmutableHashSet<Identifier> ValidContainableItemIdentifiers { get; }
private static Dictionary<ItemPrefab, ImmutableHashSet<Identifier>> AllValidContainableItemIdentifiers { get; } = new Dictionary<ItemPrefab, ImmutableHashSet<Identifier>>();
private int itemIndex = 0;
private int itemIndex;
private AIObjectiveDecontainItem decontainObjective;
private readonly HashSet<Item> ignoredItems = new HashSet<Item>();
private Item targetItem;
@@ -219,9 +219,8 @@ namespace Barotrauma
return Priority;
}
public override void Update(float deltaTime)
protected override void Act(float deltaTime)
{
base.Update(deltaTime);
if (targetItem == null)
{
if (character.FindItem(ref itemIndex, out Item item, identifiers: ValidContainableItemIdentifiers, ignoreBroken: false, customPredicate: IsValidContainable, customPriorityFunction: GetPriority))
@@ -233,6 +232,7 @@ namespace Barotrauma
}
targetItem = item;
}
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
float GetPriority(Item item)
{
try
@@ -256,11 +256,7 @@ namespace Barotrauma
}
}
}
}
protected override void Act(float deltaTime)
{
if (targetItem != null)
else
{
if(decontainObjective == null && !IsValidContainable(targetItem))
{
@@ -290,10 +286,6 @@ namespace Barotrauma
Reset();
});
}
else
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
}
private bool IsValidContainable(Item item)
@@ -310,7 +302,7 @@ namespace Barotrauma
if (parentItem.HasTag("donttakeitems")) { 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 (AIObjectiveLoadItems.ItemMatchesTargetCondition(item, TargetItemCondition)) { return false; }
if (TargetItemCondition == AIObjectiveLoadItems.ItemCondition.Full)

View File

@@ -63,7 +63,7 @@ namespace Barotrauma
{
if (item == null || item.Removed) { 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 (targetCondition.HasValue && container.Inventory.IsFull() && container.Inventory.AllItems.None(i => ItemMatchesTargetCondition(i, targetCondition.Value))) { return false; }
if (!AIObjectiveCleanupItems.IsItemInsideValidSubmarine(item, character)) { return false; }

View File

@@ -89,12 +89,7 @@ namespace Barotrauma
var target = objective.Key;
if (!Targets.Contains(target))
{
var subObjective = objective.Value;
if (CurrentSubObjective == subObjective)
{
CurrentSubObjective.Abandon = !CurrentSubObjective.IsCompleted;
}
subObjectives.Remove(subObjective);
subObjectives.Remove(objective.Value);
}
}
SyncRemovedObjectives(Objectives, GetList());
@@ -157,6 +152,11 @@ namespace Barotrauma
else
{
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);
Priority = MathHelper.Lerp(0, max, value);
}

View File

@@ -20,6 +20,8 @@ namespace Barotrauma
MaxValue = 2
}
public const float MaxObjectivePriority = 100;
public const float EmergencyObjectivePriority = 90;
public const float HighestOrderPriority = 70;
public const float LowestOrderPriority = 60;
public const float RunPriority = 50;
@@ -134,11 +136,21 @@ namespace Barotrauma
{
CoroutineManager.StopCoroutines(delayedObjective.Value);
}
var prevIdleObjective = GetObjective<AIObjectiveIdle>();
DelayedObjectives.Clear();
Objectives.Clear();
FailedAutonomousObjectives = false;
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;
foreach (var autonomousObjective in character.Info.Job.Prefab.AutonomousObjectives)
{
@@ -164,7 +176,7 @@ namespace Barotrauma
AddObjective(objective, delay: Rand.Value() / 2);
objectiveCount++;
}
}
}
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
}
@@ -419,7 +431,7 @@ namespace Barotrauma
newObjective = new AIObjectiveGoTo(order.OrderGiver, character, this, repeat: true, priorityModifier: priorityModifier)
{
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,
ExtraDistanceWhileSwimming = 100,
AllowGoingOutside = true,
@@ -642,10 +654,11 @@ namespace Barotrauma
public AIObjective GetActiveObjective() => CurrentObjective?.GetActiveObjective();
public T GetOrder<T>() where T : AIObjective => CurrentOrders.FirstOrDefault(o => o.Objective is T)?.Objective as T;
/// <summary>
/// Returns the last active objective of the specific type.
/// </summary>
public T GetActiveObjective<T>() where T : AIObjective => CurrentObjective?.GetSubObjectivesRecursive(includingSelf: true).LastOrDefault(so => so is T) as T;
public T GetLastActiveObjective<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>
/// Returns all active objectives of the specific type. Creates a new collection -> don't use too frequently.

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