Merge remote-tracking branch 'origin/master' into dev

# Conflicts:
#	Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs
#	Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs
#	Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs
#	Barotrauma/BarotraumaClient/LinuxClient.csproj
#	Barotrauma/BarotraumaClient/MacClient.csproj
#	Barotrauma/BarotraumaClient/WindowsClient.csproj
#	Barotrauma/BarotraumaServer/LinuxServer.csproj
#	Barotrauma/BarotraumaServer/MacServer.csproj
#	Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs
#	Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs
#	Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs
#	Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs
#	Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs
#	Barotrauma/BarotraumaServer/WindowsServer.csproj
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs
#	Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Enums.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs
#	Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs
#	Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs
#	Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs
#	Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs
#	Barotrauma/BarotraumaShared/changelog.txt
This commit is contained in:
Regalis11
2023-06-15 16:55:39 +03:00
238 changed files with 5395 additions and 3045 deletions
+4 -4
View File
@@ -27,13 +27,15 @@ body:
attributes:
label: Reproduction steps
description: |
If possible, describe how the developers can get the bug to happen. It is often extremely hard to fix a bug if we don't know how to reproduce it.
If possible, describe how the developers can get the bug to happen (or, in other words, what actions lead to you encountering the bug). **This is by far the most important part of the report** - it is often extremely difficult, or even impossible, to diagnose an issue if we don't know the conditions it occurs in.
If you have a save, a submarine file, screenshots or any other files that might help us diagnose the issue, you can attach them here. Note that GitHub doesn't support the .save or .sub file extensions, so you should .zip those types of files to allow them to be attached.
placeholder: |
1. Start a multiplayer campaign
2. Spawn a bike horn with console commands
3. Use the bike horn
4. Observe how the game crashes
validations:
required: true
- type: dropdown
id: prevalence
attributes:
@@ -52,9 +54,7 @@ body:
label: Version
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
options:
- 0.21.6.0
- 0.21.6.0 (Unstable)
- Faction/endgame test branch
- v1.0.20.1
- Other
validations:
required: true
@@ -19,7 +19,7 @@ namespace Barotrauma
var target = _selectedAiTarget ?? _lastAiTarget;
if (target != null && target.Entity != null)
{
var memory = GetTargetMemory(target, false);
var memory = GetTargetMemory(target);
if (memory != null)
{
Vector2 targetPos = memory.Location;
@@ -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);
@@ -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;
}
@@ -214,7 +214,6 @@ namespace Barotrauma
double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI;
cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f;
TransformCursorPos();
bool ragdollInput = msg.ReadBoolean();
keys[(int)InputType.Ragdoll].Held = ragdollInput;
@@ -850,7 +850,7 @@ namespace Barotrauma
if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover)
{
//highlight the slot the treatment item is in
var rootContainer = matchingItem.GetRootContainer() ?? matchingItem;
var rootContainer = matchingItem.RootContainer ?? matchingItem;
var index = Character.Controlled.Inventory.FindIndex(rootContainer);
if (Character.Controlled.Inventory.visualSlots != null && index > -1 && index < Character.Controlled.Inventory.visualSlots.Length &&
Character.Controlled.Inventory.visualSlots[index].HighlightTimer <= 0.0f)
@@ -870,7 +870,7 @@ namespace Barotrauma
{
if (wearable.Type == WearableType.Hair)
{
if (HairWithHatSprite != null)
if (HairWithHatSprite != null && !hideLimb)
{
DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect);
depthStep += step;
@@ -86,7 +86,7 @@ namespace Barotrauma
public string ModVersion = ContentPackage.DefaultModVersion;
public Md5Hash? ExpectedHash { get; private set; }
public Md5Hash? ExpectedHash { get; set; }
public bool IsCore = false;
@@ -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
@@ -9,6 +9,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
@@ -636,15 +637,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);
@@ -1141,6 +1156,15 @@ namespace Barotrauma
GameMain.LightManager.DebugLos = state;
NewMessage("Los debug draw mode " + (GameMain.LightManager.DebugLos ? "enabled" : "disabled"), Color.Yellow);
});
AssignOnExecute("debugwiring", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
{
state = !ConnectionPanel.DebugWiringMode;
}
ConnectionPanel.DebugWiringMode = state;
NewMessage("Wiring debug mode " + (ConnectionPanel.DebugWiringMode ? "enabled" : "disabled"), Color.Yellow);
});
AssignRelayToServer("debugdraw", false);
AssignOnExecute("devmode", (string[] args) =>
@@ -1263,8 +1287,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;
@@ -1279,7 +1303,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);
@@ -2338,7 +2362,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));
}
@@ -2811,7 +2835,26 @@ namespace Barotrauma
ContentPackageManager.EnabledPackages.ReloadCore();
}));
#warning TODO: reimplement?
#if WINDOWS
commands.Add(new Command("startdedicatedserver", "", (string[] args) =>
{
Process.Start("DedicatedServer.exe");
}));
commands.Add(new Command("editserversettings", "", (string[] args) =>
{
if (Process.GetProcessesByName("DedicatedServer").Length > 0)
{
NewMessage("Can't be edited if DedicatedServer.exe is already running", Color.Red);
}
else
{
Process.Start("notepad.exe", "serversettings.xml");
}
}));
#endif
#warning TODO: reimplement?
/*commands.Add(new Command("ingamemodswap", "", (string[] args) =>
{
ContentPackage.IngameModSwap = !ContentPackage.IngameModSwap;
@@ -662,34 +662,36 @@ namespace Barotrauma
Identifier missionIdentifier = msg.ReadIdentifier();
int locationIndex = msg.ReadInt32();
int destinationIndex = msg.ReadInt32();
string missionName = msg.ReadString();
MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier);
if (prefab != null)
if (Screen.Selected != GameMain.NetLobbyScreen)
{
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName),
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier);
if (prefab != null)
{
IconColor = prefab.IconColor
};
if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count)
{
Location location = map.Locations[locationIndex];
map.Discover(location, checkTalents: false);
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName),
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
{
IconColor = prefab.IconColor
};
if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count)
{
Location location = map.Locations[locationIndex];
map.Discover(location, checkTalents: false);
LocationConnection? connection = null;
if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count)
{
Location destination = map.Locations[destinationIndex];
connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination));
}
if (connection != null)
{
location.UnlockMission(prefab, connection);
}
else
{
location.UnlockMission(prefab);
LocationConnection? connection = null;
if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count)
{
Location destination = map.Locations[destinationIndex];
connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination));
}
if (connection != null)
{
location.UnlockMission(prefab, connection);
}
else
{
location.UnlockMission(prefab);
}
}
}
}
@@ -88,13 +88,14 @@ namespace Barotrauma
public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element)
=> TextManager.SpeciallyHandledCharCategories
.Where(category => element.GetAttributeBool($"is{category}", category switch {
// CJK isn't supported by default
// CJK and Japanese aren't supported by default
TextManager.SpeciallyHandledCharCategory.CJK => false,
TextManager.SpeciallyHandledCharCategory.Japanese => false,
// For backwards compatibility, we assume that Cyrillic is supported by default
TextManager.SpeciallyHandledCharCategory.Cyrillic => true,
_ => throw new Exception("unreachable")
_ => throw new NotImplementedException($"nameof{category} not implemented.")
}))
.Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category);
@@ -517,6 +518,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;
@@ -533,7 +533,7 @@ namespace Barotrauma
{
if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
{
if (campaign.GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation)
if (MathF.Round(campaign.GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation)
{
return false;
}
@@ -532,6 +532,7 @@ namespace Barotrauma
void drawRect(Vector2 topLeft, Vector2 bottomRight)
{
int minWidth = GUI.IntScale(5);
if (OverflowClip) { topLeft.X = Math.Max(topLeft.X, 0.0f); }
if (bottomRight.X - topLeft.X < minWidth) { bottomRight.X = topLeft.X + minWidth; }
GUI.DrawRectangle(spriteBatch,
Rect.Location.ToVector2() + topLeft,
@@ -801,6 +802,7 @@ namespace Barotrauma
IEnumerable<GUITextBox> GetAndSortTextBoxes(GUIComponent parent) => parent.GetAllChildren<GUITextBox>().OrderBy(t => t.Rect.Y).ThenBy(t => t.Rect.X);
GUITextBox SelectNextTextBox(GUIListBox listBox)
{
if (listBox?.SelectedComponent == null) { return null; }
var textBoxes = GetAndSortTextBoxes(listBox.SelectedComponent);
if (textBoxes.Any())
{
@@ -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)
@@ -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")
};
}
@@ -175,6 +175,7 @@ namespace Barotrauma
{
if (relativeOffset.NearlyEquals(value)) { return; }
relativeOffset = value;
recalculateRect = true;
RecalculateChildren(false, false);
}
}
@@ -870,7 +870,7 @@ namespace Barotrauma
{
foreach (var minRep in priceInfo.MinReputation)
{
if (campaign.GetReputation(minRep.Key) < minRep.Value)
if (MathF.Round(campaign.GetReputation(minRep.Key)) < minRep.Value)
{
return minRep;
}
@@ -1930,7 +1930,7 @@ namespace Barotrauma
"campaignstore.reputationrequired",
("[amount]", ((int)requiredReputation.Value.Value).ToString()),
("[faction]", TextManager.Get("faction." + requiredReputation.Value.Key).Value));
Color color = campaign.GetReputation(requiredReputation.Value.Key) < requiredReputation.Value.Value ?
Color color = MathF.Round(campaign.GetReputation(requiredReputation.Value.Key)) < requiredReputation.Value.Value ?
GUIStyle.Orange : GUIStyle.Green;
toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖";
}
@@ -807,8 +807,10 @@ namespace Barotrauma
{
if (GameMain.Client == null)
{
GameMain.GameSession.PurchaseSubmarine(selectedSubmarine);
GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch);
if (GameMain.GameSession.TryPurchaseSubmarine(selectedSubmarine))
{
GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch);
}
RefreshSubmarineDisplay(true);
}
else
@@ -829,7 +831,7 @@ namespace Barotrauma
{
if (GameMain.Client == null)
{
GameMain.GameSession.PurchaseSubmarine(selectedSubmarine);
GameMain.GameSession.TryPurchaseSubmarine(selectedSubmarine);
RefreshSubmarineDisplay(true);
}
else
@@ -23,9 +23,12 @@ namespace Barotrauma
private float votingTime = 100f;
private float timer;
private VoteType currentVoteType;
private Color SubmarineColor => GUIStyle.Orange;
private static Color SubmarineColor => GUIStyle.Orange;
private Point createdForResolution;
//timer ran out but server still hasn't notified of the result of the vote
public bool TimedOut => VoteRunning && timer - votingTime > 10.0f;
public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float votingTime)
{
if (starter == null || info == null) { return null; }
@@ -664,7 +664,10 @@ namespace Barotrauma
while (Timing.Accumulator >= Timing.Step)
{
Timing.TotalTime += Timing.Step;
if (!Paused)
{
Timing.TotalTimeUnpaused += Timing.Step;
}
Stopwatch sw = new Stopwatch();
sw.Start();
@@ -944,7 +947,10 @@ namespace Barotrauma
PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency);
}
if (!Paused) { Timing.Alpha = Timing.Accumulator / Timing.Step; }
if (!Paused)
{
Timing.Alpha = Timing.Accumulator / Timing.Step;
}
if (performanceCounterTimer.ElapsedMilliseconds > 1000)
{
@@ -31,7 +31,7 @@ namespace Barotrauma
// Item must be in a non-equipment slot if possible
if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; }
// Item must not be contained inside an item in an equipment slot
if (item.GetRootContainer() is Item rootContainer && IsInEquipmentSlot(rootContainer)) { return false; }
if (item.RootContainer is Item rootContainer && IsInEquipmentSlot(rootContainer)) { return false; }
return true;
}, recursive: true).Distinct();
@@ -166,6 +166,9 @@ namespace Barotrauma
if (Submarine.MainSub == null || Level.Loaded == null) { return; }
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)
@@ -194,13 +197,23 @@ namespace Barotrauma
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]");
allowEndingRound = !ForceMapUI && !ShowCampaignUI;
}
else
{
allowEndingRound = false;
}
break;
}
if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted())
@@ -227,7 +240,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;
}
@@ -193,7 +193,7 @@ namespace Barotrauma
if (GameMain.Client == null)
{
yield return CoroutineStatus.Failure;
yield return CoroutineStatus.Success;
}
if (GameMain.Client.LateCampaignJoin)
@@ -335,7 +335,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; }
@@ -345,7 +345,11 @@ namespace Barotrauma
endTransition.Stop();
overlayColor = Color.Transparent;
if (DateTime.Now > timeOut) { GameMain.NetLobbyScreen.Select(); }
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)
@@ -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;
}
}
@@ -310,9 +310,10 @@ namespace Barotrauma
};
}
}
var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform),
RichString.Rich(missionMessage), wrap: true);
if (selectedMissions.Contains(displayedMission) && displayedMission.Completed)
if (selectedMissions.Contains(displayedMission))
{
RichString reputationText = displayedMission.GetReputationRewardText();
if (!reputationText.IsNullOrEmpty())
@@ -324,7 +325,7 @@ namespace Barotrauma
if (totalReward > 0)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub)));
if (GameMain.IsMultiplayer && Character.Controlled is { } controlled)
if (GameMain.IsMultiplayer && Character.Controlled is { } controlled && displayedMission.Completed)
{
var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option<int>.Some(totalReward));
if (share > 0)
@@ -575,7 +575,7 @@ namespace Barotrauma
//cancel dragging if too far away from the container of the dragged item
if (DraggingItems.Any())
{
var rootContainer = DraggingItems.First().GetRootContainer();
var rootContainer = DraggingItems.First().RootContainer;
var rootInventory = DraggingItems.First().ParentInventory;
if (rootContainer != null)
@@ -155,8 +155,6 @@ namespace Barotrauma.Items.Components
convexHull.Enabled = true;
SetVertices(convexHull, rect);
}
convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom;
if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; }
}
@@ -169,12 +167,11 @@ namespace Barotrauma.Items.Components
IsHorizontal ?
new Vector2[] { new Vector2(verts[0].X, center.Y), new Vector2(verts[2].X, center.Y) } :
new Vector2[] { new Vector2(center.X, verts[0].Y), new Vector2(center.X, verts[2].Y) });
convexHull.MaxMergeLosVerticesDist = 35.0f;
}
partial void UpdateProjSpecific(float deltaTime)
{
convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom;
if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; }
if (shakeTimer > 0.0f)
{
shakeTimer -= deltaTime;
@@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
if (character == null || !character.IsKeyDown(InputType.Aim)) { return; }
if (character == null || !character.IsKeyDown(InputType.Aim) || !character.CanAim) { return; }
//camera focused on some other item/device, don't draw the crosshair
if (character.ViewTarget != null && (character.ViewTarget is Item viewTargetItem) && viewTargetItem.Prefab.FocusOnSelected) { return; }
@@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components
{
//whole text can fit in the textblock, no need to scroll
needsScrolling = false;
scrollingText = DisplayText.Value;
TextBlock.Text = scrollingText = DisplayText.Value;
scrollPadding = 0;
scrollAmount = 0.0f;
scrollIndex = 0;
@@ -73,6 +73,7 @@ namespace Barotrauma.Items.Components
Step = 0.05f,
OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
{
lastReceivedTargetForce = null;
float newTargetForce = barScroll * 200.0f - 100.0f;
if (Math.Abs(newTargetForce - targetForce) < 0.01) { return false; }
@@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components
private GUIFrame selectedItemFrame;
private GUIFrame selectedItemReqsFrame;
private GUITextBlock amountTextMin, amountTextMax;
private GUITextBlock amountTextMax;
private GUIScrollBar amountInput;
public GUIButton ActivateButton
@@ -29,6 +29,9 @@ namespace Barotrauma.Items.Components
private GUIComponent outputSlot;
private GUIComponent inputInventoryHolder, outputInventoryHolder;
private readonly List<GUIButton> itemCategoryButtons = new List<GUIButton>();
private MapEntityCategory? selectedItemCategory;
public FabricationRecipe SelectedItem
{
get { return selectedItem; }
@@ -77,7 +80,67 @@ namespace Barotrauma.Items.Components
AutoScaleVertical = true
};
var mainFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter)
var innerArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), paddedFrame.RectTransform, Anchor.Center), isHorizontal: true)
{
RelativeSpacing = 0.01f,
Stretch = true,
CanBeFocused = true
};
List<MapEntityCategory> itemCategories = Enum.GetValues<MapEntityCategory>().ToList();
itemCategories.Remove(MapEntityCategory.None);
itemCategories.RemoveAll(c => fabricationRecipes.None(f => f.Value?.TargetItem is ItemPrefab ti && ti.Category.HasFlag(c)));
itemCategoryButtons.Clear();
//only create category buttons if there's more than one category in addition to "All"
if (itemCategories.Count > 2)
{
// === Item category buttons ===
var categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.05f, 1.0f), innerArea.RectTransform))
{
RelativeSpacing = 0.01f
};
int buttonSize = Math.Min(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Height / itemCategories.Count);
var categoryButton = new GUIButton(new RectTransform(new Point(buttonSize), categoryButtonContainer.RectTransform), style: "CategoryButton.All")
{
ToolTip = TextManager.Get("MapEntityCategory.All"),
OnClicked = OnClickedCategoryButton
};
itemCategoryButtons.Add(categoryButton);
foreach (MapEntityCategory category in itemCategories)
{
categoryButton = new GUIButton(new RectTransform(new Point(buttonSize), categoryButtonContainer.RectTransform),
style: "CategoryButton." + category)
{
ToolTip = TextManager.Get("MapEntityCategory." + category),
UserData = category,
OnClicked = OnClickedCategoryButton
};
itemCategoryButtons.Add(categoryButton);
}
bool OnClickedCategoryButton(GUIButton button, object userData)
{
MapEntityCategory? newCategory = !button.Selected ? (MapEntityCategory?)userData : null;
if (newCategory.HasValue) { itemFilterBox.Text = ""; }
selectedItemCategory = newCategory;
FilterEntities(newCategory, itemFilterBox.Text);
return true;
}
foreach (var btn in itemCategoryButtons)
{
btn.RectTransform.SizeChanged += () =>
{
if (btn.Frame.sprites == null || !btn.Frame.sprites.TryGetValue(GUIComponent.ComponentState.None, out var spriteList)) { return; }
var sprite = spriteList?.First();
if (sprite == null) { return; }
btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width)));
};
}
}
var mainFrame = new GUILayoutGroup(new RectTransform(Vector2.One, innerArea.RectTransform), childAnchor: Anchor.TopCenter)
{
RelativeSpacing = 0.02f,
Stretch = true,
@@ -105,10 +168,13 @@ namespace Barotrauma.Items.Components
Padding = Vector4.Zero,
AutoScaleVertical = true
};
itemFilterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), createClearButton: true);
itemFilterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), createClearButton: true)
{
OverflowClip = true
};
itemFilterBox.OnTextChanged += (textBox, text) =>
{
FilterEntities(text);
FilterEntities(selectedItemCategory, text);
return true;
};
filterArea.RectTransform.MaxSize = new Point(int.MaxValue, itemFilterBox.Rect.Height);
@@ -174,7 +240,7 @@ namespace Barotrauma.Items.Components
Stretch = true
};
amountTextMin = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);
new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);
amountInput = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1.0f), amountInputHolder.RectTransform), barSize: 0.1f, style: "GUISlider")
{
@@ -489,15 +555,37 @@ namespace Barotrauma.Items.Components
inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f);
}
var requiredItemPrefab = requiredItem.FirstMatchingPrefab;
var itemIcon = requiredItemPrefab.InventoryIcon ?? requiredItemPrefab.Sprite;
Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect;
itemIcon.Draw(
spriteBatch,
slotRect.Center.ToVector2(),
color: requiredItemPrefab.InventoryIconColor * 0.3f,
scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y));
var requiredItemPrefab = requiredItem.FirstMatchingPrefab;
float iconAlpha = 0.0f;
ItemPrefab requiredItemToDisplay;
int count = requiredItem.ItemPrefabs.Count();
if (count > 1)
{
float iconCycleSpeed = 0.5f / count;
float iconCycleT = (float)Timing.TotalTime * iconCycleSpeed;
int iconIndex = (int)(iconCycleT % requiredItem.ItemPrefabs.Count());
requiredItemToDisplay = requiredItem.ItemPrefabs.Skip(iconIndex).FirstOrDefault();
iconAlpha = Math.Min(Math.Abs(MathF.Sin(iconCycleT * MathHelper.Pi)) * 2.0f, 1.0f);
}
else
{
requiredItemToDisplay = requiredItem.ItemPrefabs.FirstOrDefault();
iconAlpha = 1.0f;
}
if (iconAlpha > 0.0f)
{
var itemIcon = requiredItemToDisplay.InventoryIcon ?? requiredItemToDisplay.Sprite;
itemIcon.Draw(
spriteBatch,
slotRect.Center.ToVector2(),
color: requiredItemToDisplay.InventoryIconColor * 0.3f * iconAlpha,
scale: Math.Min(slotRect.Width * 0.9f / itemIcon.size.X, slotRect.Height * 0.9f / itemIcon.size.Y));
}
if (missingCount > 1)
{
Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom);
@@ -552,9 +640,13 @@ namespace Barotrauma.Items.Components
}
toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖";
if (!requiredItemPrefab.Description.IsNullOrEmpty())
if (!requiredItem.OverrideDescription.IsNullOrEmpty())
{
toolTipText = '\n' + requiredItemPrefab.Description;
toolTipText += '\n' + requiredItem.OverrideDescription;
}
else if (!requiredItemPrefab.Description.IsNullOrEmpty())
{
toolTipText += '\n' + requiredItemPrefab.Description;
}
tooltip = new ToolTip { TargetElement = slotRect, Tooltip = toolTipText };
}
@@ -601,22 +693,21 @@ namespace Barotrauma.Items.Components
}
}
private bool FilterEntities(string filter)
private bool FilterEntities(MapEntityCategory? category, string filter)
{
if (string.IsNullOrWhiteSpace(filter))
foreach (GUIComponent child in itemList.Content.Children)
{
itemList.Content.Children.ForEach(c => c.Visible = true);
}
else
{
foreach (GUIComponent child in itemList.Content.Children)
{
FabricationRecipe recipe = child.UserData as FabricationRecipe;
if (recipe?.DisplayName == null) { continue; }
child.Visible = recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase);
}
}
FabricationRecipe recipe = child.UserData as FabricationRecipe;
if (recipe?.DisplayName == null) { continue; }
child.Visible =
(string.IsNullOrWhiteSpace(filter) || recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) &&
(!category.HasValue || recipe.TargetItem.Category.HasFlag(category.Value));
}
foreach (GUIButton btn in itemCategoryButtons)
{
btn.Selected = (MapEntityCategory?)btn.UserData == selectedItemCategory;
}
HideEmptyItemListCategories();
return true;
@@ -648,7 +739,7 @@ namespace Barotrauma.Items.Components
public bool ClearFilter()
{
FilterEntities("");
FilterEntities(selectedItemCategory, "");
itemList.UpdateScrollBarSize();
itemList.BarScroll = 0.0f;
itemFilterBox.Text = "";
@@ -737,6 +828,7 @@ namespace Barotrauma.Items.Components
TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUIStyle.Red : GUIStyle.Green, font: GUIStyle.SubHeadingFont)
{
AutoScaleHorizontal = true,
ToolTip = TextManager.Get("fabricatorrequiredskills.tooltip")
};
foreach (Skill skill in selectedItem.RequiredSkills)
{
@@ -125,18 +125,15 @@ namespace Barotrauma.Items.Components
{
public static MiniMapSettings Default = new MiniMapSettings
(
ignoreOutposts: false,
createHullElements: true,
elementColor: MiniMap.MiniMapBaseColor
);
public readonly bool IgnoreOutposts;
public readonly bool CreateHullElements;
public readonly Color ElementColor;
public MiniMapSettings(bool ignoreOutposts = false, bool createHullElements = false, Color? elementColor = null)
public MiniMapSettings(bool createHullElements = false, Color? elementColor = null)
{
IgnoreOutposts = ignoreOutposts;
CreateHullElements = createHullElements;
ElementColor = elementColor ?? MiniMap.MiniMapBaseColor;
}
@@ -403,7 +400,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; }
@@ -436,7 +434,11 @@ namespace Barotrauma.Items.Components
prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
submarineContainer.ClearChildren();
if (item.Submarine is null) { return; }
if (item.Submarine is null)
{
displayedSubs.Clear();
return;
}
scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center));
miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false };
@@ -444,8 +446,8 @@ namespace Barotrauma.Items.Components
ImmutableHashSet<Item> hullPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent<Door>() != null || it.GetComponent<Turret>() != null)).ToImmutableHashSet();
miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents);
IEnumerable<Item> electrialPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.GetComponent<Repairable>() != null);
electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electrialPointsOfInterest, out electricalMapComponents);
IEnumerable<Item> electricalPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.GetComponent<Repairable>() != null);
electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electricalPointsOfInterest, out electricalMapComponents);
Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>();
@@ -535,7 +537,7 @@ namespace Barotrauma.Items.Components
displayedSubs.Clear();
displayedSubs.Add(item.Submarine);
displayedSubs.AddRange(item.Submarine.DockedTo);
displayedSubs.AddRange(item.Submarine.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID));
subEntities = MapEntity.mapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList();
@@ -550,7 +552,7 @@ namespace Barotrauma.Items.Components
item.Submarine is { } itemSub &&
(
!displayedSubs.Contains(itemSub) || // current sub not displayed
itemSub.DockedTo.Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed
itemSub.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID).Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed
displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed
) ||
prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed
@@ -730,7 +732,7 @@ namespace Barotrauma.Items.Components
if (sprite != null && ShowHullIntegrity)
{
Vector2 spriteSize = sprite.size;
Rectangle worldBorders = item.Submarine.GetDockedBorders();
Rectangle worldBorders = item.Submarine.GetDockedBorders(allowDifferentTeam: false);
worldBorders.Location += item.Submarine.WorldPosition.ToPoint();
foreach (Gap gap in Gap.GapList)
{
@@ -914,7 +916,7 @@ namespace Barotrauma.Items.Components
}
RectangleF dockedBorders = item.Submarine.GetDockedBorders();
RectangleF dockedBorders = item.Submarine.GetDockedBorders(allowDifferentTeam: false);
dockedBorders.Location += item.Submarine.WorldPosition;
RectangleF parentRect = miniMapFrame.Rect;
@@ -1063,7 +1065,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
{
@@ -1302,7 +1306,7 @@ namespace Barotrauma.Items.Components
GameMain.Instance.GraphicsDevice.SetRenderTarget(rt);
GameMain.Instance.GraphicsDevice.Clear(Color.Transparent);
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
Rectangle worldBorders = sub.GetDockedBorders();
Rectangle worldBorders = sub.GetDockedBorders(allowDifferentTeam: false);
worldBorders.Location += sub.WorldPosition.ToPoint();
parentRect.Inflate(-inflate, -inflate);
@@ -1523,7 +1527,7 @@ namespace Barotrauma.Items.Components
Dictionary<MapEntity, MiniMapGUIComponent> pointsOfInterestCollection = new Dictionary<MapEntity, MiniMapGUIComponent>();
RectangleF worldBorders = sub.GetDockedBorders();
RectangleF worldBorders = sub.GetDockedBorders(allowDifferentTeam: false);
worldBorders.Location += sub.WorldPosition;
// create a container that has the same "aspect ratio" as the sub
@@ -1536,7 +1540,7 @@ namespace Barotrauma.Items.Components
GUIFrame hullContainer = new GUIFrame(new RectTransform(containerScale * elementPadding, parent.RectTransform, Anchor.Center), style: null);
ImmutableHashSet<Submarine> connectedSubs = sub.GetConnectedSubs().ToImmutableHashSet();
ImmutableHashSet<Submarine> connectedSubs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID).ToImmutableHashSet();
ImmutableArray<Hull> hullList = ImmutableArray<Hull>.Empty;
ImmutableDictionary<Hull, ImmutableArray<Hull>> combinedHulls = ImmutableDictionary<Hull, ImmutableArray<Hull>>.Empty;
@@ -1683,7 +1687,7 @@ namespace Barotrauma.Items.Components
bool IsPartofSub(MapEntity entity)
{
if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.HiddenInGame) { return false; }
return !settings.IgnoreOutposts || sub.IsEntityFoundOnThisSub(entity, true);
return sub.IsEntityFoundOnThisSub(entity, true);
}
bool IsStandaloneHull(Hull hull)
@@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components
private const float NearbyObjectUpdateInterval = 1.0f;
float nearbyObjectUpdateTimer;
private List<Submarine> connectedSubs = new List<Submarine>();
private readonly List<Submarine> connectedSubs = new List<Submarine>();
private const float ConnectedSubUpdateInterval = 1.0f;
float connectedSubUpdateTimer;
@@ -335,9 +335,11 @@ namespace Barotrauma.Items.Components
// Setup layout for nav terminal
if (isConnectedToSteering || RightLayout)
{
controlContainer.RectTransform.AbsoluteOffset = Point.Zero;
controlContainer.RectTransform.RelativeOffset = controlBoxOffset;
controlContainer.RectTransform.SetPosition(Anchor.TopRight);
sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest;
if (HasMineralScanner) { PreventMineralScannerOverlap(); }
sonarView.RectTransform.SetPosition(Anchor.CenterLeft);
sonarView.RectTransform.Resize(GUISizeCalculation);
GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize);
@@ -431,10 +433,11 @@ namespace Barotrauma.Items.Components
var mineralScannerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, zoomSlider.Parent.RectTransform.RelativeSize.Y), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null);
mineralScannerSwitch = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), mineralScannerFrame.RectTransform, Anchor.CenterLeft), string.Empty, style: "SwitchHorizontal")
{
Selected = UseMineralScanner,
OnClicked = (button, data) =>
{
useMineralScanner = !useMineralScanner;
button.Selected = useMineralScanner;
UseMineralScanner = !UseMineralScanner;
button.Selected = UseMineralScanner;
if (GameMain.Client != null)
{
unsentChanges = true;
@@ -496,12 +499,12 @@ namespace Barotrauma.Items.Components
{
if (transducer.Transducer.Item.Submarine == null) { continue; }
if (connectedSubs.Contains(transducer.Transducer.Item.Submarine)) { continue; }
connectedSubs = transducer.Transducer.Item.Submarine?.GetConnectedSubs();
connectedSubs.AddRange(transducer.Transducer.Item.Submarine.GetConnectedSubs());
}
}
else if (item.Submarine != null)
{
connectedSubs = item.Submarine?.GetConnectedSubs();
connectedSubs.AddRange(item.Submarine?.GetConnectedSubs());
}
connectedSubUpdateTimer = ConnectedSubUpdateInterval;
}
@@ -1032,7 +1035,7 @@ namespace Barotrauma.Items.Components
missionIndex++;
}
if (HasMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null &&
if (HasMineralScanner && UseMineralScanner && CurrentMode == Mode.Active && MineralClusters != null &&
(item.CurrentHull == null || !DetectSubmarineWalls))
{
foreach (var c in MineralClusters)
@@ -1311,7 +1314,6 @@ namespace Barotrauma.Items.Components
float worldPingRadiusSqr = worldPingRadius * worldPingRadius;
disruptedDirections.Clear();
if (Level.Loaded == null) { return; }
for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
{
@@ -1513,9 +1515,10 @@ namespace Barotrauma.Items.Components
}
}
foreach (Item item in Item.ItemList)
foreach (Item item in Item.SonarVisibleItems)
{
if (item.CurrentHull == null && item.Prefab.SonarSize > 0.0f)
System.Diagnostics.Debug.Assert(item.Prefab.SonarSize > 0.0f);
if (item.CurrentHull == null)
{
float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared();
if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr)
@@ -1923,7 +1926,7 @@ namespace Barotrauma.Items.Components
float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
}
msg.WriteBoolean(useMineralScanner);
msg.WriteBoolean(UseMineralScanner);
}
}
@@ -1935,7 +1938,7 @@ namespace Barotrauma.Items.Components
float zoomT = 1.0f;
bool directionalPing = useDirectionalPing;
float directionT = 0.0f;
bool mineralScanner = useMineralScanner;
bool mineralScanner = UseMineralScanner;
if (isActive)
{
zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
@@ -1966,7 +1969,7 @@ namespace Barotrauma.Items.Components
pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
}
useDirectionalPing = directionalModeSwitch.Selected = directionalPing;
useMineralScanner = mineralScanner;
UseMineralScanner = mineralScanner;
if (mineralScannerSwitch != null)
{
mineralScannerSwitch.Selected = mineralScanner;
@@ -1983,7 +1986,7 @@ namespace Barotrauma.Items.Components
directionalModeSwitch.Selected = useDirectionalPing;
if (mineralScannerSwitch != null)
{
mineralScannerSwitch.Selected = useMineralScanner;
mineralScannerSwitch.Selected = UseMineralScanner;
}
}
}
@@ -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);
@@ -21,7 +21,8 @@ namespace Barotrauma.Items.Components
public float FlashTimer { get; private set; }
public static Wire DraggingConnected { get; private set; }
public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Rectangle dragArea, Character character)
public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Rectangle dragArea, Character character,
out (Vector2 tooltipPos, LocalizedString text) tooltip)
{
if (DraggingConnected?.Item?.Removed ?? true)
{
@@ -64,6 +65,8 @@ namespace Barotrauma.Items.Components
}
}
tooltip = (Vector2.Zero, string.Empty);
//two passes: first the connector, then the wires to get the wires to render in front
for (int i = 0; i < 2; i++)
{
@@ -97,6 +100,42 @@ namespace Barotrauma.Items.Components
}
}
Vector2 position = c.IsOutput ? rightPos : leftPos;
Color highlightColor = Color.Transparent;
if (ConnectionPanel.ShouldDebugDrawWiring)
{
if (c.IsPower)
{
highlightColor = VisualizeSignal(0.0f, highlightColor, Color.Red);
}
else
{
highlightColor = VisualizeSignal(c.LastReceivedSignal.TimeSinceCreated, highlightColor, Color.LightGreen);
highlightColor = VisualizeSignal(c.LastSentSignal.TimeSinceCreated, highlightColor, Color.Orange);
}
bool mouseOn = Vector2.DistanceSquared(position, PlayerInput.MousePosition) < MathUtils.Pow2(35 * GUI.Scale);
LocalizedString toolTipText = c.GetToolTip();
if (mouseOn) { tooltip = (position, toolTipText); }
if (!toolTipText.IsNullOrEmpty())
{
var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite;
glowSprite.Draw(spriteBatch, position, highlightColor, glowSprite.size / 2,
scale: 45.0f / glowSprite.size.X * panel.Scale);
}
}
static Color VisualizeSignal(double timeSinceCreated, Color defaultColor, Color color)
{
if (timeSinceCreated < 1.0f)
{
float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * 10.0f) + 3.0f) / 4.0f;
Color targetColor = Color.Lerp(defaultColor, color, pulseAmount);
return Color.Lerp(targetColor, defaultColor, (float)timeSinceCreated);
}
return defaultColor;
}
//outputs are drawn at the right side of the panel, inputs at the left
if (c.IsOutput)
{
@@ -127,7 +166,6 @@ namespace Barotrauma.Items.Components
}
}
if (DraggingConnected != null)
{
if (mouseInRect)
@@ -225,7 +263,9 @@ namespace Barotrauma.Items.Components
GUI.DrawString(spriteBatch, labelPos, text, GUIStyle.TextColorBright, font: GUIStyle.SmallFont);
float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale;
connectionSprite.Draw(spriteBatch, position, scale: connectorSpriteScale);
}
private void DrawWires(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 wirePosition, bool mouseIn, Wire equippedWire, float wireInterval)
@@ -307,6 +347,63 @@ namespace Barotrauma.Items.Components
FlashTimer -= deltaTime;
}
private (string signal, LocalizedString tooltip) lastSignalToolTip;
private (int powerValue, LocalizedString tooltip) lastPowerToolTip;
private LocalizedString GetToolTip()
{
if (LastReceivedSignal.TimeSinceCreated < 1.0f)
{
return getSignalTooltip(LastReceivedSignal, "receivedsignal");
}
else if (LastSentSignal.TimeSinceCreated < 1.0f)
{
return getSignalTooltip(LastSentSignal, "sentsignal");
}
LocalizedString getSignalTooltip(Signal signal, string textTag)
{
if (lastSignalToolTip.signal == signal.value && !lastSignalToolTip.tooltip.IsNullOrEmpty()) { return lastSignalToolTip.tooltip; }
lastSignalToolTip = (signal.value, TextManager.GetWithVariable(textTag, "[signal]", signal.value));
return lastSignalToolTip.tooltip;
}
if (IsPower)
{
if (item.GetComponent<Powered>() is Powered powered)
{
if (IsOutput)
{
if (powered.CurrPowerConsumption < 0)
{
return getPowerTooltip(-(int)powered.CurrPowerConsumption, "reactoroutput");
}
else if (powered is PowerTransfer || powered is PowerContainer)
{
return getPowerTooltip((int)(Grid?.Power ?? 0), "reactoroutput");
}
}
else if (!IsOutput)
{
float powerConsumption = powered.GetCurrentPowerConsumption(this);
if (!MathUtils.NearlyEqual((int)powerConsumption, 0.0f))
{
return getPowerTooltip((int)powerConsumption, "reactorload");
}
}
}
}
LocalizedString getPowerTooltip(int powerValue, string textTag)
{
if (lastPowerToolTip.powerValue == powerValue && !lastPowerToolTip.tooltip.IsNullOrEmpty()) { return lastPowerToolTip.tooltip; }
lastPowerToolTip = (powerValue, TextManager.GetWithVariable(textTag, "[kw]", powerValue.ToString()));
return lastPowerToolTip.tooltip;
}
return null;
}
private static void DrawWire(SpriteBatch spriteBatch, Wire wire, Vector2 end, Vector2 start, Wire equippedWire, ConnectionPanel panel, LocalizedString label)
{
int textX = (int)start.X;
@@ -10,6 +10,10 @@ namespace Barotrauma.Items.Components
{
partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable
{
public static bool DebugWiringMode;
public static double DebugWiringEnabledUntil;
public static bool ShouldDebugDrawWiring => DebugWiringMode || Timing.TotalTimeUnpaused < DebugWiringEnabledUntil;
//how long the rewiring sound plays after doing changes to the wiring
const float RewireSoundDuration = 5.0f;
@@ -120,12 +124,15 @@ namespace Barotrauma.Items.Components
if (user != Character.Controlled || user == null) { return; }
HighlightedWire = null;
Connection.DrawConnections(spriteBatch, this, dragArea.Rect, user);
Connection.DrawConnections(spriteBatch, this, dragArea.Rect, user, out (Vector2 tooltipPos, LocalizedString text) tooltip);
foreach (UISprite sprite in GUIStyle.GetComponentStyle("ConnectionPanelFront").Sprites[GUIComponent.ComponentState.None])
{
sprite.Draw(spriteBatch, GuiFrame.Rect, Color.White, SpriteEffects.None);
}
if (!tooltip.text.IsNullOrEmpty())
{
GUIComponent.DrawToolTip(spriteBatch, tooltip.text, tooltip.tooltipPos);
}
}
private void CheckForLabelOverlap()
@@ -250,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)
@@ -337,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)
{
@@ -5,7 +5,6 @@ using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -119,6 +118,13 @@ namespace Barotrauma.Items.Components
get { return sectionExtents; }
}
public readonly record struct VisualSignal(
float TimeSent,
Color Color,
int Direction);
private VisualSignal lastReceivedSignal;
public static Wire DraggingWire
{
get => draggingWire;
@@ -126,13 +132,11 @@ namespace Barotrauma.Items.Components
public static Sprite ExtractWireSprite(ContentXElement element)
{
if (defaultWireSprite == null)
{
defaultWireSprite = new Sprite("Content/Items/Electricity/signalcomp.png", new Rectangle(970, 47, 14, 16), new Vector2(0.5f, 0.5f))
defaultWireSprite ??=
new Sprite("Content/Items/Electricity/signalcomp.png", new Rectangle(970, 47, 14, 16), new Vector2(0.5f, 0.5f))
{
Depth = 0.855f
};
}
Sprite overrideSprite = null;
foreach (var subElement in element.Elements())
@@ -153,6 +157,35 @@ namespace Barotrauma.Items.Components
if (wireSprite != defaultWireSprite) { overrideSprite = wireSprite; }
}
public void RegisterSignal(Signal signal, Connection source)
{
lastReceivedSignal = new VisualSignal(
(float)Timing.TotalTimeUnpaused,
GetSignalColor(signal),
Direction: source == connections[0] ? 1 : -1);
}
private static readonly Color[] dataSignalColors = new Color[] { Color.White, Color.LightBlue, Color.CornflowerBlue, Color.Blue, Color.BlueViolet, Color.Violet };
private static Color GetSignalColor(Signal signal)
{
if (signal.value == "0")
{
return Color.Red;
}
else if (signal.value == "1")
{
return Color.LightGreen;
}
else if (float.TryParse(signal.value, out float floatValue))
{
//convert numeric values to a color (guessing the value might be somewhere in the range of 0-200)
//so a player with a keen eye can get some info out of the color of the signal
return ToolBox.GradientLerp(Math.Abs(floatValue / 200.0f), dataSignalColors);
}
return Color.LightBlue;
}
public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1)
{
Draw(spriteBatch, editing, Vector2.Zero, itemDepth);
@@ -166,20 +199,7 @@ namespace Barotrauma.Items.Components
return;
}
Vector2 drawOffset = Vector2.Zero;
Submarine sub = item.Submarine;
if (IsActive && sub == null) // currently being rewired, we need to get the sub from the connections in case the wire has been taken outside
{
if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; }
if (connections[1] != null && connections[1].Item.Submarine != null) { sub = connections[1].Item.Submarine; }
}
if (sub != null)
{
drawOffset = sub.DrawPosition + sub.HiddenSubPosition;
}
drawOffset += offset;
Vector2 drawOffset = GetDrawOffset() + offset;
float baseDepth = UseSpriteDepth ? item.SpriteDepth : wireSprite.Depth;
float depth = item.IsSelected ? 0.0f : SubEditorScreen.IsWiringMode() ? 0.02f : baseDepth + (item.ID % 100) * 0.000001f;// item.GetDrawDepth(wireSprite.Depth, wireSprite);
@@ -188,7 +208,9 @@ namespace Barotrauma.Items.Components
{
foreach (WireSection section in sections)
{
section.Draw(spriteBatch, wireSprite, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f);
section.Draw(spriteBatch, wireSprite,
Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor,
drawOffset, depth + 0.00001f, Width * 2.0f);
}
}
else if (item.IsSelected)
@@ -270,6 +292,12 @@ namespace Barotrauma.Items.Components
}
}
if (ConnectionPanel.ShouldDebugDrawWiring)
{
DebugDraw(spriteBatch, alpha: 0.2f);
}
if (!editing || !GameMain.SubEditorScreen.WiringMode) { return; }
for (int i = 0; i < nodes.Count; i++)
@@ -295,6 +323,102 @@ namespace Barotrauma.Items.Components
}
}
public void DebugDraw(SpriteBatch spriteBatch, float alpha = 1.0f)
{
if (sections.Count == 0 || Hidden)
{
return;
}
const float PowerPulseSpeedLow = 5.0f;
const float PowerPulseSpeedHigh = 10.0f;
const float PowerHighlightScaleLow = 1.5f;
const float PowerHighlightScaleHigh = 2.5f;
const float SignalIndicatorInterval = 15.0f;
const float SignalIndicatorSpeed = 100.0f;
Vector2 drawOffset = GetDrawOffset();
Color currentHighlightColor = Color.Transparent;
float highlightScale = 0.0f;
if (connections[0] != null && connections[1] != null)
{
float voltage = Math.Max(GetVoltage(0), GetVoltage(1));
float GetVoltage(int connectionIndex)
{
var connection1 = connections[connectionIndex];
var connection2 = connections[1 - connectionIndex];
if (connection1.IsOutput && connection1.Grid is { Power: > 0.01f } grid1)
{
if (connection2.Item.GetComponent<Powered>() is Powered powered &&
(powered.GetCurrentPowerConsumption(connection2) > 0 || powered is PowerTransfer))
{
return grid1.Voltage;
}
}
return 0.0f;
}
if (voltage > 0.0f)
{
//pulse faster when there's overvoltage
float pulseSpeed = voltage > 1.2f ? PowerPulseSpeedHigh : PowerPulseSpeedLow;
float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * pulseSpeed) + 1.5f) / 2.5f;
voltage = Math.Min(voltage, 1.0f);
highlightScale = MathHelper.Lerp(PowerHighlightScaleLow, PowerHighlightScaleHigh, voltage);
currentHighlightColor = Color.Red * voltage * pulseAmount;
}
}
if (highlightScale > 0.0f)
{
foreach (WireSection section in sections)
{
section.Draw(spriteBatch, wireSprite, currentHighlightColor * alpha, drawOffset, 0.0f, Width * highlightScale);
}
}
float signalDuration = (float)Timing.TotalTimeUnpaused - lastReceivedSignal.TimeSent;
if (ConnectionPanel.ShouldDebugDrawWiring && signalDuration < 1.0f)
{
//make some wires "off sync" so it's easier to differentiate signals on overlapping wires
float offset = item.ID % 2 == 1 ? SignalIndicatorInterval / 2 : 0.0f;
float signalProgress = ((float)(Timing.TotalTimeUnpaused * SignalIndicatorSpeed + offset) % SignalIndicatorInterval) * lastReceivedSignal.Direction;
foreach (WireSection section in sections)
{
for (float x = 0; x < section.Length; x += SignalIndicatorInterval)
{
Vector2 dir = (section.End - section.Start) / section.Length;
float posOnSection = x + signalProgress;
if (posOnSection < 0 || posOnSection > section.Length) { continue; }
Vector2 signalPos = section.Start + drawOffset + dir * posOnSection;
float a = 1.0f - Vector2.Distance(Screen.Selected.Cam.WorldViewCenter, signalPos) / 500.0f;
if (a < 0) { continue; }
signalPos.Y = -signalPos.Y;
GUI.DrawRectangle(spriteBatch, signalPos - Vector2.One * 2.5f, Vector2.One * 5, lastReceivedSignal.Color * a * (1.0f - signalDuration) * alpha, isFilled: true);
}
}
}
}
private Vector2 GetDrawOffset()
{
Submarine sub = item.Submarine;
if (IsActive && sub == null) // currently being rewired, we need to get the sub from the connections in case the wire has been taken outside
{
if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; }
if (connections[1] != null && connections[1].Item.Submarine != null) { sub = connections[1].Item.Submarine; }
}
if (sub == null)
{
return Vector2.Zero;
}
else
{
return sub.DrawPosition + sub.HiddenSubPosition;
}
}
private void DrawHangingWire(SpriteBatch spriteBatch, Vector2 start, float depth)
{
float angle = (float)Math.Sin(GameMain.GameScreen.GameTime * 2.0f + item.ID) * 0.2f;
@@ -46,6 +46,13 @@ namespace Barotrauma.Items.Components
private set;
}
[Serialize(false, IsPropertySaveable.No)]
public bool DebugWiring
{
get;
private set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool ShowDeadCharacters
{
@@ -111,6 +118,11 @@ namespace Barotrauma.Items.Components
refEntity = item;
}
if (equipper != null && equipper == Character.Controlled && DebugWiring)
{
ConnectionPanel.DebugWiringEnabledUntil = Timing.TotalTimeUnpaused + 0.5;
}
thermalEffectState += deltaTime;
thermalEffectState %= 10000.0f;
@@ -153,6 +165,11 @@ namespace Barotrauma.Items.Components
IsActive = false;
}
public override void Drop(Character dropper, bool setTransform = true)
{
Unequip(dropper);
}
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
if (character == null) { return; }
@@ -1,4 +1,5 @@
using Barotrauma.Networking;
using Barotrauma.Lights;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Collision;
using Microsoft.Xna.Framework;
@@ -10,6 +11,8 @@ namespace Barotrauma.Items.Components
{
private GUIMessageBox autodockingVerification;
private readonly ConvexHull[] convexHulls = new ConvexHull[2];
public Vector2 DrawSize
{
//use the extents of the item as the draw size
@@ -109,6 +112,15 @@ namespace Barotrauma.Items.Components
}
}
partial void RemoveConvexHulls()
{
for (int i = 0; i < convexHulls.Length; i++)
{
convexHulls[i]?.Remove();
convexHulls[i] = null;
}
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
bool isDocked = msg.ReadBoolean();
@@ -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,7 +1411,7 @@ 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;
@@ -1460,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:
@@ -9,15 +9,17 @@ namespace Barotrauma
{
private LightSource lightSource;
partial void UpdateProjSpecific(float growModifier)
private float particleTimer;
partial void UpdateProjSpecific(float growModifier, float deltaTime)
{
if (this is DummyFireSource)
{
EmitParticles(size, WorldPosition, hull, growModifier, null);
EmitParticles(size, WorldPosition, deltaTime, hull, growModifier, null);
}
else
{
EmitParticles(size, WorldPosition, hull, growModifier, OnChangeHull);
EmitParticles(size, WorldPosition, deltaTime, hull, growModifier, OnChangeHull);
}
lightSource.Color = new Color(1.0f, 0.45f, 0.3f) * Rand.Range(0.8f, 1.0f);
@@ -25,23 +27,29 @@ namespace Barotrauma
if (Vector2.DistanceSquared(lightSource.Position, position) > 5.0f) { lightSource.Position = position + Vector2.UnitY * 30.0f; }
}
public static void EmitParticles(Vector2 size, Vector2 worldPosition, Hull hull, float growModifier, Particle.OnChangeHullHandler onChangeHull = null)
public void EmitParticles(Vector2 size, Vector2 worldPosition, float deltaTime, Hull hull, float growModifier, Particle.OnChangeHullHandler onChangeHull = null)
{
float particleCount = Rand.Range(0.0f, size.X / 50.0f);
var particlePrefab = ParticleManager.FindPrefab("flame");
if (particlePrefab == null) { return; }
for (int i = 0; i < particleCount; i++)
float particlesPerSecond = MathHelper.Clamp(size.X / 2.0f, 10.0f, 200.0f);
float particleInterval = 1.0f / particlesPerSecond;
particleTimer += deltaTime;
while (particleTimer > particleInterval)
{
particleTimer -= particleInterval;
Vector2 particlePos = new Vector2(
worldPosition.X + Rand.Range(0.0f, size.X),
Rand.Range(worldPosition.Y - size.Y, worldPosition.Y + 20.0f));
worldPosition.Y - size.Y + particlePrefab.CollisionRadius);
Vector2 particleVel = new Vector2(
particlePos.X - (worldPosition.X + size.X / 2.0f),
Math.Max((float)Math.Sqrt(size.X) * Rand.Range(0.0f, 15.0f) * growModifier, 0.0f));
particleVel.X = MathHelper.Clamp(particleVel.X, -200.0f, 200.0f);
var particle = GameMain.ParticleManager.CreateParticle("flame",
var particle = GameMain.ParticleManager.CreateParticle(particlePrefab,
particlePos, particleVel, 0.0f, hull);
if (particle == null) { continue; }
@@ -54,7 +62,7 @@ namespace Barotrauma
if (Rand.Int(5) == 1)
{
var smokeParticle = GameMain.ParticleManager.CreateParticle("smoke",
particlePos, new Vector2(particleVel.X, particleVel.Y * 0.1f), 0.0f, hull);
particlePos, new Vector2(particleVel.X, particleVel.Y * 0.1f), 0.0f, hull);
if (smokeParticle != null)
{
@@ -27,7 +27,7 @@ namespace Barotrauma
{
foreach (var edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection))
{
intersectionFound = true;
particlePos = intersection;
@@ -93,13 +93,16 @@ namespace Barotrauma.Lights
private readonly int thickness;
public bool IsExteriorWall;
public VertexPositionColor[] ShadowVertices { get; private set; }
public VertexPositionTexture[] PenumbraVertices { get; private set; }
public int ShadowVertexCount { get; private set; }
public int PenumbraVertexCount { get; private set; }
/// <summary>
/// Overrides the maximum distance a LOS vertex can be moved to make it align with a nearby LOS segment
/// </summary>
public float? MaxMergeLosVerticesDist;
private readonly HashSet<ConvexHull> overlappingHulls = new HashSet<ConvexHull>();
public MapEntity ParentEntity { get; private set; }
@@ -130,7 +133,7 @@ namespace Barotrauma.Lights
public Rectangle BoundingBox { get; private set; }
public ConvexHull(Rectangle rect, bool? isHorizontal, MapEntity parent)
public ConvexHull(Rectangle rect, bool isHorizontal, MapEntity parent)
{
shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice)
{
@@ -150,15 +153,15 @@ namespace Barotrauma.Lights
BoundingBox = rect;
this.isHorizontal = isHorizontal ?? BoundingBox.Width > BoundingBox.Height;
this.isHorizontal = isHorizontal;
if (ParentEntity is Structure structure)
{
System.Diagnostics.Debug.Assert(!structure.Removed);
Debug.Assert(!structure.Removed);
isHorizontal = structure.IsHorizontal;
}
else if (ParentEntity is Item item)
{
System.Diagnostics.Debug.Assert(!item.Removed);
Debug.Assert(!item.Removed);
var door = item.GetComponent<Door>();
if (door != null) { isHorizontal = door.IsHorizontal; }
}
@@ -205,44 +208,97 @@ namespace Barotrauma.Lights
{
if (ch == this) { return; }
//hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces)
float mergeDist = MathHelper.Clamp(ch.thickness * 0.55f, 16, 512);
mergeDist = Math.Min(mergeDist, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) / 2);
//merge dist in the direction parallel to the segment
//(e.g. how far up/down we can stretch a vertical segment)
float mergeDistParallel = MathHelper.Clamp(ch.thickness * 0.65f, 16, 512);
if (MaxMergeLosVerticesDist.HasValue)
{
mergeDistParallel = Math.Max(mergeDistParallel, MaxMergeLosVerticesDist.Value);
}
else
{
Rectangle inflatedAABB = ch.BoundingBox;
inflatedAABB.Inflate(2, 2);
//if this los segment isn't touching the other's bounding box,
//don't extend the segment by more than 50% of it's length
if (!inflatedAABB.Contains(losVertices[0].Pos) &&
!inflatedAABB.Contains(losVertices[1].Pos))
{
mergeDistParallel = Math.Min(mergeDistParallel, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) * 0.5f);
}
}
//merge dist in the direction perpendicular to the segment
//(e.g. how far right/left we can stretch a vertical segment)
//do not allow more than ~half of the thickness, because that'd make the segment go outside the convex hull
float mergeDistPerpendicular = Math.Min(mergeDistParallel, thickness * 0.35f);
float mergeDistSqr = mergeDist * mergeDist;
Vector2 center = (losVertices[0].Pos + losVertices[1].Pos) / 2;
bool changed = false;
for (int i = 0; i < losVertices.Length; i++)
{
//find the closest point on the other convex hull segment
Vector2 segmentDir = Vector2.Normalize(losVertices[i].Pos - center);
//check if the closest point on the other convex hull segment is close enough, disregarding any offsets
//otherwise we might end up moving the vertex too much if we stretch it to an already-offset segment
if (!isCloseEnough(
MathUtils.GetClosestPointOnLineSegment(ch.losVertices[0].Pos, ch.losVertices[1].Pos, losVertices[i].Pos),
losVertices[i].Pos))
{
continue;
}
//check the offset position of the segment next
Vector2 closest = MathUtils.GetClosestPointOnLineSegment(
ch.losVertices[0].Pos + ch.losOffsets[0],
ch.losVertices[1].Pos + ch.losOffsets[1],
losVertices[i].Pos);
if (Vector2.DistanceSquared(closest, losVertices[i].Pos) > mergeDistSqr) { continue; }
if (!isCloseEnough(closest, losVertices[i].Pos)) { continue; }
//find where the segments would intersect if they had infinite length
// if it's close to the closest point, let's use that instead to keep
// the direction of the segment unchanged (i.e. vertical segment stays vertical)
if (MathUtils.GetLineIntersection(
ch.losVertices[0].Pos + ch.losOffsets[0],
ch.losVertices[1].Pos + ch.losOffsets[1],
losVertices[0].Pos,
losVertices[1].Pos,
out Vector2 intersection))
ch.losVertices[0].Pos + ch.losOffsets[0], ch.losVertices[1].Pos + ch.losOffsets[1],
losVertices[0].Pos, losVertices[1].Pos,
areLinesInfinite: true, out Vector2 intersection) &&
//the intersection needs to be outwards from the vertex we're checking
Vector2.Dot(segmentDir, intersection - losVertices[i].Pos) > 0 &&
//the intersection needs to be close enough to the default position of the vertex and the closest point
//(we don't want to merge the segments somewhere close to infinity!)
(Vector2.DistanceSquared(intersection, losVertices[i].Pos) < mergeDistParallel * mergeDistParallel ||
Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f))
{
if (Vector2.DistanceSquared(intersection, losVertices[i].Pos) < mergeDistSqr ||
Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f)
{
closest = intersection;
}
closest = intersection;
}
//don't move the vertices of the segment too close to each other
if (Vector2.DistanceSquared(losVertices[1 - i].Pos + losOffsets[1 - i], closest) < mergeDistPerpendicular * mergeDistPerpendicular)
{
continue;
}
losOffsets[i] = closest - losVertices[i].Pos;
overlappingHulls.Add(ch);
ch.overlappingHulls.Add(this);
changed = true;
bool isCloseEnough(Vector2 closest, Vector2 vertex)
{
float dist = Vector2.Distance(closest, vertex);
if (dist < 0.001f) { return true; }
if (dist > mergeDistParallel) { return false; }
Vector2 closestDir = (closest - vertex) / dist;
float dot = Math.Abs(Vector2.Dot(segmentDir, closestDir));
float distAlongAxis = dist * dot;
if (distAlongAxis > mergeDistParallel) { return false; }
float distPerpendicular = dist * (1.0f - dot);
if (distPerpendicular > mergeDistPerpendicular) { return false; }
return true;
}
}
if (changed && refreshOtherOverlappingHulls)
{
@@ -253,6 +309,13 @@ namespace Barotrauma.Lights
}
}
public bool LosIntersects(Vector2 pos1, Vector2 pos2)
{
return MathUtils.LineSegmentsIntersect(
losVertices[0].Pos + losOffsets[0], losVertices[1].Pos + losOffsets[1],
pos1, pos2);
}
public void Rotate(Vector2 origin, float amount)
{
Matrix rotationMatrix =
@@ -347,6 +410,7 @@ namespace Barotrauma.Lights
for (int i = 0; i < 2; i++)
{
losVertices[i] = new SegmentPoint(losPoints[i], this);
losOffsets[i] = Vector2.Zero;
}
overlappingHulls.Clear();
@@ -612,8 +676,11 @@ namespace Barotrauma.Lights
vertexPos0 += ParentEntity.Submarine.DrawPosition;
vertexPos1 += ParentEntity.Submarine.DrawPosition;
}
Vector2 viewTargetPos = LightManager.ViewTarget.WorldPosition;
float alpha = IsSegmentFacing(vertexPos0, vertexPos1, viewTargetPos) ? 1.0f : 0.5f;
float alpha = 1.0f;
if (LightManager.ViewTarget != null)
{
alpha = IsSegmentFacing(vertexPos0, vertexPos1, LightManager.ViewTarget.WorldPosition) ? 1.0f : 0.5f;
}
vertexPos0.Y = -vertexPos0.Y;
vertexPos1.Y = -vertexPos1.Y;
GUI.DrawLine(spriteBatch, vertexPos0, vertexPos1, color * alpha, width: width);
@@ -189,7 +189,7 @@ namespace Barotrauma.Lights
}
}
private class RayCastTask
private sealed class RayCastTask
{
public LightSource LightSource;
public Vector2 DrawPos;
@@ -298,7 +298,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
@@ -477,6 +478,17 @@ namespace Barotrauma.Lights
light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount);
}
if (ConnectionPanel.ShouldDebugDrawWiring)
{
foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.mapEntityList))
{
if (e is Item item && item.GetComponent<Wire>() is Wire wire)
{
wire.DebugDraw(spriteBatch, alpha: 0.4f);
}
}
}
lightEffect.World = transform;
GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.Additive);
@@ -686,19 +698,42 @@ namespace Barotrauma.Lights
if (LosEnabled && LosMode != LosMode.None && ViewTarget != null)
{
Vector2 pos = ViewTarget.DrawPosition;
if (ViewTarget is Character character &&
bool centeredOnHead = false;
if (ViewTarget is Character character &&
character.AnimController?.GetLimb(LimbType.Head) is Limb head &&
!head.IsSevered && !head.Removed)
{
pos = head.body.DrawPosition;
centeredOnHead = true;
}
Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height);
Matrix shadowTransform = cam.ShaderTransform
* Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f;
var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget.Submarine);
//make sure the head isn't peeking through any LOS segments, and if it is,
//center the LOS on the character's collider instead
if (centeredOnHead)
{
foreach (var ch in convexHulls)
{
Vector2 currentViewPos = pos;
Vector2 defaultViewPos = ViewTarget.DrawPosition;
if (ch.ParentEntity?.Submarine != null)
{
defaultViewPos -= ch.ParentEntity.Submarine.DrawPosition;
currentViewPos -= ch.ParentEntity.Submarine.DrawPosition;
}
//check if a line from the character's collider to the head intersects with the los segment (= head poking through it)
if (ch.LosIntersects(defaultViewPos, currentViewPos))
{
pos = ViewTarget.DrawPosition;
}
}
}
if (convexHulls != null)
{
List<VertexPositionColor> shadowVerts = new List<VertexPositionColor>();
@@ -706,7 +741,6 @@ namespace Barotrauma.Lights
foreach (ConvexHull convexHull in convexHulls)
{
if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; }
if (LosMode == LosMode.BlockOutsideView && !convexHull.IsExteriorWall) { continue; };
Vector2 relativeLightPos = pos;
if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; }
@@ -744,13 +778,14 @@ namespace Barotrauma.Lights
public void DebugDrawLos(SpriteBatch spriteBatch, Camera cam)
{
if (ViewTarget == null) { return; }
Vector2 pos = ViewTarget?.Position ?? cam.Position;
spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform);
var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget?.Submarine);
var convexHulls = ConvexHull.GetHullsInRange(pos, cam.WorldView.Width * 0.75f, ViewTarget?.Submarine);
Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height);
foreach (ConvexHull convexHull in convexHulls)
{
if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; }
if (convexHull.ParentEntity is Structure { CastShadow: false }) { continue; }
convexHull.DebugDraw(spriteBatch);
}
spriteBatch.End();
@@ -53,10 +53,6 @@ namespace Barotrauma.Lights
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)]
public float Rotation { get; set; }
[Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+
" Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")]
public bool Directional { get; set; }
public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation)));
private float flicker;
@@ -233,6 +229,7 @@ namespace Barotrauma.Lights
//do we need to recalculate the vertices of the light volume
private bool needsRecalculation;
private bool needsRecalculationWhenUpToDate;
public bool NeedsRecalculation
{
get { return needsRecalculation; }
@@ -246,9 +243,15 @@ namespace Barotrauma.Lights
}
}
needsRecalculation = value;
if (needsRecalculation && state != LightVertexState.UpToDate)
{
//if we're currently recalculating light vertices, mark that we need to recalculate them again after it's done
needsRecalculationWhenUpToDate = true;
}
}
}
//when were the vertices of the light volume last calculated
public float LastRecalculationTime { get; private set; }
@@ -309,8 +312,6 @@ namespace Barotrauma.Lights
if (Math.Abs(value - rotation) < 0.001f) { return; }
rotation = value;
dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation));
if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null)
{
return;
@@ -321,8 +322,6 @@ namespace Barotrauma.Lights
}
}
private Vector2 dir = Vector2.UnitX;
private Vector2 _spriteScale = Vector2.One;
public Vector2 SpriteScale
@@ -396,6 +395,8 @@ namespace Barotrauma.Lights
public float Priority;
public float PriorityMultiplier = 1.0f;
private Vector2 lightTextureTargetSize;
public Vector2 LightTextureTargetSize
@@ -444,7 +445,7 @@ namespace Barotrauma.Lights
public bool Enabled = true;
private readonly ISerializableEntity conditionalTarget;
private readonly PropertyConditional.LogicalOperatorType logicalOperator;
private readonly PropertyConditional.Comparison comparison;
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null)
@@ -452,8 +453,11 @@ namespace Barotrauma.Lights
{
lightSourceParams = new LightSourceParams(element);
CastShadows = element.GetAttributeBool("castshadows", true);
logicalOperator = element.GetAttributeEnum(nameof(logicalOperator),
element.GetAttributeEnum("comparison", logicalOperator));
string comparison = element.GetAttributeString("comparison", null);
if (comparison != null)
{
Enum.TryParse(comparison, ignoreCase: true, out this.comparison);
}
if (lightSourceParams.DeformableLightSpriteElement != null)
{
@@ -466,7 +470,13 @@ namespace Barotrauma.Lights
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "conditional":
conditionals.AddRange(PropertyConditional.FromXElement(subElement));
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
conditionals.Add(new PropertyConditional(attribute));
}
}
break;
}
}
@@ -529,32 +539,11 @@ namespace Barotrauma.Lights
var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub);
if (fullChList == null) { return; }
//used to check whether the lightsource hits the target hull if the light is directional
Vector2 ray = new Vector2(dir.X, -dir.Y) * TextureRange;
Vector2 normal = new Vector2(-ray.Y, ray.X);
chList.List.Clear();
foreach (var convexHull in fullChList.List)
{
if (!convexHull.Enabled) { continue; }
if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; }
if (lightSourceParams.Directional && false)
{
Rectangle bounds = convexHull.BoundingBox;
//invert because GetLineRectangleIntersection uses the messed up rects that start from top-left
bounds.Y -= bounds.Height;
//the ray can't hit if
// center is in the opposite direction from the ray (cheapest check first)
if (Vector2.Dot(ray, convexHull.BoundingBox.Center.ToVector2() - lightPos) <= 0 &&
/*ray doesn't hit the convex hull*/
!MathUtils.GetLineRectangleIntersection(lightPos, lightPos + ray, bounds, out _) &&
/*normal vectors of the ray don't hit the convex hull */
!MathUtils.GetLineRectangleIntersection(lightPos + normal, lightPos - normal, bounds, out _))
{
continue;
}
}
chList.List.Add(convexHull);
}
chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch));
@@ -704,7 +693,7 @@ namespace Barotrauma.Lights
lock (mutex)
{
hull.RefreshWorldPositions();
hull.GetVisibleSegments(drawPos, visibleSegments);
hull.GetVisibleSegments(drawPos, visibleSegments);
foreach (var visibleSegment in visibleSegments)
{
if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null)
@@ -713,7 +702,6 @@ namespace Barotrauma.Lights
}
}
}
}
}
foreach (ConvexHull hull in chList.List)
@@ -812,7 +800,7 @@ namespace Barotrauma.Lights
}
else
{
intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection);
intersects = MathUtils.GetLineSegmentIntersection(p1a, p1b, p2a, p2b, out intersection);
}
if (intersects)
@@ -1006,7 +994,7 @@ namespace Barotrauma.Lights
}
else
{
intersects = MathUtils.GetLineIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection);
intersects = MathUtils.GetLineSegmentIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection);
}
if (intersects)
@@ -1041,9 +1029,11 @@ namespace Barotrauma.Lights
Vector2 drawPos = calculatedDrawPos;
float cosAngle = (float)Math.Cos(Rotation);
float sinAngle = -(float)Math.Sin(Rotation);
Vector2 uvOffset = Vector2.Zero;
Vector2 overrideTextureDims = Vector2.One;
Vector2 dir = this.dir;
if (OverrideLightTexture != null)
{
overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
@@ -1052,7 +1042,8 @@ namespace Barotrauma.Lights
if (LightSpriteEffect == SpriteEffects.FlipHorizontally)
{
origin.X = OverrideLightTexture.SourceRect.Width - origin.X;
dir = -dir;
cosAngle = -cosAngle;
sinAngle = -sinAngle;
}
if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; }
uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f);
@@ -1125,8 +1116,8 @@ namespace Barotrauma.Lights
//calculate texture coordinates based on the light's rotation
Vector2 originDiff = diff;
diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y;
diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X;
diff.X = originDiff.X * cosAngle - originDiff.Y * sinAngle;
diff.Y = originDiff.X * sinAngle + originDiff.Y * cosAngle;
diff *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y)));
diff += uvOffset;
}
@@ -1231,6 +1222,9 @@ namespace Barotrauma.Lights
}
drawPos.Y = -drawPos.Y;
float cosAngle = (float)Math.Cos(Rotation);
float sinAngle = -(float)Math.Sin(Rotation);
float bounds = TextureRange;
if (OverrideLightTexture != null)
@@ -1242,8 +1236,8 @@ namespace Barotrauma.Lights
origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y);
origin *= TextureRange;
drawPos.X += origin.X * dir.Y + origin.Y * dir.X;
drawPos.Y += origin.X * dir.X + origin.Y * dir.Y;
drawPos.X += origin.X * sinAngle + origin.Y * cosAngle;
drawPos.Y += origin.X * cosAngle + origin.Y * sinAngle;
}
//add a square-shaped boundary to make sure we've got something to construct the triangles from
@@ -1349,7 +1343,7 @@ namespace Barotrauma.Lights
{
if (conditionals.None()) { return; }
if (conditionalTarget == null) { return; }
if (logicalOperator == PropertyConditional.LogicalOperatorType.And)
if (comparison == PropertyConditional.Comparison.And)
{
Enabled = conditionals.All(c => c.Matches(conditionalTarget));
}
@@ -1405,7 +1399,9 @@ namespace Barotrauma.Lights
CalculateLightVertices(verts);
LastRecalculationTime = (float)Timing.TotalTime;
NeedsRecalculation = false;
NeedsRecalculation = needsRecalculationWhenUpToDate;
needsRecalculationWhenUpToDate = false;
state = LightVertexState.UpToDate;
}
}
@@ -56,12 +56,12 @@ namespace Barotrauma
}
private static readonly List<RoundSound> roundSounds = new List<RoundSound>();
private static readonly Dictionary<string, RoundSound> roundSoundByPath = new Dictionary<string, RoundSound>();
public static RoundSound? Load(ContentXElement element, bool stream = false)
{
if (GameMain.SoundManager?.Disabled ?? true) { return null; }
var filename = element.GetAttributeContentPath("file") ?? element.GetAttributeContentPath("sound");
if (filename is null)
{
string errorMsg = "Error when loading round sound (" + element + ") - file path not set";
@@ -70,7 +70,11 @@ namespace Barotrauma
return null;
}
Sound? existingSound = roundSounds.Find(s => s.Filename == filename?.FullPath && s.Stream == stream && s.Sound is { Disposed: false })?.Sound;
Sound? existingSound = null;
if (roundSoundByPath.TryGetValue(filename.FullPath, out RoundSound? rs) && rs.Sound is { Disposed: false })
{
existingSound = rs.Sound;
}
if (existingSound is null)
{
@@ -99,7 +103,10 @@ namespace Barotrauma
}
RoundSound newSound = new RoundSound(element, existingSound);
if (filename is not null && !newSound.Stream)
{
roundSoundByPath.TryAdd(filename.FullPath, newSound);
}
roundSounds.Add(newSound);
return newSound;
}
@@ -124,24 +131,14 @@ namespace Barotrauma
roundSound.Sound = existingSound;
}
private static void Remove(RoundSound roundSound)
{
#warning TODO: what is going on here????
roundSound.Sound?.Dispose();
if (roundSounds.Contains(roundSound)) { roundSounds.Remove(roundSound); }
foreach (RoundSound otherSound in roundSounds)
{
if (otherSound.Sound == roundSound.Sound) { otherSound.Sound = null; }
}
}
public static void RemoveAllRoundSounds()
{
for (int i = roundSounds.Count - 1; i >= 0; i--)
foreach (var roundSound in roundSounds)
{
Remove(roundSounds[i]);
roundSound.Sound?.Dispose();
}
roundSounds.Clear();
roundSoundByPath.Clear();
}
}
}
@@ -58,11 +58,8 @@ namespace Barotrauma
convexHulls ??= new List<ConvexHull>();
var h = new ConvexHull(
new Rectangle((position - size / 2).ToPoint(), size.ToPoint()),
IsHorizontal,
this)
{
IsExteriorWall = IsExteriorWall
};
IsHorizontal,
this);
if (Math.Abs(rotation) > 0.001f)
{
h.Rotate(position, rotation);
@@ -501,7 +498,7 @@ namespace Barotrauma
private bool ConditionalMatches(PropertyConditional conditional)
{
if (!string.IsNullOrEmpty(conditional.TargetItemComponent))
if (!string.IsNullOrEmpty(conditional.TargetItemComponentName))
{
return false;
}
@@ -116,11 +116,11 @@ namespace Barotrauma
foreach (MapEntity e in entitiesToRender)
{
if (!e.DrawOverWater) continue;
if (!e.DrawOverWater) { continue; }
if (predicate != null)
{
if (!predicate(e)) continue;
if (!predicate(e)) { continue; }
}
e.Draw(spriteBatch, editing, false);
@@ -1,6 +1,7 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -18,7 +19,7 @@ namespace Barotrauma
return;
}
List<Submarine> subsToMove = submarine.GetConnectedSubs();
var subsToMove = submarine.GetConnectedSubs();
foreach (Submarine dockedSub in subsToMove)
{
if (dockedSub == submarine) { continue; }
@@ -51,7 +52,6 @@ namespace Barotrauma
sub.PhysicsBody.SetTransformIgnoreContacts(sub.PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(moveAmount), 0.0f);
}
}
if (closestSub != null && subsToMove.Contains(closestSub))
{
GameMain.GameScreen.Cam.Position += moveAmount;
@@ -67,6 +67,10 @@ namespace Barotrauma
else if (ConnectedDoor != null)
{
sprite = iconSprites["Door"];
if (ConnectedDoor.IsHorizontal && Ladders == null)
{
clr = Color.Yellow;
}
}
else if (Ladders != null)
{
@@ -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;
@@ -514,6 +517,7 @@ namespace Barotrauma.Networking
DisplayInLoadingScreens = true
};
Quit();
GUI.DisableHUD = false;
GameMain.ServerListScreen.Select();
return;
}
@@ -931,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 + "). Round init status: {roundInitStatus}." + campaignErrorInfo;
", 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);
}
@@ -1487,16 +1491,17 @@ namespace Barotrauma.Networking
NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID))
{
campaign.PendingSaveID = campaignSaveID;
DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0, 0, 60);
DateTime saveFileTimeOut = DateTime.Now + CampaignSaveTransferTimeOut;
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).");
new GUIMessageBox(TextManager.Get("error"), TextManager.Get("campaignsavetransfer.timeout"));
GameMain.NetLobbyScreen.Select();
roundInitStatus = RoundInitStatus.Interrupted;
yield return CoroutineStatus.Failure;
//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);
}
@@ -1712,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;
@@ -2872,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;
@@ -3063,8 +3070,12 @@ namespace Barotrauma.Networking
if (votingInterface != null)
{
votingInterface.Update(deltaTime);
if (!votingInterface.VoteRunning)
if (!votingInterface.VoteRunning || votingInterface.TimedOut)
{
if (votingInterface.TimedOut)
{
DebugConsole.AddWarning($"Voting interface timed out.");
}
votingInterface.Remove();
votingInterface = null;
}
@@ -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)
@@ -53,7 +53,7 @@ namespace Barotrauma.Networking
}
}
private readonly ref struct LobbyDataChangedEventHandler
private readonly struct LobbyDataChangedEventHandler : IDisposable
{
private readonly Action<Lobby> action;
@@ -187,7 +187,7 @@ namespace Barotrauma.Networking
int botSpawnMode = 0,
bool? useRespawnShuttle = null)
{
if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return;
if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return; }
IWriteMessage outMsg = new WriteOnlyMessage();
@@ -253,12 +253,9 @@ namespace Barotrauma.Networking
//in push-to-talk mode, InputType.Voice uses the active chat mode
bool usingActiveMode = PlayerInput.KeyDown(InputType.Voice);
bool pttDown = (usingActiveMode || usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null;
if (pttDown || captureTimer <= 0)
{
ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode;
}
if (pttDown)
{
ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode;
allowEnqueue = true;
}
}
@@ -115,17 +115,20 @@ namespace Barotrauma.Networking
float speechImpedimentMultiplier = 1.0f - client.Character.SpeechImpediment / 100.0f;
bool spectating = Character.Controlled == null;
float rangeMultiplier = spectating ? 2.0f : 1.0f;
WifiComponent radio = null;
WifiComponent senderRadio = null;
var messageType =
!client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) && ChatMessage.CanUseRadio(Character.Controlled) ?
ChatMessageType.Radio : ChatMessageType.Default;
!client.VoipQueue.ForceLocal &&
ChatMessage.CanUseRadio(client.Character, out senderRadio) &&
ChatMessage.CanUseRadio(Character.Controlled, out var recipientRadio) &&
senderRadio.CanReceive(recipientRadio) ?
ChatMessageType.Radio : ChatMessageType.Default;
client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
client.RadioNoise = 0.0f;
if (messageType == ChatMessageType.Radio)
{
client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier);
client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier);
if (distanceFactor > RangeNear && !spectating)
{
//noise starts increasing exponentially after 40% range
@@ -348,11 +348,13 @@ namespace Barotrauma
switch (voteType)
{
case VoteType.PurchaseAndSwitchSub:
GameMain.GameSession.PurchaseSubmarine(subInfo);
GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems);
if (GameMain.GameSession.TryPurchaseSubmarine(subInfo))
{
GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems);
}
break;
case VoteType.PurchaseSub:
GameMain.GameSession.PurchaseSubmarine(subInfo);
GameMain.GameSession.TryPurchaseSubmarine(subInfo);
break;
case VoteType.SwitchSub:
GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems);
@@ -1,4 +1,5 @@
using FarseerPhysics;
using Barotrauma.Extensions;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
@@ -48,19 +49,23 @@ namespace Barotrauma.Particles
private Vector2 drawPosition;
private float drawRotation;
private Vector2 colliderRadius;
private Hull currentHull;
private List<Gap> hullGaps;
private bool hasSubEmitters;
private List<ParticleEmitter> subEmitters = new List<ParticleEmitter>();
private readonly List<ParticleEmitter> subEmitters = new List<ParticleEmitter>();
private float animState;
private int animFrame;
private float collisionUpdateTimer;
private bool changesSize;
public bool HighQualityCollisionDetection;
public Vector4 ColorMultiplier;
@@ -127,7 +132,10 @@ namespace Barotrauma.Particles
position = (tracerPoints.Item1 + tracerPoints.Item2) / 2;
}
RefreshColliderSize();
sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f);
changesSize = !sizeChange.NearlyEquals(Vector2.Zero);
this.position = position;
prevPosition = position;
@@ -256,8 +264,12 @@ namespace Barotrauma.Particles
}
}
size.X += sizeChange.X * deltaTime;
size.Y += sizeChange.Y * deltaTime;
if (changesSize)
{
size.X += sizeChange.X * deltaTime;
size.Y += sizeChange.Y * deltaTime;
RefreshColliderSize();
}
if (UseMiddleColor)
{
@@ -344,11 +356,11 @@ namespace Barotrauma.Particles
{
Rectangle hullRect = currentHull.WorldRect;
Vector2 collisionNormal = Vector2.Zero;
if (velocity.Y < 0.0f && position.Y - prefab.CollisionRadius * size.Y < hullRect.Y - hullRect.Height)
if (velocity.Y < 0.0f && position.Y - colliderRadius.Y < hullRect.Y - hullRect.Height)
{
collisionNormal = new Vector2(0.0f, 1.0f);
}
else if (velocity.Y > 0.0f && position.Y + prefab.CollisionRadius * size.Y > hullRect.Y)
else if (velocity.Y > 0.0f && position.Y + colliderRadius.Y > hullRect.Y)
{
collisionNormal = new Vector2(0.0f, -1.0f);
}
@@ -378,11 +390,11 @@ namespace Barotrauma.Particles
}
collisionNormal = Vector2.Zero;
if (velocity.X < 0.0f && position.X - prefab.CollisionRadius * size.X < hullRect.X)
if (velocity.X < 0.0f && position.X - colliderRadius.X < hullRect.X)
{
collisionNormal = new Vector2(1.0f, 0.0f);
}
else if (velocity.X > 0.0f && position.X + prefab.CollisionRadius * size.X > hullRect.Right)
else if (velocity.X > 0.0f && position.X + colliderRadius.X > hullRect.Right)
{
collisionNormal = new Vector2(-1.0f, 0.0f);
}
@@ -431,6 +443,13 @@ namespace Barotrauma.Particles
return UpdateResult.Normal;
}
private void RefreshColliderSize()
{
if (!prefab.UseCollision) { return; }
colliderRadius = new Vector2(prefab.CollisionRadius);
if (!prefab.InvariantCollisionSize) { colliderRadius *= size; }
}
private void ApplyDrag(float dragCoefficient, float deltaTime)
{
Vector2 relativeVel = velocity;
@@ -475,11 +494,11 @@ namespace Barotrauma.Particles
{
if (collisionNormal.X > 0.0f)
{
position.X = Math.Max(position.X, prevHullRect.X + prefab.CollisionRadius * size.X);
position.X = Math.Max(position.X, prevHullRect.X + colliderRadius.X);
}
else
{
position.X = Math.Min(position.X, prevHullRect.Right - prefab.CollisionRadius * size.X);
position.X = Math.Min(position.X, prevHullRect.Right - colliderRadius.X);
}
velocity.X = Math.Sign(collisionNormal.X) * Math.Abs(velocity.X) * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction);
@@ -488,11 +507,11 @@ namespace Barotrauma.Particles
{
if (collisionNormal.Y > 0.0f)
{
position.Y = Math.Max(position.Y, prevHullRect.Y - prevHullRect.Height + prefab.CollisionRadius * size.Y);
position.Y = Math.Max(position.Y, prevHullRect.Y - prevHullRect.Height + colliderRadius.Y);
}
else
{
position.Y = Math.Min(position.Y, prevHullRect.Y - prefab.CollisionRadius * size.Y);
position.Y = Math.Min(position.Y, prevHullRect.Y - colliderRadius.Y);
}
velocity.X *= (1.0f - prefab.Friction);
@@ -513,26 +532,26 @@ namespace Barotrauma.Particles
if (position.Y < center.Y)
{
position.Y = hullRect.Y - hullRect.Height - prefab.CollisionRadius;
position.Y = hullRect.Y - hullRect.Height - colliderRadius.Y;
velocity.X *= (1.0f - prefab.Friction);
velocity.Y = -velocity.Y * prefab.Restitution;
}
else if (position.Y > center.Y)
{
position.Y = hullRect.Y + prefab.CollisionRadius;
position.Y = hullRect.Y + colliderRadius.Y;
velocity.X *= (1.0f - prefab.Friction);
velocity.Y = -velocity.Y * prefab.Restitution;
}
if (position.X < center.X)
{
position.X = hullRect.X - prefab.CollisionRadius;
position.X = hullRect.X - colliderRadius.X;
velocity.X = -velocity.X * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction);
}
else if (position.X > center.X)
{
position.X = hullRect.X + hullRect.Width + prefab.CollisionRadius;
position.X = hullRect.X + hullRect.Width + colliderRadius.X;
velocity.X = -velocity.X * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction);
}
@@ -559,11 +578,12 @@ namespace Barotrauma.Particles
Color currColor = new Color(color.ToVector4() * ColorMultiplier);
if (prefab.Sprites[spriteIndex] is SpriteSheet)
Vector2 drawPos = new Vector2(drawPosition.X, -drawPosition.Y);
if (prefab.Sprites[spriteIndex] is SpriteSheet sheet)
{
((SpriteSheet)prefab.Sprites[spriteIndex]).Draw(
sheet.Draw(
spriteBatch, animFrame,
new Vector2(drawPosition.X, -drawPosition.Y),
drawPos,
currColor * (currColor.A / 255.0f),
prefab.Sprites[spriteIndex].Origin, drawRotation,
drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth);
@@ -571,11 +591,23 @@ namespace Barotrauma.Particles
else
{
prefab.Sprites[spriteIndex].Draw(spriteBatch,
new Vector2(drawPosition.X, -drawPosition.Y),
drawPos,
currColor * (currColor.A / 255.0f),
prefab.Sprites[spriteIndex].Origin, drawRotation,
drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth);
}
/*if (GameMain.DebugDraw && prefab.UseCollision)
{
GUI.DrawLine(spriteBatch,
drawPos - Vector2.UnitX * colliderRadius.X,
drawPos + Vector2.UnitX * colliderRadius.X,
Color.Gray);
GUI.DrawLine(spriteBatch,
drawPos - Vector2.UnitY * colliderRadius.Y,
drawPos + Vector2.UnitY * colliderRadius.Y,
Color.Gray);
}*/
}
}
}
@@ -135,6 +135,9 @@ namespace Barotrauma.Particles
[Editable(0.0f, 10000.0f), Serialize(0.0f, IsPropertySaveable.No, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")]
public float CollisionRadius { get; private set; }
[Editable, Serialize(false, IsPropertySaveable.No, description: "If enabled, the size (or changes in size) of the particle doesn't affect the size of the collider.")]
public bool InvariantCollisionSize { get; private set; }
[Editable, Serialize(false, IsPropertySaveable.No, description: "Does the particle collide with the walls of the submarine and the level.")]
public bool UseCollision { get; private set; }
@@ -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)
@@ -435,8 +435,11 @@ namespace Barotrauma
graphics.BlendState = BlendState.NonPremultiplied;
graphics.SamplerStates[0] = SamplerState.PointClamp;
graphics.SamplerStates[1] = SamplerState.PointClamp;
GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply();
Quad.Render();
graphics.SamplerStates[0] = SamplerState.LinearWrap;
graphics.SamplerStates[1] = SamplerState.LinearWrap;
}
if (Character.Controlled is { } character)
@@ -59,7 +59,6 @@ namespace Barotrauma
private GUIImage playstyleBanner;
private GUITextBlock playstyleDescription;
private const string RemoteContentUrl = "http://www.barotraumagame.com/gamedata/";
private readonly GUIComponent remoteContentContainer;
private XDocument remoteContentDoc;
@@ -1525,10 +1524,11 @@ namespace Barotrauma
private void FetchRemoteContent()
{
if (string.IsNullOrEmpty(RemoteContentUrl)) { return; }
string remoteContentUrl = GameSettings.CurrentConfig.RemoteMainMenuContentUrl;
if (string.IsNullOrEmpty(remoteContentUrl)) { return; }
try
{
var client = new RestClient(RemoteContentUrl);
var client = new RestClient(remoteContentUrl);
var request = new RestRequest("MenuContent.xml", Method.GET);
TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request),
RemoteContentReceived);
@@ -751,12 +751,20 @@ namespace Barotrauma
};
ServerMessage.OnTextChanged += (textBox, text) =>
{
Vector2 textSize = textBox.Font.MeasureString(textBox.WrappedText);
textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10));
serverMessageContainer.UpdateScrollBarSize();
serverMessageHint.Visible = !textBox.Selected && !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text);
RefreshServerInfoSize();
return true;
};
ServerMessage.RectTransform.SizeChanged += RefreshServerInfoSize;
void RefreshServerInfoSize()
{
serverMessageHint.Visible = !ServerMessage.Selected && !ServerMessage.Readonly && string.IsNullOrWhiteSpace(ServerMessage.Text);
Vector2 textSize = ServerMessage.Font.MeasureString(ServerMessage.WrappedText);
ServerMessage.RectTransform.NonScaledSize = new Point(ServerMessage.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10));
serverMessageContainer.UpdateScrollBarSize();
}
ServerMessage.OnEnterPressed += (textBox, text) =>
{
string str = textBox.Text;
@@ -812,7 +812,7 @@ namespace Barotrauma
private bool SortList(GUIButton button, object obj)
{
if (!(obj is ColumnLabel sortBy)) { return false; }
if (obj is not ColumnLabel sortBy) { return false; }
SortList(sortBy, toggle: true);
return true;
}
@@ -848,8 +848,7 @@ namespace Barotrauma
{
if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; }
if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; }
int comparison = sortedAscending ? 1 : -1;
return CompareServer(sortBy, s1, s2) * comparison;
return CompareServer(sortBy, s1, s2, sortedAscending);
});
}
@@ -857,22 +856,31 @@ namespace Barotrauma
{
var children = serverList.Content.RectTransform.Children.Reverse().ToList();
int comparison = sortedAscending ? 1 : -1;
foreach (var child in children)
{
if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; }
if (CompareServer(sortedBy, serverInfo, serverInfo2) * comparison >= 0)
if (CompareServer(sortedBy, serverInfo, serverInfo2, sortedAscending) >= 0)
{
var index = serverList.Content.RectTransform.GetChildIndex(child);
component.RectTransform.RepositionChildInHierarchy(index + 1);
component.RectTransform.RepositionChildInHierarchy(Math.Min(index + 1, serverList.Content.CountChildren - 1));
return;
}
}
component.RectTransform.SetAsFirstChild();
}
private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2)
private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2, bool ascending)
{
//always put servers with unknown ping at the bottom (unless we're specifically sorting by ping)
//servers without a ping are often unreachable/spam
bool s1HasPing = s1.Ping.IsSome();
bool s2HasPing = s2.Ping.IsSome();
if (s1HasPing != s2HasPing)
{
return s1HasPing ? -1 : 1;
}
int comparison = ascending ? 1 : -1;
switch (sortBy)
{
case ColumnLabel.ServerListCompatible:
@@ -880,18 +888,18 @@ namespace Barotrauma
bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion);
if (s1Compatible == s2Compatible) { return 0; }
return s1Compatible ? -1 : 1;
return (s1Compatible ? -1 : 1) * comparison;
case ColumnLabel.ServerListHasPassword:
if (s1.HasPassword == s2.HasPassword) { return 0; }
return s1.HasPassword ? 1 : -1;
return (s1.HasPassword ? 1 : -1) * comparison;
case ColumnLabel.ServerListName:
// I think we actually want culture-specific sorting here?
return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture);
return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * comparison;
case ColumnLabel.ServerListRoundStarted:
if (s1.GameStarted == s2.GameStarted) { return 0; }
return s1.GameStarted ? 1 : -1;
return (s1.GameStarted ? 1 : -1) * comparison;
case ColumnLabel.ServerListPlayers:
return s2.PlayerCount.CompareTo(s1.PlayerCount);
return s2.PlayerCount.CompareTo(s1.PlayerCount) * comparison;
case ColumnLabel.ServerListPing:
return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch
{
@@ -899,7 +907,7 @@ namespace Barotrauma
(true, true) => s2Ping.CompareTo(s1Ping),
(false, true) => 1,
(true, false) => -1
};
} * comparison;
default:
return 0;
}
@@ -1504,9 +1512,41 @@ namespace Barotrauma
private void AddToServerList(ServerInfo serverInfo, bool skipPing = false)
{
const int MaxAllowedPlayers = 1000;
const int MaxAllowedSimilarServers = 10;
const float MinSimilarityPercentage = 0.8f;
if (string.IsNullOrWhiteSpace(serverInfo.ServerName)) { return; }
if (serverInfo.PlayerCount > serverInfo.MaxPlayers) { return; }
if (serverInfo.PlayerCount < 0) { return; }
if (serverInfo.MaxPlayers <= 0) { return; }
//no way a legit server can have this many players
if (serverInfo.MaxPlayers > MaxAllowedPlayers) { return; }
int similarServerCount = 0;
string serverInfoStr = getServerInfoStr(serverInfo);
foreach (var serverElement in serverList.Content.Children)
{
if (!serverElement.Visible) { continue; }
if (serverElement.UserData is not ServerInfo otherServer || otherServer == serverInfo) { continue; }
if (ToolBox.LevenshteinDistance(serverInfoStr, getServerInfoStr(otherServer)) < serverInfoStr.Length * (1.0f - MinSimilarityPercentage))
{
similarServerCount++;
if (similarServerCount > MaxAllowedSimilarServers)
{
DebugConsole.Log($"Server {serverInfo.ServerName} seems to be almost identical to {otherServer.ServerName}. Hiding as a potential spam server.");
break;
}
}
}
if (similarServerCount > MaxAllowedSimilarServers) { return; }
static string getServerInfoStr(ServerInfo serverInfo)
{
string str = serverInfo.ServerName + serverInfo.ServerMessage + serverInfo.MaxPlayers;
if (str.Length > 200) { return str.Substring(0, 200); }
return str;
}
RemoveMsgFromServerList(MsgUserData.RefreshingServerList);
RemoveMsgFromServerList(MsgUserData.NoServers);
@@ -1522,7 +1562,6 @@ namespace Barotrauma
UpdateServerInfoUI(serverInfo);
if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); }
InsertServer(serverInfo, serverFrame);
}
private void UpdateServerInfoUI(ServerInfo serverInfo)
@@ -1736,7 +1775,7 @@ namespace Barotrauma
AddToFavoriteServers(serverInfo);
}
SortList(sortedBy, toggle: false);
InsertServer(serverInfo, serverFrame);
FilterServers();
}
@@ -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
{
@@ -525,6 +525,11 @@ namespace Barotrauma
GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green);
}
WayPoint.ShowWayPoints = true;
var matchingTickBox = showEntitiesTickBoxes?.Find(tb => tb.UserData as string == "waypoint");
if (matchingTickBox != null)
{
matchingTickBox.Selected = true;
}
generateWaypointsVerification.Close();
return true;
};
@@ -2847,7 +2852,7 @@ namespace Barotrauma
{
OnClicked = (button, o) =>
{
var requiredPackages = MapEntity.mapEntityList.Select(e => e.Prefab.ContentPackage)
var requiredPackages = MapEntity.mapEntityList.Select(e => e?.Prefab?.ContentPackage)
.Where(cp => cp != null)
.Distinct().OfType<ContentPackage>().Select(p => p.Name).ToHashSet();
var tickboxes = requiredContentPackList.Content.Children.OfType<GUITickBox>().ToArray();
@@ -3546,10 +3551,46 @@ 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);
if (id == -1) { continue; }
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);
@@ -5755,7 +5796,10 @@ namespace Barotrauma
{
item.SetTransform(dummyCharacter.SimPosition, 0.0f);
item.UpdateTransform();
item.SetTransform(item.body.SimPosition, 0.0f);
if (item.body != null)
{
item.SetTransform(item.body.SimPosition, 0.0f);
}
//wires need to be updated for the last node to follow the player during rewiring
Wire wire = item.GetComponent<Wire>();
@@ -5868,6 +5912,11 @@ namespace Barotrauma
spriteBatch.End();
}
if (GameMain.LightManager.DebugLos)
{
GameMain.LightManager.DebugDrawLos(spriteBatch, cam);
}
//-------------------- HUD -----------------------------
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState);
@@ -343,12 +343,11 @@ namespace Barotrauma
if (property.PropertyType == typeof(string) && value == null)
{
value = "";
}
}
Identifier propertyTag = $"{property.PropertyInfo.DeclaringType.Name}.{property.PropertyInfo.Name}".ToIdentifier();
Identifier fallbackTag = property.PropertyInfo.Name.ToIdentifier();
LocalizedString displayName =
TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier());
LocalizedString displayName = TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier());
if (displayName.IsNullOrEmpty())
{
Editable editable = property.GetAttribute<Editable>();
@@ -380,10 +379,14 @@ namespace Barotrauma
}
LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description");
if (toolTip.IsNullOrEmpty() && entity.GetType() != property.PropertyInfo.DeclaringType)
if (entity.GetType() != property.PropertyInfo.DeclaringType)
{
Identifier propertyTagForDerivedClass = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier();
toolTip = TextManager.Get($"{propertyTagForDerivedClass}.description", $"sp.{propertyTagForDerivedClass}.description");
var toolTipForDerivedClass = TextManager.Get($"{propertyTagForDerivedClass}.description", $"sp.{propertyTagForDerivedClass}.description");
if (!toolTipForDerivedClass.IsNullOrEmpty())
{
toolTip = toolTipForDerivedClass;
}
}
if (toolTip.IsNullOrEmpty())
{
@@ -163,7 +163,7 @@ namespace Barotrauma.Steam
CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.LocalPackages.Refresh());
}
public static async Task CreatePublishStagingCopy(string modVersion, ContentPackage contentPackage)
public static async Task CreatePublishStagingCopy(string title, string modVersion, ContentPackage contentPackage)
{
await Task.Yield();
@@ -184,11 +184,13 @@ namespace Barotrauma.Steam
throw new Exception("Staging copy could not be loaded",
result.TryUnwrapFailure(out var exception) ? exception : null);
}
//Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be
//Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be
ModProject modProject = new ModProject(tempPkg)
{
ModVersion = modVersion
ModVersion = modVersion,
Name = title,
ExpectedHash = tempPkg.CalculateHash(name: title, modVersion: modVersion)
};
modProject.Save(stagingFileListPath);
}
@@ -9,6 +9,7 @@ using Barotrauma.Extensions;
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Steamworks;
using Directory = Barotrauma.IO.Directory;
using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>;
using Path = Barotrauma.IO.Path;
@@ -157,7 +158,7 @@ namespace Barotrauma.Steam
}
var selectedTitle =
new GUITextBlock(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), workshopItem.Title ?? localPackage.Name,
new GUITextBlock(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), localPackage.Name,
font: GUIStyle.LargeFont);
if (workshopItem.Id != 0)
{
@@ -212,7 +213,7 @@ namespace Barotrauma.Steam
};
Label(rightTop, TextManager.Get("WorkshopItemTitle"), GUIStyle.SubHeadingFont);
var titleTextBox = new GUITextBox(NewItemRectT(rightTop), workshopItem.Title ?? localPackage.Name);
var titleTextBox = new GUITextBox(NewItemRectT(rightTop), localPackage.Name);
Label(rightTop, TextManager.Get("WorkshopItemDescription"), GUIStyle.SubHeadingFont);
var descriptionTextBox
@@ -320,7 +321,9 @@ namespace Barotrauma.Steam
workshopItem.Id == 0
? Steamworks.Ugc.Editor.NewCommunityFile
: new Steamworks.Ugc.Editor(workshopItem.Id);
ugcEditor = ugcEditor.WithTitle(titleTextBox.Text)
ugcEditor = ugcEditor
.InLanguage(SteamUtils.SteamUILanguage ?? string.Empty)
.WithTitle(titleTextBox.Text)
.WithDescription(descriptionTextBox.Text)
.WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value))
.WithChangeLog(changeNoteTextBox.Text)
@@ -478,7 +481,7 @@ namespace Barotrauma.Steam
bool stagingReady = false;
Exception? stagingException = null;
TaskPool.Add("CreatePublishStagingCopy",
SteamManager.Workshop.CreatePublishStagingCopy(modVersion, localPackage),
SteamManager.Workshop.CreatePublishStagingCopy(editor.Title ?? localPackage.Name, modVersion, localPackage),
(t) =>
{
stagingReady = true;
@@ -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;
}
}
}
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.1.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
+2 -2
View File
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.1.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
@@ -30,10 +30,16 @@ float xLosAlpha;
float4 xColor;
float blurDistance;
float4 mainPS(VertexShaderOutput input) : COLOR0
{
float4 sampleColor = tex2D(TextureSampler, input.TexCoords);
float4 losColor = tex2D(LosSampler, input.TexCoords);
float4 losColor = tex2D(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance));
losColor += tex2D(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance));
losColor += tex2D(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance));
losColor += tex2D(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y + blurDistance));
losColor = losColor * 0.25f;
float obscureAmount = 1.0f - losColor.r;
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.1.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.1.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
+2 -2
View File
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.1.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
@@ -60,6 +60,10 @@ namespace Barotrauma
{
distance = Math.Min(distance, Vector2.Distance(recipient.Character.ViewTarget.WorldPosition, WorldPosition));
}
if (ViewTarget != null && ViewTarget != this)
{
distance = Math.Min(distance, Vector2.Distance(comparePosition, ViewTarget.WorldPosition));
}
float priority = 1.0f - MathUtils.InverseLerp(
NetConfig.HighPrioCharacterPositionUpdateDistance,
@@ -155,8 +159,6 @@ namespace Barotrauma
memInput.RemoveAt(memInput.Count - 1);
TransformCursorPos();
if ((dequeuedInput == InputNetFlags.None || dequeuedInput == InputNetFlags.FacingLeft) && Math.Abs(AnimController.Collider.LinearVelocity.X) < 0.005f && Math.Abs(AnimController.Collider.LinearVelocity.Y) < 0.2f)
{
while (memInput.Count > 5 && memInput[memInput.Count - 1].states == dequeuedInput)
@@ -14,7 +14,7 @@ namespace Barotrauma
static partial class DebugConsole
{
private static readonly RateLimiter rateLimiter = new(
maxRequests: 10,
maxRequests: 50,
expiryInSeconds: 5,
punishmentRules: new[]
{
@@ -2526,14 +2526,14 @@ namespace Barotrauma
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
@@ -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,10 +12,10 @@ namespace Barotrauma
{
internal partial class MedicalClinic
{
// allow 10 requests per 5 seconds, announce to chat if the limit is reached
// allow 20 requests per 5 seconds, announce to chat if the limit is reached
private readonly RateLimiter rateLimiter = new(
maxRequests: 10,
expiryInSeconds: 5,
maxRequests: RateLimitMaxRequests,
expiryInSeconds: RateLimitExpiry,
punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce));
private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry);
@@ -126,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);
@@ -18,11 +18,11 @@ namespace Barotrauma.Items.Components
if (item.CanClientAccess(c))
{
lastReceivedTargetForce = null;
if (Math.Abs(newTargetForce - targetForce) > 0.01f)
{
GameServer.Log(GameServer.CharacterLogName(c.Character) + " set the force of " + item.Name + " to " + (int)(newTargetForce) + " %", ServerLog.MessageType.ItemInteraction);
}
targetForce = newTargetForce;
User = c.Character;
}
@@ -184,6 +184,8 @@ namespace Barotrauma
int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand),
rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand);
if (IsSlotIndexOutOfBound(leftHandSlot) || IsSlotIndexOutOfBound(rightHandSlot)) { return; }
TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot);
TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot);
@@ -198,6 +200,8 @@ namespace Barotrauma
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)
@@ -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;
@@ -21,15 +21,13 @@ namespace Barotrauma.Networking
public override Voting Voting { get; }
private string serverName;
public string ServerName
{
get { return serverName; }
get { return ServerSettings.ServerName; }
set
{
if (string.IsNullOrEmpty(value)) { return; }
serverName = value;
ServerSettings.ServerName = value;
}
}
@@ -133,8 +131,6 @@ namespace Barotrauma.Networking
name = name.Substring(0, NetConfig.ServerNameMaxLength);
}
this.serverName = name;
LastClientListUpdateID = 0;
ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP);
@@ -775,9 +771,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);
}
}
}
}
@@ -790,8 +789,11 @@ namespace Barotrauma.Networking
break;
}
if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
{
MultiPlayerCampaign.LoadCampaign(saveName);
{
using (dosProtection.Pause(connectedClient))
{
MultiPlayerCampaign.LoadCampaign(saveName);
}
}
}
break;
@@ -1661,38 +1663,54 @@ namespace Barotrauma.Networking
//characters or items spawned mid-round don't necessarily exist at the client's end yet
if (!c.NeedsMidRoundSync)
{
foreach (Character character in Character.CharacterList)
Character clientCharacter = c.Character;
foreach (Character otherCharacter in Character.CharacterList)
{
if (!character.Enabled) { continue; }
if (!otherCharacter.Enabled) { continue; }
if (c.SpectatePos == null)
{
float distSqr = Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition);
if (c.Character.ViewTarget != null)
//not spectating ->
// check if the client's character, or the entity they're viewing,
// is close enough to the other character or the entity the other character is viewing
float distSqr = GetShortestDistance(clientCharacter.WorldPosition, otherCharacter);
if (clientCharacter.ViewTarget != null && clientCharacter.ViewTarget != clientCharacter)
{
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(character.WorldPosition, c.Character.ViewTarget.WorldPosition));
distSqr = Math.Min(distSqr, GetShortestDistance(clientCharacter.ViewTarget.WorldPosition, otherCharacter));
}
if (distSqr >= MathUtils.Pow2(character.Params.DisableDistance)) { continue; }
if (distSqr >= MathUtils.Pow2(otherCharacter.Params.DisableDistance)) { continue; }
}
else
else if (otherCharacter != clientCharacter)
{
if (character != c.Character && Vector2.DistanceSquared(character.WorldPosition, c.SpectatePos.Value) >= MathUtils.Pow2(character.Params.DisableDistance))
{
continue;
}
//spectating ->
// check if the position the client is viewing
// is close enough to the other character or the entity the other character is viewing
if (GetShortestDistance(c.SpectatePos.Value, otherCharacter) >= MathUtils.Pow2(otherCharacter.Params.DisableDistance)) { continue; }
}
float updateInterval = character.GetPositionUpdateInterval(c);
c.PositionUpdateLastSent.TryGetValue(character, out float lastSent);
static float GetShortestDistance(Vector2 viewPos, Character targetCharacter)
{
float distSqr = Vector2.DistanceSquared(viewPos, targetCharacter.WorldPosition);
if (targetCharacter.ViewTarget != null && targetCharacter.ViewTarget != targetCharacter)
{
//if the character is viewing something (far-away turret?),
//we might want to send updates about it to the spectating client even though they're far away from the actual character
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(viewPos, targetCharacter.ViewTarget.WorldPosition));
}
return distSqr;
}
float updateInterval = otherCharacter.GetPositionUpdateInterval(c);
c.PositionUpdateLastSent.TryGetValue(otherCharacter, out float lastSent);
if (lastSent > NetTime.Now)
{
//sent in the future -> can't be right, remove
c.PositionUpdateLastSent.Remove(character);
c.PositionUpdateLastSent.Remove(otherCharacter);
}
else
{
if (lastSent > NetTime.Now - updateInterval) { continue; }
}
if (!c.PendingPositionUpdates.Contains(character)) { c.PendingPositionUpdates.Enqueue(character); }
if (!c.PendingPositionUpdates.Contains(otherCharacter)) { c.PendingPositionUpdates.Enqueue(otherCharacter); }
}
foreach (Submarine sub in Submarine.Loaded)
@@ -2594,16 +2612,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;
}
@@ -3084,7 +3106,7 @@ namespace Barotrauma.Networking
default:
if (command != "")
{
if (command.ToLower() == serverName.ToLower())
if (command.ToLower() == ServerName.ToLower())
{
//a private message to the host
if (OwnerConnection != null)
@@ -3139,7 +3161,7 @@ namespace Barotrauma.Networking
//msg sent by the server
if (senderCharacter == null)
{
senderName = serverName;
senderName = ServerName;
}
else //msg sent by an AI character
{
@@ -3173,7 +3195,7 @@ namespace Barotrauma.Networking
//msg sent by the server
if (senderCharacter == null)
{
senderName = serverName;
senderName = ServerName;
}
else //sent by an AI character, not allowed when the game is not running
{
@@ -3396,33 +3418,35 @@ namespace Barotrauma.Networking
}
}
public void SwitchSubmarine()
public bool TrySwitchSubmarine()
{
if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return; }
if (Voting.ActiveVote is not Voting.SubmarineVote subVote) { return false; }
SubmarineInfo targetSubmarine = subVote.Sub;
VoteType voteType = Voting.ActiveVote.VoteType;
Client starter = Voting.ActiveVote.VoteStarter;
bool purchaseFailed = false;
switch (voteType)
{
case VoteType.PurchaseAndSwitchSub:
case VoteType.PurchaseSub:
// Pay for submarine
GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter);
purchaseFailed = !GameMain.GameSession.TryPurchaseSubmarine(targetSubmarine, starter);
break;
case VoteType.SwitchSub:
break;
default:
return;
return false;
}
if (voteType != VoteType.PurchaseSub)
if (voteType != VoteType.PurchaseSub && !purchaseFailed)
{
GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter);
}
Voting.StopSubmarineVote(true);
Voting.StopSubmarineVote(passed: !purchaseFailed);
return !purchaseFailed;
}
public void UpdateClientPermissions(Client client)
@@ -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()
};
@@ -370,6 +370,8 @@ namespace Barotrauma.Networking
"192-255",
"384-591",
"1024-1279",
"4352-4607", //Hangul Jamo
"44032-55215", //Hangul Syllables
"19968-21327","21329-40959","13312-19903","131072-173791","173824-178207","178208-183983","63744-64255","194560-195103" //CJK
};
@@ -42,7 +42,11 @@ namespace Barotrauma
{
if (passed)
{
GameMain.Server?.SwitchSubmarine();
if (GameMain.Server != null && !GameMain.Server.TrySwitchSubmarine())
{
passed = false;
State = VoteState.Failed;
}
}
else
{
@@ -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)));
@@ -71,6 +71,15 @@ namespace Barotrauma
StrikesResetInterval = 60,
StrikeThreshold = 6;
private const int MinPacketLimitMultipler = 1;
private static int GetMaxPacketLimit(ServerSettings settings)
=> (int)MathF.Ceiling(
settings.MaxPacketAmount *
MathF.Max(
settings.TickRate / (float)ServerSettings.DefaultTickRate,
MinPacketLimitMultipler)); // Prevent the rate limit multiplier from being less than 1.
/// <summary>
/// Called when the server receives a packet to start logging how much time it takes to process.
/// </summary>
@@ -122,11 +131,7 @@ namespace Barotrauma
private void StartFor(Client client)
{
if (!clients.ContainsKey(client))
{
clients.Add(client, new OffenseData());
}
clients.TryAdd(client, new OffenseData());
clients[client].Stopwatch.Start();
}
@@ -149,7 +154,7 @@ namespace Barotrauma
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)
if (data.PacketCount > GetMaxPacketLimit(settings) && settings.MaxPacketAmount > ServerSettings.PacketLimitMin)
{
AttemptKickClient(client, TextManager.Get("PacketLimitKicked"));
clients.Remove(client);
@@ -216,7 +221,7 @@ namespace Barotrauma
{
if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings)
{
if (data.PacketCount > settings.MaxPacketAmount * 0.9f)
if (data.PacketCount > GetMaxPacketLimit(settings) * 0.9f)
{
GameServer.Log($"{NetworkMember.ClientLogName(client)} is sending a lot of packets and almost got kicked! ({data.PacketCount}).", ServerLog.MessageType.DoSProtection);
}
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.1.4.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
@@ -95,10 +95,7 @@ namespace Barotrauma
{
get
{
if (visibleHulls == null)
{
visibleHulls = Character.GetVisibleHulls();
}
visibleHulls ??= Character.GetVisibleHulls();
return visibleHulls;
}
private set
@@ -107,12 +104,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 +262,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 +276,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 +310,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 +320,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 +335,7 @@ namespace Barotrauma
}
else
{
return targetInventory.TryPutItem(item, Character, CharacterInventory.anySlot);
return targetInventory.TryPutItem(item, Character, CharacterInventory.AnySlot);
}
}
@@ -339,7 +360,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;
@@ -401,14 +422,9 @@ namespace Barotrauma
var door = gap.ConnectedDoor;
if (door != null)
{
if (!door.CanBeTraversed)
if (!pathSteering.CanAccessDoor(door))
{
if (!door.HasAccess(Character))
{
if (!canAttackDoors) { continue; }
// Treat doors that don't have access to like they were farther, because it will take time to break them.
multiplier = 5;
}
continue;
}
}
else
@@ -449,9 +465,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);
@@ -369,7 +369,13 @@ namespace Barotrauma
}
else if (targetCharacter.AIController is EnemyAIController enemy)
{
if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
if (enemy.PetBehavior != null && (PetBehavior != null || AIParams.HasTag("pet")))
{
// Pets see other pets as pets by default.
// Monsters see them only as pet only when they have a matching ai target. Otherwise they use the other tags, specified below.
targetingTag = "pet";
}
else if (targetCharacter.IsHusk && AIParams.HasTag("husk"))
{
targetingTag = "husk";
}
@@ -480,7 +486,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
@@ -695,6 +701,9 @@ namespace Barotrauma
// Can't target characters of same species/group because that would make us hostile to all friendly characters in the same species/group.
if (Character.IsSameSpeciesOrGroup(c)) { return false; }
if (targetCharacter.IsSameSpeciesOrGroup(c)) { return false; }
//don't try to attack targets in a sub that belongs to a different team
//(for example, targets in an outpost if we're in the main sub)
if (c.Submarine?.TeamID != Character.Submarine?.TeamID) { return false; }
if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
{
return a.Damage >= selectedTargetingParams.Threshold;
@@ -894,7 +903,7 @@ namespace Barotrauma
_previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
{
// Keep heading to the last known position of the target
var memory = GetTargetMemory(target, false);
var memory = GetTargetMemory(target);
if (memory != null)
{
var location = memory.Location;
@@ -981,7 +990,7 @@ namespace Barotrauma
}
else
{
PathSteering.SetPath(path);
PathSteering.SetPath(patrolTarget.SimPosition, path);
patrolTimerMargin = 0;
newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f);
searchingNewHull = false;
@@ -1088,13 +1097,13 @@ namespace Barotrauma
Character owner = GetOwner(item);
if (owner != null)
{
if (Character.IsFriendly(owner))
if (Character.IsFriendly(owner) || owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
ResetAITarget();
State = AIState.Idle;
return;
}
else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
else
{
SelectedAiTarget = owner.AiTarget;
}
@@ -2186,7 +2195,7 @@ namespace Barotrauma
}
}
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true);
AITargetMemory targetMemory = GetTargetMemory(attacker.AiTarget, addIfNotFound: true, keepAlive: true);
targetMemory.Priority += GetRelativeDamage(attackResult.Damage, Character.Vitality) * AIParams.AggressionHurt;
// Only allow to react once. Otherwise would attack the target with only a fraction of a cooldown
@@ -2531,8 +2540,10 @@ namespace Barotrauma
if (Math.Abs(limbDiff.X) < itemBodyExtent &&
Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor)
{
Vector2 velocity = limbDiff;
if (limbDiff.LengthSquared() > 0.01f) { velocity = Vector2.Normalize(velocity); }
item.body.LinearVelocity *= 0.9f;
item.body.LinearVelocity -= limbDiff * 0.25f;
item.body.LinearVelocity -= velocity * 0.25f;
bool wasBroken = item.Condition <= 0.0f;
item.AddDamage(Character, item.WorldPosition, new Attack(0.0f, 0.0f, 0.0f, 0.0f, 0.02f * Character.Params.EatingSpeed), deltaTime);
Character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
@@ -2924,7 +2935,8 @@ namespace Barotrauma
}
}
}
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0)
//no need to eat if the character is already in full health (except if it's a pet - pets actually need to eat to stay alive, not just to regain health)
if (targetParams.State == AIState.Eat && Character.Params.Health.HealthRegenerationWhenEating > 0 && !Character.IsPet)
{
valueModifier *= MathHelper.Lerp(1f, 0.1f, Character.HealthPercentage / 100f);
}
@@ -3021,7 +3033,7 @@ namespace Barotrauma
//if the target is very close, the distance doesn't make much difference
// -> just ignore the distance and target whatever has the highest priority
dist = Math.Max(dist, 100.0f);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true);
AITargetMemory targetMemory = GetTargetMemory(aiTarget, addIfNotFound: true, keepAlive: SelectedAiTarget != aiTarget);
if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
{
float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y;
@@ -3090,12 +3102,20 @@ namespace Barotrauma
if (aiTarget.Entity is Item i)
{
Character owner = GetOwner(i);
// Don't target items that we own.
// This is a rare case, and almost entirely related to Humanhusks, so let's check it last to reduce unnecessary checks (although the check shouldn't be expensive)
if (owner == Character) { continue; }
if (owner != null && (Character.IsFriendly(owner) || owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)))
if (owner != null)
{
continue;
if (owner.AiTarget != null && ignoredTargets.Contains(owner.AiTarget)) { continue; }
if (Character.IsFriendly(owner))
{
// Don't target items that we own. This is a rare case, and almost entirely related to Humanhusks (in the vanilla game).
continue;
}
if (owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{
// ignore if owner is tagged to be explicitly ignored (Feign Death)
continue;
}
}
}
if (targetCharacter != null)
@@ -3418,7 +3438,7 @@ namespace Barotrauma
return false;
}
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound)
private AITargetMemory GetTargetMemory(AITarget target, bool addIfNotFound = false, bool keepAlive = false)
{
if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
{
@@ -3428,9 +3448,8 @@ namespace Barotrauma
targetMemories.Add(target, memory);
}
}
if (addIfNotFound)
if (keepAlive)
{
// Keep the memory alive.
memory.Priority = Math.Max(memory.Priority, minPriority);
}
return memory;
@@ -3446,7 +3465,7 @@ namespace Barotrauma
}
else if (CanPerceive(_selectedAiTarget, checkVisibility: false))
{
var memory = GetTargetMemory(_selectedAiTarget, false);
var memory = GetTargetMemory(_selectedAiTarget);
if (memory != null)
{
memory.Location = _selectedAiTarget.WorldPosition;
@@ -3643,7 +3662,11 @@ namespace Barotrauma
{
isStateChanged = true;
SetStateResetTimer();
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
if (!Character.IsPet || !target.IsHuman)
{
//don't turn pets hostile to all humans when attacked by one
ChangeParams(target.SpeciesName, state, priority, ignoreAttacksIfNotInSameSub: !target.IsHuman);
}
if (target.IsHuman)
{
priority = GetTargetParams("human")?.Priority;
@@ -3934,7 +3957,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))
@@ -10,7 +10,7 @@ namespace Barotrauma
{
partial class HumanAIController : AIController
{
public static bool debugai;
public static bool DebugAI;
public static bool DisableCrewAI;
private readonly AIObjectiveManager objectiveManager;
@@ -42,6 +42,8 @@ namespace Barotrauma
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>();
private readonly HashSet<Hull> dirtyHullSafetyCalculations = new HashSet<Hull>();
private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f;
private bool wasConscious;
@@ -55,6 +57,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 +95,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 +231,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 +251,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 +323,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 +369,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))
@@ -420,6 +438,7 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls)
{
PropagateHullSafety(Character, h);
dirtyHullSafetyCalculations.Remove(h);
}
}
else
@@ -427,18 +446,33 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls)
{
RefreshHullSafety(h);
dirtyHullSafetyCalculations.Remove(h);
}
}
foreach (Hull h in dirtyHullSafetyCalculations)
{
RefreshHullSafety(h);
}
}
dirtyHullSafetyCalculations.Clear();
if (reportProblemsTimer <= 0.0f)
{
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();
}
@@ -590,7 +624,7 @@ namespace Barotrauma
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
Character.CurrentHull.IsWetRoom;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo goTo && goTo.Target == Character;
bool IsOrderedToWait() => Character.IsOnPlayerTeam && ObjectiveManager.CurrentOrder is AIObjectiveGoTo { IsWaitOrder: true };
bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
{
@@ -875,7 +909,7 @@ namespace Barotrauma
var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { return 0; }
var rootContainer = container.Item.GetRootContainer() ?? container.Item;
var rootContainer = container.Item.RootContainer ?? container.Item;
if (rootContainer.GetComponent<Fabricator>() != null || rootContainer.GetComponent<Deconstructor>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{
@@ -906,20 +940,40 @@ 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;
private const float RefuseDraggingAfter = 10.0f;
/// <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 ||
@@ -931,8 +985,8 @@ namespace Barotrauma
}
draggedTimer += deltaTime;
if (draggedTimer > RefuseDraggingAfter ||
(draggedTimer > 0.5f && refuseDraggingTimer > 0.0f))
if (draggedTimer > RefuseDraggingThresholdHigh ||
(refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow))
{
draggedTimer = 0.0f;
refuseDraggingTimer = RefuseDraggingDuration;
@@ -945,7 +999,8 @@ namespace Barotrauma
{
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>();
@@ -1044,25 +1099,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
}
}
}
}
@@ -1093,21 +1144,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 > AfflictionPrefab.Bleeding.TreatmentThreshold && !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);
}
}
}
@@ -1236,7 +1299,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);
}
@@ -1402,7 +1465,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;
@@ -1587,7 +1650,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)
@@ -1604,7 +1667,7 @@ namespace Barotrauma
/// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true)
=> HasItem(character, AIObjectiveFindDivingGear.HEAVY_DIVING_GEAR, out _, requireOxygenTank ? AIObjectiveFindDivingGear.OXYGEN_SOURCE : Identifier.Empty, conditionPercentage, requireEquipped: true,
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes));
predicate: (Item item) => character.HasEquippedItem(item, InvSlotType.OuterClothes | InvSlotType.InnerClothes));
/// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen.
@@ -1837,18 +1900,22 @@ namespace Barotrauma
private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
/// <summary>
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threads.
/// Updates the hull safety for all ai characters in the team. The idea is that the crew communicates (magically) via radio about the threats.
/// The safety levels need to be calculated for each bot individually, because the formula takes into account things like current orders.
/// There's now a cached value per each hull, which should prevent too frequent calculations.
/// </summary>
public static void PropagateHullSafety(Character character, Hull hull)
{
DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull));
DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
}
public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
private void RefreshHullSafety(Hull hull)
{
if (GetHullSafety(hull, Character, VisibleHulls) > HULL_SAFETY_THRESHOLD)
var visibleHulls = dirtyHullSafetyCalculations.Contains(hull) ? hull.GetConnectedHulls(includingThis: true, searchDepth: 1) : VisibleHulls;
float hullSafety = GetHullSafety(hull, Character, visibleHulls);
if (hullSafety > HULL_SAFETY_THRESHOLD)
{
UnsafeHulls.Remove(hull);
}
@@ -1916,7 +1983,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>();
@@ -1933,7 +2000,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));
}
@@ -2108,7 +2175,7 @@ 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
{
@@ -2120,9 +2187,11 @@ namespace Barotrauma
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
{
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
//NPCs that allow some campaign interaction are not turned hostile by low reputation
if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; }
if (!npc.IsEscorted && npc.AIController is HumanAIController npcAI)
if (npc.AIController is HumanAIController npcAI)
{
return !npcAI.IsInHostileFaction();
}
@@ -2134,6 +2203,7 @@ namespace Barotrauma
public bool IsInHostileFaction()
{
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;
@@ -2145,8 +2215,7 @@ namespace Barotrauma
}
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{
var reputation = campaign.Map?.CurrentLocation?.Reputation;
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
if (campaign.CurrentLocation is { IsFactionHostile: true })
{
return true;
}
@@ -2154,71 +2223,89 @@ namespace Barotrauma
return false;
}
public static bool IsActive(Character other) => other != null && !other.Removed && !other.IsDead && !other.IsUnconscious;
public static bool IsActive(Character c) => c != null && c.Enabled && !c.IsUnconscious;
public static bool IsTrueForAllCrewMembers(Character character, Func<HumanAIController, bool> predicate)
public static bool IsTrueForAllBotsInTheCrew(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c))
if (!IsBotInTheCrew(character, c)) { continue; }
if (!predicate(c.AIController as HumanAIController))
{
if (!predicate(c.AIController as HumanAIController))
{
return false;
}
return false;
}
}
}
return true;
}
public static bool IsTrueForAnyCrewMember(Character character, Func<HumanAIController, bool> predicate)
public static bool IsTrueForAnyBotInTheCrew(Character character, Func<HumanAIController, bool> predicate)
{
if (character == null) { return false; }
foreach (var c in Character.CharacterList)
{
if (FilterCrewMember(character, c))
if (!IsBotInTheCrew(character, c)) { continue; }
if (predicate(c.AIController as HumanAIController))
{
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 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);
}
@@ -2238,7 +2325,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)
{
@@ -2283,10 +2370,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)
@@ -2354,9 +2440,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)
{
@@ -2370,7 +2456,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;
@@ -2405,11 +2491,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
}
}
@@ -22,7 +22,10 @@ namespace Barotrauma
private readonly Character character;
private Vector2 currentTarget;
/// <summary>
/// In sim units.
/// </summary>
private Vector2 currentTargetPos;
private float findPathTimer;
@@ -40,11 +43,6 @@ namespace Barotrauma
get { return pathFinder; }
}
public Vector2 CurrentTarget
{
get { return currentTarget; }
}
public bool IsPathDirty
{
get;
@@ -54,9 +52,9 @@ namespace Barotrauma
/// <summary>
/// Returns true if any node in the path is in stairs
/// </summary>
public bool InStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool PathHasStairs => currentPath != null && currentPath.Nodes.Any(n => n.Stairs != null);
public bool IsCurrentNodeLadder => currentPath?.CurrentNode?.Ladders != null && currentPath.CurrentNode.Ladders.Item.IsInteractable(character);
public bool IsCurrentNodeLadder => GetCurrentLadder() != null;
public bool IsNextNodeLadder => GetNextLadder() != null;
@@ -64,14 +62,9 @@ namespace Barotrauma
{
get
{
if (currentPath == null) { return false; }
if (currentPath.CurrentNode == null) { return false; }
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
var currentLadder = GetCurrentLadder();
if (currentLadder == null) { return false; }
if (!currentLadder.Item.IsInteractable(character)) { return false; }
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
return currentLadder == GetNextLadder();
}
}
@@ -107,13 +100,10 @@ namespace Barotrauma
findPathTimer -= step;
}
public void SetPath(SteeringPath path)
public void SetPath(Vector2 targetPos, SteeringPath path)
{
currentTargetPos = targetPos;
currentPath = path;
if (path.Nodes.Any())
{
currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
}
findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false;
}
@@ -136,66 +126,28 @@ namespace Barotrauma
steering += addition;
}
/// <summary>
/// Seeks the ladder from the next and next + 1 nodes.
/// </summary>
public Ladder GetNextLadder()
{
if (currentPath == null) { return null; }
if (currentPath.NextNode == null) { return null; }
if (currentPath.NextNode.Ladders != null && currentPath.NextNode.Ladders.Item.IsInteractable(character))
{
return currentPath.NextNode.Ladders;
}
else
{
int index = currentPath.CurrentIndex + 2;
if (currentPath.Nodes.Count > index)
{
var node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
//if the next node is a hatch, check if the node after that is a ladder
else if (node.ConnectedDoor != null && node.ConnectedDoor.IsHorizontal)
{
index++;
if (currentPath.Nodes.Count > index)
{
node = currentPath.Nodes[index];
if (node == null) { return null; }
if (node.Ladders != null && node.Ladders.Item.IsInteractable(character))
{
return node.Ladders;
}
}
}
public Ladder GetCurrentLadder() => GetLadder(currentPath?.CurrentNode);
}
return null;
public Ladder GetNextLadder() => GetLadder(currentPath?.NextNode);
private Ladder GetLadder(WayPoint wp)
{
if (wp?.Ladders?.Item is Item item && item.IsInteractable(character))
{
return wp.Ladders;
}
return null;
}
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;
if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null)
{
//target in a different sub than where the character is now
//take that into account when calculating if the target has moved
Submarine currentPathSub = currentPath?.CurrentNode?.Submarine;
if (currentPathSub == character.Submarine) { currentPathSub = currentPath?.Nodes.LastOrDefault()?.Submarine; }
if (currentPathSub != character.Submarine && targetDiff.LengthSquared() > 1 && currentPathSub != null)
{
Vector2 subDiff = character.Submarine.SimPosition - currentPathSub.SimPosition;
targetDiff += subDiff;
}
}
// If the target has moved, we need a new path.
// Different subs are already taken into account before setting the target.
// Triggers when either the target or we have changed subs, but only once (until the new path has been accepted).
Vector2 targetDiff = target - currentTargetPos;
if (targetDiff.LengthSquared() > 1)
{
needsNewPath = true;
@@ -205,15 +157,33 @@ namespace Barotrauma
if (needsNewPath || findPathTimer < -1.0f)
{
IsPathDirty = true;
if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp)
{
if (character.Submarine != null && wp.Ladders == null && wp.ConnectedDoor == null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
{
// Not moving -> need a new path.
needsNewPath = true;
}
if (character.Submarine == null && 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();
currentTarget = target;
currentTargetPos = target;
Vector2 currentPos = host.SimPosition;
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).
@@ -234,6 +204,14 @@ namespace Barotrauma
useNewPath = Vector2.DistanceSquared(character.WorldPosition, currentPath.CurrentNode.WorldPosition) > Math.Pow(Vector2.Distance(character.WorldPosition, newPath.Nodes.First().WorldPosition) * 3, 2);
}
}
if (!useNewPath && !character.CanSeeTarget(currentPath.CurrentNode))
{
// If we are set to disregard the new path, ensure that we can actually see the current node of the old path,
// because it's possible that there's e.g. a closed door between us and the current node,
// and in that case we'd want to use the new path instead of the old.
// There's visibility checks in the pathfinder calls, so the new path should always be ok.
useNewPath = true;
}
bool IsIdenticalPath()
{
@@ -312,6 +290,7 @@ namespace Barotrauma
//if not in water and the waypoint is between the top and bottom of the collider, no need to move vertically
if (canClimb && !character.AnimController.InWater && !character.IsClimbing && diff.Y < collider.Height / 2 + collider.Radius)
{
// TODO: might cause some edge cases -> do we need this?
diff.Y = 0.0f;
}
if (diff == Vector2.Zero) { return Vector2.Zero; }
@@ -328,12 +307,12 @@ namespace Barotrauma
}
if (currentPath.Finished)
{
Vector2 pos2 = host.SimPosition;
Vector2 hostPosition = host.SimPosition;
if (character != null && character.Submarine == null && CurrentPath.Nodes.Count > 0 && CurrentPath.Nodes.Last().Submarine != null)
{
pos2 -= CurrentPath.Nodes.Last().Submarine.SimPosition;
hostPosition -= CurrentPath.Nodes.Last().Submarine.SimPosition;
}
return currentTarget - pos2;
return currentTargetPos - hostPosition;
}
bool doorsChecked = false;
checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime());
@@ -353,14 +332,46 @@ namespace Barotrauma
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
Ladder currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder != null && !currentLadder.Item.IsInteractable(character))
{
currentLadder = null;
}
Ladder currentLadder = GetCurrentLadder();
Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder;
bool useLadders = canClimb && ladders != null && steering.LengthSquared() > 0.1f && (!isDiving || steering.Y > 1);
bool useLadders = canClimb && ladders != null;
var collider = character.AnimController.Collider;
Vector2 colliderSize = collider.GetSize();
if (useLadders)
{
if (character.IsClimbing && Math.Abs(diff.X) - ConvertUnits.ToDisplayUnits(colliderSize.X) > Math.Abs(diff.Y))
{
// If the current node is horizontally farther from us than vertically, we don't want to keep climbing the ladders.
useLadders = false;
}
else if (!character.IsClimbing && currentPath.NextNode != null && nextLadder == null)
{
Vector2 diffToNextNode = currentPath.NextNode.WorldPosition - pos;
if (Math.Abs(diffToNextNode.X) > Math.Abs(diffToNextNode.Y))
{
// If the next node is horizontally farther from us than vertically, we don't want to start climbing.
useLadders = false;
}
}
else if (isDiving && steering.Y < 1)
{
// When diving, only use ladders to get upwards (towards the surface), otherwise we can just ignore them.
useLadders = false;
}
}
if (character.IsClimbing && !useLadders)
{
if (currentPath.IsAtEndNode && canClimb && ladders != null)
{
// Don't release the ladders when ending a path in ladders.
useLadders = true;
}
else
{
character.StopClimbing();
}
}
if (useLadders && character.SelectedSecondaryItem != ladders.Item)
{
if (character.CanInteractWith(ladders.Item))
@@ -380,56 +391,76 @@ namespace Barotrauma
}
}
}
var collider = character.AnimController.Collider;
if (character.IsClimbing && !useLadders)
{
character.StopClimbing();
}
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 && character.SelectedSecondaryItem == nextLadder.Item)
{
//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 = currentLadder == nextLadder;
if (currentLadder != null && nextLadder != null)
{
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 colliderHeight = collider.Height / 2 + collider.Radius;
float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
if (heightDiff < colliderHeight * 1.25f)
{
// Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
if (nextLadder != null && !nextLadderSameAsCurrent)
{
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
// Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
{
if (nextLadder.Item.TryInteract(character, forceSelectKey: true))
{
NextNode(!doorsChecked);
}
}
}
bool isAboveFloor;
if (diff.Y < 0)
{
// When climbing down, let's use the collider bottom to prevent getting stuck at the bottom of the ladders.
float colliderBottom = character.AnimController.Collider.SimPosition.Y;
float floorY = character.AnimController.FloorY;
isAboveFloor = colliderBottom > floorY;
}
else
{
// When climbing up, let's use the lowest collider (feet).
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative,
// when a foot is still below the platform.
float heightFromFloor = character.AnimController.GetHeightFromFloor();
isAboveFloor = heightFromFloor > -0.1f;
}
if (isAboveFloor)
{
if (Math.Abs(diff.Y) < distanceMargin)
{
NextNode(!doorsChecked);
}
else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
{
// Can't skip the node -> Release the ladders, because the next node is not on a ladder or it's horizontally too far.
character.StopClimbing();
}
}
}
if (!currentPath.IsAtEndNode && (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10))
else if (currentLadder != null && currentPath.NextNode != 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);
@@ -440,7 +471,6 @@ namespace Barotrauma
if (door == null || door.CanBeTraversed)
{
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1));
Vector2 colliderSize = collider.GetSize();
float targetDistance = Math.Max(Math.Max(colliderSize.X, colliderSize.Y) / 2 * margin, 0.5f);
float horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
@@ -459,7 +489,6 @@ namespace Barotrauma
{
// Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity;
// If the character is very short, it would fail to use the waypoint nodes because they are always too high.
// If the character is very thin, it would often fail to reach the waypoints, because the horizontal distance is too small.
@@ -486,9 +515,12 @@ 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)
{
NextNode(!doorsChecked);
if (door is not { CanBeTraversed: false } && (currentLadder == null || nextLadder == null))
{
NextNode(!doorsChecked);
}
}
}
if (currentPath.CurrentNode == null)
@@ -507,9 +539,9 @@ namespace Barotrauma
currentPath.SkipToNextNode();
}
private bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
public bool CanAccessDoor(Door door, Func<Controller, bool> buttonFilter = null)
{
if (door.IsBroken) { return true; }
if (door.CanBeTraversed) { return true; }
if (door.IsClosed)
{
if (!door.Item.IsInteractable(character)) { return false; }
@@ -605,10 +637,12 @@ namespace Barotrauma
{
//the node we're heading towards is the last one in the path, and at a door
//the door needs to be open for the character to reach the node
if (currentWaypoint.ConnectedDoor.LinkedGap != null)
if (currentWaypoint.ConnectedDoor.LinkedGap is Gap linkedGap)
{
// Keep the airlock doors closed, but not in ruins/wrecks
if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false } || currentWaypoint.Submarine == null || currentWaypoint.Submarine.Info.IsRuin || currentWaypoint.Submarine.Info.IsWreck)
if (currentWaypoint.Submarine == null ||
currentWaypoint.Submarine.Info is { IsPlayer: false } ||
!linkedGap.IsRoomToRoom ||
(linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
{
shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor;
@@ -213,7 +213,7 @@ namespace Barotrauma
{
foreach (Voronoi2.GraphEdge edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
{
Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos);
@@ -506,15 +506,31 @@ namespace Barotrauma
}
}
protected static bool CanEquip(Character character, Item item)
public virtual void SpeakAfterOrderReceived() { }
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 +546,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()

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