v1.0.13.1 (first post-1.0 patch)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -425,6 +425,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);
|
||||
@@ -632,15 +636,29 @@ namespace Barotrauma
|
||||
commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) =>
|
||||
{
|
||||
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);
|
||||
@@ -1146,7 +1164,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);
|
||||
|
||||
@@ -1248,8 +1266,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;
|
||||
@@ -1264,7 +1282,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);
|
||||
|
||||
@@ -2323,7 +2341,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));
|
||||
}
|
||||
@@ -2859,7 +2877,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) =>
|
||||
{
|
||||
@@ -3345,6 +3363,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"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1072,18 +1072,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -377,6 +377,8 @@ namespace Barotrauma.Lights
|
||||
|
||||
public float Priority;
|
||||
|
||||
public float PriorityMultiplier = 1.0f;
|
||||
|
||||
private Vector2 lightTextureTargetSize;
|
||||
|
||||
public Vector2 LightTextureTargetSize
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -859,17 +865,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);
|
||||
}
|
||||
@@ -886,7 +899,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);
|
||||
}
|
||||
@@ -899,7 +912,7 @@ namespace Barotrauma.Networking
|
||||
$"Mission equality check failed. The mission selected at your end doesn't match the one loaded by the server " +
|
||||
$"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);
|
||||
}
|
||||
@@ -922,7 +935,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);
|
||||
}
|
||||
@@ -1322,6 +1335,8 @@ namespace Barotrauma.Networking
|
||||
eventErrorWritten = false;
|
||||
GameMain.NetLobbyScreen.StopWaitingForStartRound();
|
||||
|
||||
debugStartGameCampaignSaveID = null;
|
||||
|
||||
while (CoroutineManager.IsCoroutineRunning("EndGame"))
|
||||
{
|
||||
EndCinematic?.Stop();
|
||||
@@ -1471,7 +1486,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).");
|
||||
@@ -1480,30 +1516,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);
|
||||
@@ -1697,7 +1717,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;
|
||||
|
||||
@@ -2587,31 +2607,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)
|
||||
@@ -2864,12 +2877,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 + ")"));
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -3546,10 +3546,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);
|
||||
@@ -5263,7 +5298,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (PlayerInput.KeyHit(Keys.E) && mode == Mode.Default)
|
||||
if (PlayerInput.KeyHit(InputType.Use) && mode == Mode.Default)
|
||||
{
|
||||
if (dummyCharacter != null)
|
||||
{
|
||||
@@ -5354,6 +5389,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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal class LeakyBucket
|
||||
{
|
||||
private readonly Queue<Action> queue;
|
||||
private readonly int capacity;
|
||||
private readonly float cooldownInSeconds;
|
||||
private float timer;
|
||||
|
||||
public LeakyBucket(float cooldownInSeconds, int capacity)
|
||||
{
|
||||
this.cooldownInSeconds = cooldownInSeconds;
|
||||
this.capacity = capacity;
|
||||
queue = new Queue<Action>(capacity);
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
if (timer > 0f)
|
||||
{
|
||||
timer -= deltaTime;
|
||||
return;
|
||||
}
|
||||
|
||||
if (queue.Count is 0) { return; }
|
||||
|
||||
TryDequeue();
|
||||
}
|
||||
|
||||
private void TryDequeue()
|
||||
{
|
||||
timer = cooldownInSeconds;
|
||||
if (queue.TryDequeue(out var action))
|
||||
{
|
||||
action.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryEnqueue(Action item)
|
||||
{
|
||||
if (queue.Count >= capacity) { return false; }
|
||||
queue.Enqueue(item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,11 +142,17 @@ namespace Barotrauma
|
||||
GameMain.Instance.GraphicsDevice.SetRenderTarget(rt);
|
||||
GameMain.Instance.GraphicsDevice.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;
|
||||
|
||||
@@ -6,14 +6,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>
|
||||
<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'">
|
||||
|
||||
@@ -6,14 +6,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>
|
||||
<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'">
|
||||
|
||||
@@ -6,7 +6,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>
|
||||
@@ -14,7 +14,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>
|
||||
<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'">
|
||||
|
||||
@@ -6,14 +6,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-2023</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>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -6,14 +6,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-2023</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>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -13,6 +13,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>
|
||||
@@ -608,12 +617,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.");
|
||||
@@ -622,7 +631,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);
|
||||
});
|
||||
|
||||
@@ -1992,6 +2001,7 @@ namespace Barotrauma
|
||||
"freecam",
|
||||
(Client client, Vector2 cursorWorldPos, string[] args) =>
|
||||
{
|
||||
client.UsingFreeCam = true;
|
||||
GameMain.Server.SetClientCharacter(client, null);
|
||||
client.SpectateOnly = true;
|
||||
}
|
||||
@@ -2105,7 +2115,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);
|
||||
@@ -2114,8 +2124,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);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2509,26 +2519,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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 _:
|
||||
|
||||
@@ -102,9 +102,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)
|
||||
if (similarity + c.ChatSpamSpeed > 5.0f && !isSpamExempt)
|
||||
{
|
||||
GameMain.Server.KarmaManager.OnSpamFilterTriggered(c);
|
||||
|
||||
@@ -125,7 +125,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
c.ChatSpamSpeed += similarity + 0.5f;
|
||||
|
||||
if (c.ChatSpamTimer > 0.0f && !isOwner)
|
||||
if (c.ChatSpamTimer > 0.0f && !isSpamExempt)
|
||||
{
|
||||
ChatMessage denyMsg = Create("", TextManager.Get("SpamFilterBlocked").Value, ChatMessageType.Server, null);
|
||||
c.ChatSpamTimer = 10.0f;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -303,7 +303,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);
|
||||
@@ -332,9 +332,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)
|
||||
@@ -387,10 +386,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)
|
||||
{
|
||||
@@ -692,10 +691,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();
|
||||
switch (header)
|
||||
{
|
||||
@@ -772,9 +775,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,11 +790,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;
|
||||
@@ -1085,7 +1094,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;
|
||||
@@ -1246,7 +1255,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());
|
||||
@@ -1406,19 +1415,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)
|
||||
{
|
||||
@@ -1442,45 +1455,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1520,11 +1542,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();
|
||||
@@ -2582,16 +2600,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;
|
||||
}
|
||||
|
||||
@@ -3588,15 +3610,28 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
private readonly RateLimiter charInfoRateLimiter = new(
|
||||
maxRequests: 5,
|
||||
expiryInSeconds: 10,
|
||||
punishmentRules: new[]
|
||||
{
|
||||
(RateLimitAction.OnLimitReached, RateLimitPunishment.Announce),
|
||||
(RateLimitAction.OnLimitDoubled, RateLimitPunishment.Kick)
|
||||
});
|
||||
|
||||
private void UpdateCharacterInfo(IReadMessage message, Client sender)
|
||||
{
|
||||
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;
|
||||
@@ -3614,42 +3649,31 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
}
|
||||
|
||||
int tagCount = message.ReadByte();
|
||||
HashSet<Identifier> tagSet = new HashSet<Identifier>();
|
||||
for (int i = 0; i < tagCount; i++)
|
||||
{
|
||||
tagSet.Add(message.ReadIdentifier());
|
||||
}
|
||||
int hairIndex = message.ReadByte();
|
||||
int beardIndex = message.ReadByte();
|
||||
int moustacheIndex = message.ReadByte();
|
||||
int faceAttachmentIndex = message.ReadByte();
|
||||
Color skinColor = message.ReadColorR8G8B8();
|
||||
Color hairColor = message.ReadColorR8G8B8();
|
||||
Color facialHairColor = message.ReadColorR8G8B8();
|
||||
|
||||
List<JobVariant> jobPreferences = new List<JobVariant>();
|
||||
int count = message.ReadByte();
|
||||
for (int i = 0; i < Math.Min(count, 3); i++)
|
||||
{
|
||||
string jobIdentifier = message.ReadString();
|
||||
int variant = message.ReadByte();
|
||||
if (JobPrefab.Prefabs.TryGet(jobIdentifier, out JobPrefab jobPrefab))
|
||||
{
|
||||
if (jobPrefab.HiddenJob) { continue; }
|
||||
jobPreferences.Add(new JobVariant(jobPrefab, variant));
|
||||
}
|
||||
}
|
||||
|
||||
sender.CharacterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, newName);
|
||||
sender.CharacterInfo.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
|
||||
sender.CharacterInfo.Head.SkinColor = skinColor;
|
||||
sender.CharacterInfo.Head.HairColor = hairColor;
|
||||
sender.CharacterInfo.Head.FacialHairColor = facialHairColor;
|
||||
|
||||
if (jobPreferences.Count > 0)
|
||||
sender.CharacterInfo.RecreateHead(
|
||||
tags: netInfo.Tags.ToImmutableHashSet(),
|
||||
hairIndex: netInfo.HairIndex,
|
||||
beardIndex: netInfo.BeardIndex,
|
||||
moustacheIndex: netInfo.MoustacheIndex,
|
||||
faceAttachmentIndex: netInfo.FaceAttachmentIndex);
|
||||
|
||||
sender.CharacterInfo.Head.SkinColor = netInfo.SkinColor;
|
||||
sender.CharacterInfo.Head.HairColor = netInfo.HairColor;
|
||||
sender.CharacterInfo.Head.FacialHairColor = netInfo.FacialHairColor;
|
||||
|
||||
if (netInfo.JobVariants.Length > 0)
|
||||
{
|
||||
sender.JobPreferences = jobPreferences;
|
||||
List<JobVariant> variants = new List<JobVariant>();
|
||||
foreach (NetJobVariant jv in netInfo.JobVariants)
|
||||
{
|
||||
if (jv.ToJobVariant() is { } variant)
|
||||
{
|
||||
variants.Add(variant);
|
||||
}
|
||||
}
|
||||
|
||||
sender.JobPreferences = variants;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,11 +24,28 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
|
||||
protected readonly Callbacks callbacks;
|
||||
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();
|
||||
|
||||
@@ -250,9 +267,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()
|
||||
};
|
||||
|
||||
@@ -520,7 +520,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.");
|
||||
@@ -585,8 +585,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;
|
||||
}
|
||||
@@ -600,7 +599,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))
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -158,7 +158,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 + ")"));
|
||||
|
||||
@@ -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,
|
||||
@@ -56,10 +57,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());
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -224,10 +224,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));
|
||||
}
|
||||
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier)));
|
||||
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier)));
|
||||
|
||||
|
||||
232
Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs
Normal file
232
Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
135
Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs
Normal file
135
Barotrauma/BarotraumaServer/ServerSource/Utils/RateLimiter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,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-2023</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>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace Barotrauma
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return CanEquip(character, item);
|
||||
return CanEquip(character, item, allowWearing: false);
|
||||
}
|
||||
|
||||
public override void OnDeselected()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (mask != targetItem)
|
||||
{
|
||||
character.Inventory.TryPutItem(mask, character, CharacterInventory.anySlot);
|
||||
character.Inventory.TryPutItem(mask, character, CharacterInventory.AnySlot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,17 +52,14 @@ namespace Barotrauma
|
||||
objectiveManager.HasOrder<AIObjectiveReturn>(o => o.Priority > 0) ||
|
||||
objectiveManager.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -125,11 +127,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)
|
||||
{
|
||||
@@ -155,7 +167,7 @@ namespace Barotrauma
|
||||
AddObjective(objective, delay: Rand.Value() / 2);
|
||||
objectiveCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
_waitTimer = Math.Max(_waitTimer, Rand.Range(0.5f, 1f) * objectiveCount);
|
||||
}
|
||||
|
||||
@@ -410,7 +422,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,
|
||||
@@ -633,10 +645,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.
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace Barotrauma
|
||||
else if (!isOrder)
|
||||
{
|
||||
var steering = component?.Item.GetComponent<Steering>();
|
||||
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != HumanAIController && c.Character.IsCaptain)))
|
||||
if (steering != null && (steering.AutoPilot || HumanAIController.IsTrueForAnyCrewMember(c => c != character && c.IsCaptain, onlyActive: true, onlyConnectedSubs: true)))
|
||||
{
|
||||
// Ignore if already set to autopilot or if there's a captain onboard
|
||||
Priority = 0;
|
||||
@@ -204,7 +204,7 @@ namespace Barotrauma
|
||||
}
|
||||
if (operateTarget != null)
|
||||
{
|
||||
if (HumanAIController.IsTrueForAnyCrewMember(other => other != HumanAIController && other.Character.IsBot && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
|
||||
if (HumanAIController.IsTrueForAnyBotInTheCrew(other => other != HumanAIController && other.ObjectiveManager.GetActiveObjective() is AIObjectiveOperateItem operateObjective && operateObjective.operateTarget == operateTarget))
|
||||
{
|
||||
// Another crew member is already targeting this entity (leak).
|
||||
Abandon = true;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user