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: attributes:
label: Reproduction steps label: Reproduction steps
description: | 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. 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: | placeholder: |
1. Start a multiplayer campaign 1. Start a multiplayer campaign
2. Spawn a bike horn with console commands 2. Spawn a bike horn with console commands
3. Use the bike horn 3. Use the bike horn
4. Observe how the game crashes 4. Observe how the game crashes
validations:
required: true
- type: dropdown - type: dropdown
id: prevalence id: prevalence
attributes: attributes:
@@ -52,9 +54,7 @@ body:
label: Version 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. 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: options:
- 0.21.6.0 - v1.0.20.1
- 0.21.6.0 (Unstable)
- Faction/endgame test branch
- Other - Other
validations: validations:
required: true required: true
@@ -19,7 +19,7 @@ namespace Barotrauma
var target = _selectedAiTarget ?? _lastAiTarget; var target = _selectedAiTarget ?? _lastAiTarget;
if (target != null && target.Entity != null) if (target != null && target.Entity != null)
{ {
var memory = GetTargetMemory(target, false); var memory = GetTargetMemory(target);
if (memory != null) if (memory != null)
{ {
Vector2 targetPos = memory.Location; Vector2 targetPos = memory.Location;
@@ -10,7 +10,7 @@ namespace Barotrauma
public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch) public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
{ {
if (Character == Character.Controlled) { return; } if (Character == Character.Controlled) { return; }
if (!debugai) { return; } if (!DebugAI) { return; }
Vector2 pos = Character.WorldPosition; Vector2 pos = Character.WorldPosition;
pos.Y = -pos.Y; pos.Y = -pos.Y;
Vector2 textOffset = new Vector2(-40, -160); Vector2 textOffset = new Vector2(-40, -160);
@@ -6,11 +6,17 @@ namespace Barotrauma
{ {
partial class Attack partial class Attack
{ {
[Serialize("StructureBlunt", IsPropertySaveable.Yes), Editable()] [Serialize("StructureBlunt", IsPropertySaveable.Yes, description: "Name of the sound effect the attack makes when it hits a structure."), Editable()]
public string StructureSoundType { get; private set; } public string StructureSoundType { get; private set; }
/// <summary>
/// Sound to play when the attack deals damage.
/// </summary>
private RoundSound sound; private RoundSound sound;
/// <summary>
/// Particle emitter to use when the attack deals damage.
/// </summary>
private ParticleEmitter particleEmitter; private ParticleEmitter particleEmitter;
partial void InitProjSpecific(ContentXElement element) partial void InitProjSpecific(ContentXElement element)
@@ -582,7 +582,7 @@ namespace Barotrauma
float closestItemDistance = Math.Max(aimAssistAmount, 2.0f); float closestItemDistance = Math.Max(aimAssistAmount, 2.0f);
foreach (MapEntity entity in entityList) foreach (MapEntity entity in entityList)
{ {
if (!(entity is Item item)) if (entity is not Item item)
{ {
continue; continue;
} }
@@ -214,7 +214,6 @@ namespace Barotrauma
double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI; double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI;
cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f; cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f;
TransformCursorPos();
bool ragdollInput = msg.ReadBoolean(); bool ragdollInput = msg.ReadBoolean();
keys[(int)InputType.Ragdoll].Held = ragdollInput; keys[(int)InputType.Ragdoll].Held = ragdollInput;
@@ -850,7 +850,7 @@ namespace Barotrauma
if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover) if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover)
{ {
//highlight the slot the treatment item is in //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); var index = Character.Controlled.Inventory.FindIndex(rootContainer);
if (Character.Controlled.Inventory.visualSlots != null && index > -1 && index < Character.Controlled.Inventory.visualSlots.Length && if (Character.Controlled.Inventory.visualSlots != null && index > -1 && index < Character.Controlled.Inventory.visualSlots.Length &&
Character.Controlled.Inventory.visualSlots[index].HighlightTimer <= 0.0f) Character.Controlled.Inventory.visualSlots[index].HighlightTimer <= 0.0f)
@@ -870,7 +870,7 @@ namespace Barotrauma
{ {
if (wearable.Type == WearableType.Hair) if (wearable.Type == WearableType.Hair)
{ {
if (HairWithHatSprite != null) if (HairWithHatSprite != null && !hideLimb)
{ {
DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect); DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect);
depthStep += step; depthStep += step;
@@ -86,7 +86,7 @@ namespace Barotrauma
public string ModVersion = ContentPackage.DefaultModVersion; public string ModVersion = ContentPackage.DefaultModVersion;
public Md5Hash? ExpectedHash { get; private set; } public Md5Hash? ExpectedHash { get; set; }
public bool IsCore = false; public bool IsCore = false;
@@ -125,9 +125,11 @@ namespace Barotrauma
public static string IncrementModVersion(string modVersion) public static string IncrementModVersion(string modVersion)
{ {
if (string.IsNullOrWhiteSpace(modVersion)) { return string.Empty; }
//look for an integer at the end of the string and increment it //look for an integer at the end of the string and increment it
int startIndex = modVersion.Length - 1; int startIndex = modVersion.Length - 1;
while (char.IsDigit(modVersion[startIndex])) { startIndex--; } while (startIndex > 0 && char.IsDigit(modVersion[startIndex])) { startIndex--; }
startIndex++; startIndex++;
if (startIndex >= modVersion.Length if (startIndex >= modVersion.Length
@@ -9,6 +9,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text; 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) => commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) =>
{ {
if (Character.Controlled == null) { return; } if (Character.Controlled == null) { return; }
WikiImage.Create(Character.Controlled); try
{
WikiImage.Create(Character.Controlled);
}
catch (Exception e)
{
DebugConsole.ThrowError("The command 'wikiimage_character' failed.", e);
}
})); }));
commands.Add(new Command("wikiimage_sub", "Save an image of the main submarine with a transparent background.", (string[] args) => commands.Add(new Command("wikiimage_sub", "Save an image of the main submarine with a transparent background.", (string[] args) =>
{ {
if (Submarine.MainSub == null) { return; } if (Submarine.MainSub == null) { return; }
MapEntity.SelectedList.Clear(); try
MapEntity.ClearHighlightedEntities(); {
WikiImage.Create(Submarine.MainSub); MapEntity.SelectedList.Clear();
MapEntity.ClearHighlightedEntities();
WikiImage.Create(Submarine.MainSub);
}
catch (Exception e)
{
DebugConsole.ThrowError("The command 'wikiimage_sub' failed.", e);
}
})); }));
AssignRelayToServer("kick", false); AssignRelayToServer("kick", false);
@@ -1141,6 +1156,15 @@ namespace Barotrauma
GameMain.LightManager.DebugLos = state; GameMain.LightManager.DebugLos = state;
NewMessage("Los debug draw mode " + (GameMain.LightManager.DebugLos ? "enabled" : "disabled"), Color.Yellow); 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); AssignRelayToServer("debugdraw", false);
AssignOnExecute("devmode", (string[] args) => AssignOnExecute("devmode", (string[] args) =>
@@ -1263,8 +1287,8 @@ namespace Barotrauma
AssignOnExecute("debugai", (string[] args) => AssignOnExecute("debugai", (string[] args) =>
{ {
HumanAIController.debugai = !HumanAIController.debugai; HumanAIController.DebugAI = !HumanAIController.DebugAI;
if (HumanAIController.debugai) if (HumanAIController.DebugAI)
{ {
GameMain.DevMode = true; GameMain.DevMode = true;
GameMain.DebugDraw = true; GameMain.DebugDraw = true;
@@ -1279,7 +1303,7 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = true; GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = 1f; GameMain.LightManager.LosAlpha = 1f;
} }
NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.Yellow); NewMessage(HumanAIController.DebugAI ? "AI debug info visible" : "AI debug info hidden", Color.Yellow);
}); });
AssignRelayToServer("debugai", false); AssignRelayToServer("debugai", false);
@@ -2338,7 +2362,7 @@ namespace Barotrauma
{ {
if (mapEntity is Item item) if (mapEntity is Item item)
{ {
item.Rect = new Rectangle(item.Rect.X, item.Rect.Y, item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y,
(int)(item.Prefab.Sprite.size.X * item.Prefab.Scale), (int)(item.Prefab.Sprite.size.X * item.Prefab.Scale),
(int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale)); (int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale));
} }
@@ -2811,7 +2835,26 @@ namespace Barotrauma
ContentPackageManager.EnabledPackages.ReloadCore(); 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) => /*commands.Add(new Command("ingamemodswap", "", (string[] args) =>
{ {
ContentPackage.IngameModSwap = !ContentPackage.IngameModSwap; ContentPackage.IngameModSwap = !ContentPackage.IngameModSwap;
@@ -662,34 +662,36 @@ namespace Barotrauma
Identifier missionIdentifier = msg.ReadIdentifier(); Identifier missionIdentifier = msg.ReadIdentifier();
int locationIndex = msg.ReadInt32(); int locationIndex = msg.ReadInt32();
int destinationIndex = msg.ReadInt32(); int destinationIndex = msg.ReadInt32();
string missionName = msg.ReadString(); string missionName = msg.ReadString();
MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier); if (Screen.Selected != GameMain.NetLobbyScreen)
if (prefab != null)
{ {
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName), MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier);
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128)) if (prefab != null)
{ {
IconColor = prefab.IconColor 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))
if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count) {
{ IconColor = prefab.IconColor
Location location = map.Locations[locationIndex]; };
map.Discover(location, checkTalents: false); 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; LocationConnection? connection = null;
if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count) if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count)
{ {
Location destination = map.Locations[destinationIndex]; Location destination = map.Locations[destinationIndex];
connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination)); connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination));
} }
if (connection != null) if (connection != null)
{ {
location.UnlockMission(prefab, connection); location.UnlockMission(prefab, connection);
} }
else else
{ {
location.UnlockMission(prefab); location.UnlockMission(prefab);
}
} }
} }
} }
@@ -88,13 +88,14 @@ namespace Barotrauma
public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element) public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element)
=> TextManager.SpeciallyHandledCharCategories => TextManager.SpeciallyHandledCharCategories
.Where(category => element.GetAttributeBool($"is{category}", category switch { .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.CJK => false,
TextManager.SpeciallyHandledCharCategory.Japanese => false,
// For backwards compatibility, we assume that Cyrillic is supported by default // For backwards compatibility, we assume that Cyrillic is supported by default
TextManager.SpeciallyHandledCharCategory.Cyrillic => true, TextManager.SpeciallyHandledCharCategory.Cyrillic => true,
_ => throw new Exception("unreachable") _ => throw new NotImplementedException($"nameof{category} not implemented.")
})) }))
.Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category); .Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category);
@@ -517,6 +518,10 @@ namespace Barotrauma
GlyphData gd = GetGlyphData(charIndex); GlyphData gd = GetGlyphData(charIndex);
if (gd.TexIndex >= 0) if (gd.TexIndex >= 0)
{ {
if (gd.TexIndex < 0 || gd.TexIndex >= textures.Count)
{
throw new ArgumentOutOfRangeException($"Error while rendering text. Texture index was out of range. Text: {text}, char: {charIndex} index: {gd.TexIndex}, texture count: {textures.Count}");
}
Texture2D tex = textures[gd.TexIndex]; Texture2D tex = textures[gd.TexIndex];
Vector2 drawOffset; Vector2 drawOffset;
drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y; drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y;
@@ -533,7 +533,7 @@ namespace Barotrauma
{ {
if (characterInfo.MinReputationToHire.factionId != Identifier.Empty) 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; return false;
} }
@@ -532,6 +532,7 @@ namespace Barotrauma
void drawRect(Vector2 topLeft, Vector2 bottomRight) void drawRect(Vector2 topLeft, Vector2 bottomRight)
{ {
int minWidth = GUI.IntScale(5); 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; } if (bottomRight.X - topLeft.X < minWidth) { bottomRight.X = topLeft.X + minWidth; }
GUI.DrawRectangle(spriteBatch, GUI.DrawRectangle(spriteBatch,
Rect.Location.ToVector2() + topLeft, 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); IEnumerable<GUITextBox> GetAndSortTextBoxes(GUIComponent parent) => parent.GetAllChildren<GUITextBox>().OrderBy(t => t.Rect.Y).ThenBy(t => t.Rect.X);
GUITextBox SelectNextTextBox(GUIListBox listBox) GUITextBox SelectNextTextBox(GUIListBox listBox)
{ {
if (listBox?.SelectedComponent == null) { return null; }
var textBoxes = GetAndSortTextBoxes(listBox.SelectedComponent); var textBoxes = GetAndSortTextBoxes(listBox.SelectedComponent);
if (textBoxes.Any()) if (textBoxes.Any())
{ {
@@ -240,7 +240,7 @@ namespace Barotrauma
private void UpdatePending() private void UpdatePending()
{ {
if (!(pendingHealList is { } healList)) { return; } if (pendingHealList is not { } healList) { return; }
ImmutableArray<MedicalClinic.NetCrewMember> pendingList = medicalClinic.PendingHeals.ToImmutableArray(); ImmutableArray<MedicalClinic.NetCrewMember> pendingList = medicalClinic.PendingHeals.ToImmutableArray();
@@ -493,20 +493,26 @@ namespace Barotrauma
GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone")) GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone"))
{ {
OnClicked = (_, _) => OnClicked = (button, _) =>
{ {
if (isWaitingForServer) { return true; }
button.Enabled = false;
isWaitingForServer = true; isWaitingForServer = true;
medicalClinic.TreatAllButtonAction(OnReceived);
bool wasSuccessful = medicalClinic.TreatAllButtonAction(_ => ReEnableButton());
if (!wasSuccessful) { ReEnableButton(); }
void ReEnableButton()
{
isWaitingForServer = false;
button.Enabled = true;
}
return true; return true;
} }
}; };
crewHealList = new CrewHealList(crewList, parent, treatAllButton); crewHealList = new CrewHealList(crewList, parent, treatAllButton);
void OnReceived(MedicalClinic.CallbackOnlyRequest obj)
{
isWaitingForServer = false;
}
} }
private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel) private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel)
@@ -585,8 +591,10 @@ namespace Barotrauma
OnClicked = (button, _) => OnClicked = (button, _) =>
{ {
button.Enabled = false; button.Enabled = false;
medicalClinic.HealAllButtonAction(request => isWaitingForServer = true;
bool wasSuccessful = medicalClinic.HealAllButtonAction(request =>
{ {
isWaitingForServer = false;
switch (request.HealResult) switch (request.HealResult)
{ {
case MedicalClinic.HealRequestResult.InsufficientFunds: case MedicalClinic.HealRequestResult.InsufficientFunds:
@@ -600,6 +608,12 @@ namespace Barotrauma
button.Enabled = true; button.Enabled = true;
ClosePopup(); ClosePopup();
}); });
if (!wasSuccessful)
{
isWaitingForServer = false;
button.Enabled = true;
}
ClosePopup(); ClosePopup();
return true; return true;
} }
@@ -610,11 +624,19 @@ namespace Barotrauma
ClickSound = GUISoundType.Cart, ClickSound = GUISoundType.Cart,
OnClicked = (button, _) => OnClicked = (button, _) =>
{ {
if (isWaitingForServer) { return true; }
button.Enabled = false; button.Enabled = false;
medicalClinic.ClearAllButtonAction(_ => isWaitingForServer = true;
bool wasSuccessful = medicalClinic.ClearAllButtonAction(_ => ReEnableButton());
if (!wasSuccessful) { ReEnableButton(); }
void ReEnableButton()
{ {
isWaitingForServer = false;
button.Enabled = true; button.Enabled = true;
}); }
return true; return true;
} }
}; };
@@ -701,10 +723,15 @@ namespace Barotrauma
OnClicked = (button, _) => OnClicked = (button, _) =>
{ {
button.Enabled = false; button.Enabled = false;
medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ => bool wasSuccessful = medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ =>
{ {
button.Enabled = true; button.Enabled = true;
}); });
if (!wasSuccessful)
{
button.Enabled = true;
}
return true; return true;
} }
}; };
@@ -792,7 +819,13 @@ namespace Barotrauma
selectedCrewAfflictionList = popupAfflictionList; selectedCrewAfflictionList = popupAfflictionList;
isWaitingForServer = true; isWaitingForServer = true;
medicalClinic.RequestAfflictions(info, OnReceived); bool wasSuccessful = medicalClinic.RequestAfflictions(info, OnReceived);
if (!wasSuccessful)
{
isWaitingForServer = false;
ClosePopup();
}
void OnReceived(MedicalClinic.AfflictionRequest request) void OnReceived(MedicalClinic.AfflictionRequest request)
{ {
@@ -800,6 +833,16 @@ namespace Barotrauma
if (request.Result != MedicalClinic.RequestResult.Success) if (request.Result != MedicalClinic.RequestResult.Success)
{ {
switch (request.Result)
{
case MedicalClinic.RequestResult.CharacterInfoMissing:
DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName}\" in medical clini because the character health was missing.");
break;
case MedicalClinic.RequestResult.CharacterNotFound:
DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName} in medical clinic because the server was unable to find a character with ID {info.ID}.");
break;
}
feedbackBlock.Text = GetErrorText(request.Result); feedbackBlock.Text = GetErrorText(request.Result);
feedbackBlock.TextColor = GUIStyle.Red; feedbackBlock.TextColor = GUIStyle.Red;
return; return;
@@ -953,14 +996,20 @@ namespace Barotrauma
} }
existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray(); existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray();
ToggleElements(ElementState.Disabled, elementsToDisable); ToggleElements(ElementState.Disabled, elementsToDisable);
medicalClinic.AddPendingButtonAction(existingMember, request => bool wasSuccessful = medicalClinic.AddPendingButtonAction(existingMember, request =>
{ {
if (request.Result == MedicalClinic.RequestResult.Timeout) if (request.Result == MedicalClinic.RequestResult.Timeout)
{ {
ToggleElements(ElementState.Enabled, elementsToDisable); ToggleElements(ElementState.Enabled, elementsToDisable);
} }
}); });
if (!wasSuccessful)
{
ToggleElements(ElementState.Enabled, elementsToDisable);
}
} }
#warning TODO: this doesn't seem like the right place for this, and it's not clear from the method signature how this differs from ToolBox.LimitString #warning TODO: this doesn't seem like the right place for this, and it's not clear from the method signature how this differs from ToolBox.LimitString
@@ -1090,9 +1139,8 @@ namespace Barotrauma
{ {
return result switch return result switch
{ {
MedicalClinic.RequestResult.Error => TextManager.Get("error"),
MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"), MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"),
_ => "What the hell did you just do" // this should never happen _ => TextManager.Get("error")
}; };
} }
@@ -175,6 +175,7 @@ namespace Barotrauma
{ {
if (relativeOffset.NearlyEquals(value)) { return; } if (relativeOffset.NearlyEquals(value)) { return; }
relativeOffset = value; relativeOffset = value;
recalculateRect = true;
RecalculateChildren(false, false); RecalculateChildren(false, false);
} }
} }
@@ -870,7 +870,7 @@ namespace Barotrauma
{ {
foreach (var minRep in priceInfo.MinReputation) foreach (var minRep in priceInfo.MinReputation)
{ {
if (campaign.GetReputation(minRep.Key) < minRep.Value) if (MathF.Round(campaign.GetReputation(minRep.Key)) < minRep.Value)
{ {
return minRep; return minRep;
} }
@@ -1930,7 +1930,7 @@ namespace Barotrauma
"campaignstore.reputationrequired", "campaignstore.reputationrequired",
("[amount]", ((int)requiredReputation.Value.Value).ToString()), ("[amount]", ((int)requiredReputation.Value.Value).ToString()),
("[faction]", TextManager.Get("faction." + requiredReputation.Value.Key).Value)); ("[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; GUIStyle.Orange : GUIStyle.Green;
toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖"; toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖";
} }
@@ -807,8 +807,10 @@ namespace Barotrauma
{ {
if (GameMain.Client == null) if (GameMain.Client == null)
{ {
GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); if (GameMain.GameSession.TryPurchaseSubmarine(selectedSubmarine))
GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch); {
GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch);
}
RefreshSubmarineDisplay(true); RefreshSubmarineDisplay(true);
} }
else else
@@ -829,7 +831,7 @@ namespace Barotrauma
{ {
if (GameMain.Client == null) if (GameMain.Client == null)
{ {
GameMain.GameSession.PurchaseSubmarine(selectedSubmarine); GameMain.GameSession.TryPurchaseSubmarine(selectedSubmarine);
RefreshSubmarineDisplay(true); RefreshSubmarineDisplay(true);
} }
else else
@@ -23,9 +23,12 @@ namespace Barotrauma
private float votingTime = 100f; private float votingTime = 100f;
private float timer; private float timer;
private VoteType currentVoteType; private VoteType currentVoteType;
private Color SubmarineColor => GUIStyle.Orange; private static Color SubmarineColor => GUIStyle.Orange;
private Point createdForResolution; 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) public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float votingTime)
{ {
if (starter == null || info == null) { return null; } if (starter == null || info == null) { return null; }
@@ -664,7 +664,10 @@ namespace Barotrauma
while (Timing.Accumulator >= Timing.Step) while (Timing.Accumulator >= Timing.Step)
{ {
Timing.TotalTime += Timing.Step; Timing.TotalTime += Timing.Step;
if (!Paused)
{
Timing.TotalTimeUnpaused += Timing.Step;
}
Stopwatch sw = new Stopwatch(); Stopwatch sw = new Stopwatch();
sw.Start(); sw.Start();
@@ -944,7 +947,10 @@ namespace Barotrauma
PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency); 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) if (performanceCounterTimer.ElapsedMilliseconds > 1000)
{ {
@@ -31,7 +31,7 @@ namespace Barotrauma
// Item must be in a non-equipment slot if possible // Item must be in a non-equipment slot if possible
if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; } if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; }
// Item must not be contained inside an item in an equipment slot // 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; return true;
}, recursive: true).Distinct(); }, recursive: true).Distinct();
@@ -166,6 +166,9 @@ namespace Barotrauma
if (Submarine.MainSub == null || Level.Loaded == null) { return; } if (Submarine.MainSub == null || Level.Loaded == null) { return; }
bool allowEndingRound = false; bool allowEndingRound = false;
endRoundButton.Color = endRoundButton.Style.Color;
endRoundButton.HoverColor = endRoundButton.Style.HoverColor;
RichString overrideEndRoundButtonToolTip = string.Empty;
var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub); var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub);
LocalizedString buttonText = ""; LocalizedString buttonText = "";
switch (availableTransition) switch (availableTransition)
@@ -194,13 +197,23 @@ namespace Barotrauma
break; break;
case TransitionType.None: case TransitionType.None:
default: default:
if (Level.Loaded.Type == LevelData.LevelType.Outpost && bool inFriendlySub = Character.Controlled is { IsInFriendlySub: true };
!Level.Loaded.IsEndBiome && if (Level.Loaded.Type == LevelData.LevelType.Outpost && !Level.Loaded.IsEndBiome &&
(Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false))) (inFriendlySub || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false)))
{ {
if (Missions.Any(m => m is SalvageMission salvageMission && salvageMission.AnyTargetNeedsToBeRetrievedToSub))
{
overrideEndRoundButtonToolTip = TextManager.Get("SalvageTargetNotInSub");
endRoundButton.Color = GUIStyle.Red * 0.7f;
endRoundButton.HoverColor = GUIStyle.Red;
}
buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]"); buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]");
allowEndingRound = !ForceMapUI && !ShowCampaignUI; allowEndingRound = !ForceMapUI && !ShowCampaignUI;
} }
else
{
allowEndingRound = false;
}
break; break;
} }
if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted()) if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted())
@@ -227,7 +240,11 @@ namespace Barotrauma
prevCampaignUIAutoOpenType = availableTransition; prevCampaignUIAutoOpenType = availableTransition;
} }
endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5); endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5);
if (endRoundButton.Text != buttonText) if (overrideEndRoundButtonToolTip != string.Empty)
{
endRoundButton.ToolTip = overrideEndRoundButtonToolTip;
}
else if (endRoundButton.Text != buttonText)
{ {
endRoundButton.ToolTip = buttonText; endRoundButton.ToolTip = buttonText;
} }
@@ -193,7 +193,7 @@ namespace Barotrauma
if (GameMain.Client == null) if (GameMain.Client == null)
{ {
yield return CoroutineStatus.Failure; yield return CoroutineStatus.Success;
} }
if (GameMain.Client.LateCampaignJoin) if (GameMain.Client.LateCampaignJoin)
@@ -335,7 +335,7 @@ namespace Barotrauma
//-------------------------------------- //--------------------------------------
//wait for the new level to be loaded //wait for the new level to be loaded
DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, seconds: 60); DateTime timeOut = DateTime.Now + GameClient.LevelTransitionTimeOut;
while (Level.Loaded == prevLevel || Level.Loaded == null) while (Level.Loaded == prevLevel || Level.Loaded == null)
{ {
if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; } if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; }
@@ -345,7 +345,11 @@ namespace Barotrauma
endTransition.Stop(); endTransition.Stop();
overlayColor = Color.Transparent; 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 (Screen.Selected is not RoundSummaryScreen)
{ {
if (continueButton != null) if (continueButton != null)
@@ -17,7 +17,8 @@ namespace Barotrauma
{ {
Undecided, Undecided,
Success, Success,
Error, CharacterInfoMissing,
CharacterNotFound,
Timeout Timeout
} }
@@ -34,7 +35,9 @@ namespace Barotrauma
private readonly List<RequestAction<CallbackOnlyRequest>> addRequests = new List<RequestAction<CallbackOnlyRequest>>(); private readonly List<RequestAction<CallbackOnlyRequest>> addRequests = new List<RequestAction<CallbackOnlyRequest>>();
private readonly List<RequestAction<CallbackOnlyRequest>> removeRequests = new List<RequestAction<CallbackOnlyRequest>>(); private readonly List<RequestAction<CallbackOnlyRequest>> removeRequests = new List<RequestAction<CallbackOnlyRequest>>();
public void RequestAfflictions(CharacterInfo info, Action<AfflictionRequest> onReceived) private static readonly LeakyBucket requestBucket = new(RateLimitExpiry / (float)RateLimitMaxRequests, 10);
public bool RequestAfflictions(CharacterInfo info, Action<AfflictionRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
@@ -42,23 +45,26 @@ namespace Barotrauma
if (Screen.Selected is TestScreen) if (Screen.Selected is TestScreen)
{ {
onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray())); onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray()));
return; return true;
} }
#endif #endif
if (info is not { Character.CharacterHealth: { } health }) if (info is not { Character.CharacterHealth: { } health })
{ {
onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray<NetAffliction>.Empty)); onReceived.Invoke(new AfflictionRequest(RequestResult.CharacterInfoMissing, ImmutableArray<NetAffliction>.Empty));
return; return true;
} }
ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health).ToImmutableArray(); ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health);
onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions)); onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions));
return; return true;
} }
afflictionRequests.Add(new RequestAction<AfflictionRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
SendAfflictionRequest(info); {
afflictionRequests.Add(new RequestAction<AfflictionRequest>(onReceived, GetTimeout()));
SendAfflictionRequest(info);
});
} }
public void RequestLatestPending(Action<PendingRequest> onReceived) public void RequestLatestPending(Action<PendingRequest> onReceived)
@@ -66,8 +72,11 @@ namespace Barotrauma
// no need to worry about syncing when there's only one pair of eyes capable of looking at the UI // no need to worry about syncing when there's only one pair of eyes capable of looking at the UI
if (GameMain.IsSingleplayer) { return; } if (GameMain.IsSingleplayer) { return; }
pendingHealRequests.Add(new RequestAction<PendingRequest>(onReceived, GetTimeout())); requestBucket.TryEnqueue(() =>
SendPendingRequest(); {
pendingHealRequests.Add(new RequestAction<PendingRequest>(onReceived, GetTimeout()));
SendPendingRequest();
});
} }
public void Update(float deltaTime) public void Update(float deltaTime)
@@ -79,6 +88,7 @@ namespace Barotrauma
UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout); UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout);
requestBucket.Update(deltaTime);
static void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); } static void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); }
} }
@@ -146,21 +156,25 @@ namespace Barotrauma
return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault(); return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault();
} }
public void TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived) public bool TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
AddEverythingToPending(); AddEverythingToPending();
onReceived(new CallbackOnlyRequest(RequestResult.Success)); onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke(); OnUpdate?.Invoke();
return; return true;
} }
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable); {
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable);
});
} }
public void HealAllButtonAction(Action<HealRequest> onReceived)
public bool HealAllButtonAction(Action<HealRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
@@ -171,33 +185,39 @@ namespace Barotrauma
OnUpdate?.Invoke(); OnUpdate?.Invoke();
} }
return; return true;
} }
if (campaign?.CampaignUI?.MedicalClinic is { } ui) if (campaign?.CampaignUI?.MedicalClinic is { } openedUi)
{ {
ui.ClosePopup(); openedUi.ClosePopup();
} }
healAllRequests.Add(new RequestAction<HealRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable); {
healAllRequests.Add(new RequestAction<HealRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable);
});
} }
public void ClearAllButtonAction(Action<CallbackOnlyRequest> onReceived) public bool ClearAllButtonAction(Action<CallbackOnlyRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
ClearPendingHeals(); ClearPendingHeals();
onReceived(new CallbackOnlyRequest(RequestResult.Success)); onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke(); OnUpdate?.Invoke();
return; return true;
} }
clearAllRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable); {
clearAllRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable);
});
} }
private void ClearRequstReceived() private void ClearRequestReceived()
{ {
ClearPendingHeals(); ClearPendingHeals();
if (TryDequeue(clearAllRequests, out var callback)) if (TryDequeue(clearAllRequests, out var callback))
@@ -224,28 +244,31 @@ namespace Barotrauma
OnUpdate?.Invoke(); OnUpdate?.Invoke();
} }
public void AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived) public bool AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
InsertPendingCrewMember(crewMember); InsertPendingCrewMember(crewMember);
onReceived(new CallbackOnlyRequest(RequestResult.Success)); onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke(); OnUpdate?.Invoke();
return; return true;
} }
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable); {
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable);
});
} }
public void RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action<CallbackOnlyRequest> onReceived) public bool RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action<CallbackOnlyRequest> onReceived)
{ {
if (GameMain.IsSingleplayer) if (GameMain.IsSingleplayer)
{ {
RemovePendingAffliction(crewMember, affliction); RemovePendingAffliction(crewMember, affliction);
onReceived(new CallbackOnlyRequest(RequestResult.Success)); onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke(); OnUpdate?.Invoke();
return; return true;
} }
INetSerializableStruct removedAffliction = new NetRemovedAffliction INetSerializableStruct removedAffliction = new NetRemovedAffliction
@@ -254,11 +277,14 @@ namespace Barotrauma
Affliction = affliction Affliction = affliction
}; };
removeRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout())); return requestBucket.TryEnqueue(() =>
ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable); {
removeRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable);
});
} }
private void NewAdditonReceived(IReadMessage inc, MessageFlag flag) private void NewAdditionReceived(IReadMessage inc, MessageFlag flag)
{ {
var crewMembers = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc); var crewMembers = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
foreach (var crewMember in crewMembers) foreach (var crewMember in crewMembers)
@@ -300,7 +326,7 @@ namespace Barotrauma
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc); NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
if (TryDequeue(afflictionRequests, out var callback)) if (TryDequeue(afflictionRequests, out var callback))
{ {
RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.Error : RequestResult.Success; RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.CharacterNotFound : RequestResult.Success;
callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray())); callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray()));
} }
} }
@@ -336,7 +362,7 @@ namespace Barotrauma
IWriteMessage msg = StartSending(); IWriteMessage msg = StartSending();
msg.WriteByte((byte)header); msg.WriteByte((byte)header);
netStruct?.Write(msg); netStruct?.Write(msg);
GameMain.Client.ClientPeer?.Send(msg, deliveryMethod); GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod);
} }
public void ClientRead(IReadMessage inc) public void ClientRead(IReadMessage inc)
@@ -356,7 +382,7 @@ namespace Barotrauma
PendingRequestReceived(inc); PendingRequestReceived(inc);
break; break;
case NetworkHeader.ADD_PENDING: case NetworkHeader.ADD_PENDING:
NewAdditonReceived(inc, flag); NewAdditionReceived(inc, flag);
break; break;
case NetworkHeader.REMOVE_PENDING: case NetworkHeader.REMOVE_PENDING:
NewRemovalReceived(inc, flag); NewRemovalReceived(inc, flag);
@@ -365,7 +391,7 @@ namespace Barotrauma
HealRequestReceived(inc); HealRequestReceived(inc);
break; break;
case NetworkHeader.CLEAR_PENDING: case NetworkHeader.CLEAR_PENDING:
ClearRequstReceived(); ClearRequestReceived();
break; break;
} }
} }
@@ -310,9 +310,10 @@ namespace Barotrauma
}; };
} }
} }
var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform),
RichString.Rich(missionMessage), wrap: true); RichString.Rich(missionMessage), wrap: true);
if (selectedMissions.Contains(displayedMission) && displayedMission.Completed) if (selectedMissions.Contains(displayedMission))
{ {
RichString reputationText = displayedMission.GetReputationRewardText(); RichString reputationText = displayedMission.GetReputationRewardText();
if (!reputationText.IsNullOrEmpty()) if (!reputationText.IsNullOrEmpty())
@@ -324,7 +325,7 @@ namespace Barotrauma
if (totalReward > 0) if (totalReward > 0)
{ {
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub))); 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)); var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option<int>.Some(totalReward));
if (share > 0) if (share > 0)
@@ -575,7 +575,7 @@ namespace Barotrauma
//cancel dragging if too far away from the container of the dragged item //cancel dragging if too far away from the container of the dragged item
if (DraggingItems.Any()) if (DraggingItems.Any())
{ {
var rootContainer = DraggingItems.First().GetRootContainer(); var rootContainer = DraggingItems.First().RootContainer;
var rootInventory = DraggingItems.First().ParentInventory; var rootInventory = DraggingItems.First().ParentInventory;
if (rootContainer != null) if (rootContainer != null)
@@ -155,8 +155,6 @@ namespace Barotrauma.Items.Components
convexHull.Enabled = true; convexHull.Enabled = true;
SetVertices(convexHull, rect); SetVertices(convexHull, rect);
} }
convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom;
if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; }
} }
@@ -169,12 +167,11 @@ namespace Barotrauma.Items.Components
IsHorizontal ? IsHorizontal ?
new Vector2[] { new Vector2(verts[0].X, center.Y), new Vector2(verts[2].X, center.Y) } : 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) }); 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) partial void UpdateProjSpecific(float deltaTime)
{ {
convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom;
if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; }
if (shakeTimer > 0.0f) if (shakeTimer > 0.0f)
{ {
shakeTimer -= deltaTime; shakeTimer -= deltaTime;
@@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components
public override void DrawHUD(SpriteBatch spriteBatch, Character character) 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 //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; } 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 //whole text can fit in the textblock, no need to scroll
needsScrolling = false; needsScrolling = false;
scrollingText = DisplayText.Value; TextBlock.Text = scrollingText = DisplayText.Value;
scrollPadding = 0; scrollPadding = 0;
scrollAmount = 0.0f; scrollAmount = 0.0f;
scrollIndex = 0; scrollIndex = 0;
@@ -73,6 +73,7 @@ namespace Barotrauma.Items.Components
Step = 0.05f, Step = 0.05f,
OnMoved = (GUIScrollBar scrollBar, float barScroll) => OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
{ {
lastReceivedTargetForce = null;
float newTargetForce = barScroll * 200.0f - 100.0f; float newTargetForce = barScroll * 200.0f - 100.0f;
if (Math.Abs(newTargetForce - targetForce) < 0.01) { return false; } if (Math.Abs(newTargetForce - targetForce) < 0.01) { return false; }
@@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components
private GUIFrame selectedItemFrame; private GUIFrame selectedItemFrame;
private GUIFrame selectedItemReqsFrame; private GUIFrame selectedItemReqsFrame;
private GUITextBlock amountTextMin, amountTextMax; private GUITextBlock amountTextMax;
private GUIScrollBar amountInput; private GUIScrollBar amountInput;
public GUIButton ActivateButton public GUIButton ActivateButton
@@ -29,6 +29,9 @@ namespace Barotrauma.Items.Components
private GUIComponent outputSlot; private GUIComponent outputSlot;
private GUIComponent inputInventoryHolder, outputInventoryHolder; private GUIComponent inputInventoryHolder, outputInventoryHolder;
private readonly List<GUIButton> itemCategoryButtons = new List<GUIButton>();
private MapEntityCategory? selectedItemCategory;
public FabricationRecipe SelectedItem public FabricationRecipe SelectedItem
{ {
get { return selectedItem; } get { return selectedItem; }
@@ -77,7 +80,67 @@ namespace Barotrauma.Items.Components
AutoScaleVertical = true 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, RelativeSpacing = 0.02f,
Stretch = true, Stretch = true,
@@ -105,10 +168,13 @@ namespace Barotrauma.Items.Components
Padding = Vector4.Zero, Padding = Vector4.Zero,
AutoScaleVertical = true 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) => itemFilterBox.OnTextChanged += (textBox, text) =>
{ {
FilterEntities(text); FilterEntities(selectedItemCategory, text);
return true; return true;
}; };
filterArea.RectTransform.MaxSize = new Point(int.MaxValue, itemFilterBox.Rect.Height); filterArea.RectTransform.MaxSize = new Point(int.MaxValue, itemFilterBox.Rect.Height);
@@ -174,7 +240,7 @@ namespace Barotrauma.Items.Components
Stretch = true 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") 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); 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; Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect;
itemIcon.Draw(
spriteBatch, var requiredItemPrefab = requiredItem.FirstMatchingPrefab;
slotRect.Center.ToVector2(),
color: requiredItemPrefab.InventoryIconColor * 0.3f, float iconAlpha = 0.0f;
scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y)); 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) if (missingCount > 1)
{ {
Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom); Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom);
@@ -552,9 +640,13 @@ namespace Barotrauma.Items.Components
} }
toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖"; 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 }; 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); FabricationRecipe recipe = child.UserData as FabricationRecipe;
} if (recipe?.DisplayName == null) { continue; }
else child.Visible =
{ (string.IsNullOrWhiteSpace(filter) || recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) &&
foreach (GUIComponent child in itemList.Content.Children) (!category.HasValue || recipe.TargetItem.Category.HasFlag(category.Value));
{ }
FabricationRecipe recipe = child.UserData as FabricationRecipe;
if (recipe?.DisplayName == null) { continue; }
child.Visible = recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase);
}
}
foreach (GUIButton btn in itemCategoryButtons)
{
btn.Selected = (MapEntityCategory?)btn.UserData == selectedItemCategory;
}
HideEmptyItemListCategories(); HideEmptyItemListCategories();
return true; return true;
@@ -648,7 +739,7 @@ namespace Barotrauma.Items.Components
public bool ClearFilter() public bool ClearFilter()
{ {
FilterEntities(""); FilterEntities(selectedItemCategory, "");
itemList.UpdateScrollBarSize(); itemList.UpdateScrollBarSize();
itemList.BarScroll = 0.0f; itemList.BarScroll = 0.0f;
itemFilterBox.Text = ""; itemFilterBox.Text = "";
@@ -737,6 +828,7 @@ namespace Barotrauma.Items.Components
TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUIStyle.Red : GUIStyle.Green, font: GUIStyle.SubHeadingFont) TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUIStyle.Red : GUIStyle.Green, font: GUIStyle.SubHeadingFont)
{ {
AutoScaleHorizontal = true, AutoScaleHorizontal = true,
ToolTip = TextManager.Get("fabricatorrequiredskills.tooltip")
}; };
foreach (Skill skill in selectedItem.RequiredSkills) foreach (Skill skill in selectedItem.RequiredSkills)
{ {
@@ -125,18 +125,15 @@ namespace Barotrauma.Items.Components
{ {
public static MiniMapSettings Default = new MiniMapSettings public static MiniMapSettings Default = new MiniMapSettings
( (
ignoreOutposts: false,
createHullElements: true, createHullElements: true,
elementColor: MiniMap.MiniMapBaseColor elementColor: MiniMap.MiniMapBaseColor
); );
public readonly bool IgnoreOutposts;
public readonly bool CreateHullElements; public readonly bool CreateHullElements;
public readonly Color ElementColor; 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; CreateHullElements = createHullElements;
ElementColor = elementColor ?? MiniMap.MiniMapBaseColor; ElementColor = elementColor ?? MiniMap.MiniMapBaseColor;
} }
@@ -403,7 +400,8 @@ namespace Barotrauma.Items.Components
private bool VisibleOnItemFinder(Item it) private bool VisibleOnItemFinder(Item it)
{ {
if (!item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; } if (it?.Submarine == null) { return false; }
if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; }
if (it.NonInteractable || it.HiddenInGame) { return false; } if (it.NonInteractable || it.HiddenInGame) { return false; }
if (it.GetComponent<Pickable>() == null) { return false; } if (it.GetComponent<Pickable>() == null) { return false; }
@@ -436,7 +434,11 @@ namespace Barotrauma.Items.Components
prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
submarineContainer.ClearChildren(); 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)); 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 }; 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(); 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); 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); 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), electrialPointsOfInterest, out electricalMapComponents); electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electricalPointsOfInterest, out electricalMapComponents);
Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>(); Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>();
@@ -535,7 +537,7 @@ namespace Barotrauma.Items.Components
displayedSubs.Clear(); displayedSubs.Clear();
displayedSubs.Add(item.Submarine); 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(); 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 && item.Submarine is { } itemSub &&
( (
!displayedSubs.Contains(itemSub) || // current sub not displayed !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 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 prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed
@@ -730,7 +732,7 @@ namespace Barotrauma.Items.Components
if (sprite != null && ShowHullIntegrity) if (sprite != null && ShowHullIntegrity)
{ {
Vector2 spriteSize = sprite.size; Vector2 spriteSize = sprite.size;
Rectangle worldBorders = item.Submarine.GetDockedBorders(); Rectangle worldBorders = item.Submarine.GetDockedBorders(allowDifferentTeam: false);
worldBorders.Location += item.Submarine.WorldPosition.ToPoint(); worldBorders.Location += item.Submarine.WorldPosition.ToPoint();
foreach (Gap gap in Gap.GapList) 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; dockedBorders.Location += item.Submarine.WorldPosition;
RectangleF parentRect = miniMapFrame.Rect; RectangleF parentRect = miniMapFrame.Rect;
@@ -1063,7 +1065,9 @@ namespace Barotrauma.Items.Components
waterVolume += linkedHull.WaterVolume; waterVolume += linkedHull.WaterVolume;
totalVolume += linkedHull.Volume; totalVolume += linkedHull.Volume;
} }
hullData.HullWaterAmount = MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100); hullData.HullWaterAmount =
waterVolume > 1.0f ?
MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100) : 0.0f;
} }
else else
{ {
@@ -1302,7 +1306,7 @@ namespace Barotrauma.Items.Components
GameMain.Instance.GraphicsDevice.SetRenderTarget(rt); GameMain.Instance.GraphicsDevice.SetRenderTarget(rt);
GameMain.Instance.GraphicsDevice.Clear(Color.Transparent); GameMain.Instance.GraphicsDevice.Clear(Color.Transparent);
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); 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(); worldBorders.Location += sub.WorldPosition.ToPoint();
parentRect.Inflate(-inflate, -inflate); parentRect.Inflate(-inflate, -inflate);
@@ -1523,7 +1527,7 @@ namespace Barotrauma.Items.Components
Dictionary<MapEntity, MiniMapGUIComponent> pointsOfInterestCollection = new Dictionary<MapEntity, MiniMapGUIComponent>(); Dictionary<MapEntity, MiniMapGUIComponent> pointsOfInterestCollection = new Dictionary<MapEntity, MiniMapGUIComponent>();
RectangleF worldBorders = sub.GetDockedBorders(); RectangleF worldBorders = sub.GetDockedBorders(allowDifferentTeam: false);
worldBorders.Location += sub.WorldPosition; worldBorders.Location += sub.WorldPosition;
// create a container that has the same "aspect ratio" as the sub // 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); 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; ImmutableArray<Hull> hullList = ImmutableArray<Hull>.Empty;
ImmutableDictionary<Hull, ImmutableArray<Hull>> combinedHulls = ImmutableDictionary<Hull, 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) bool IsPartofSub(MapEntity entity)
{ {
if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.HiddenInGame) { return false; } 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) bool IsStandaloneHull(Hull hull)
@@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components
private const float NearbyObjectUpdateInterval = 1.0f; private const float NearbyObjectUpdateInterval = 1.0f;
float nearbyObjectUpdateTimer; float nearbyObjectUpdateTimer;
private List<Submarine> connectedSubs = new List<Submarine>(); private readonly List<Submarine> connectedSubs = new List<Submarine>();
private const float ConnectedSubUpdateInterval = 1.0f; private const float ConnectedSubUpdateInterval = 1.0f;
float connectedSubUpdateTimer; float connectedSubUpdateTimer;
@@ -335,9 +335,11 @@ namespace Barotrauma.Items.Components
// Setup layout for nav terminal // Setup layout for nav terminal
if (isConnectedToSteering || RightLayout) if (isConnectedToSteering || RightLayout)
{ {
controlContainer.RectTransform.AbsoluteOffset = Point.Zero;
controlContainer.RectTransform.RelativeOffset = controlBoxOffset; controlContainer.RectTransform.RelativeOffset = controlBoxOffset;
controlContainer.RectTransform.SetPosition(Anchor.TopRight); controlContainer.RectTransform.SetPosition(Anchor.TopRight);
sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest; sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest;
if (HasMineralScanner) { PreventMineralScannerOverlap(); }
sonarView.RectTransform.SetPosition(Anchor.CenterLeft); sonarView.RectTransform.SetPosition(Anchor.CenterLeft);
sonarView.RectTransform.Resize(GUISizeCalculation); sonarView.RectTransform.Resize(GUISizeCalculation);
GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize); 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); 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") mineralScannerSwitch = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), mineralScannerFrame.RectTransform, Anchor.CenterLeft), string.Empty, style: "SwitchHorizontal")
{ {
Selected = UseMineralScanner,
OnClicked = (button, data) => OnClicked = (button, data) =>
{ {
useMineralScanner = !useMineralScanner; UseMineralScanner = !UseMineralScanner;
button.Selected = useMineralScanner; button.Selected = UseMineralScanner;
if (GameMain.Client != null) if (GameMain.Client != null)
{ {
unsentChanges = true; unsentChanges = true;
@@ -496,12 +499,12 @@ namespace Barotrauma.Items.Components
{ {
if (transducer.Transducer.Item.Submarine == null) { continue; } if (transducer.Transducer.Item.Submarine == null) { continue; }
if (connectedSubs.Contains(transducer.Transducer.Item.Submarine)) { 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) else if (item.Submarine != null)
{ {
connectedSubs = item.Submarine?.GetConnectedSubs(); connectedSubs.AddRange(item.Submarine?.GetConnectedSubs());
} }
connectedSubUpdateTimer = ConnectedSubUpdateInterval; connectedSubUpdateTimer = ConnectedSubUpdateInterval;
} }
@@ -1032,7 +1035,7 @@ namespace Barotrauma.Items.Components
missionIndex++; missionIndex++;
} }
if (HasMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null && if (HasMineralScanner && UseMineralScanner && CurrentMode == Mode.Active && MineralClusters != null &&
(item.CurrentHull == null || !DetectSubmarineWalls)) (item.CurrentHull == null || !DetectSubmarineWalls))
{ {
foreach (var c in MineralClusters) foreach (var c in MineralClusters)
@@ -1311,7 +1314,6 @@ namespace Barotrauma.Items.Components
float worldPingRadiusSqr = worldPingRadius * worldPingRadius; float worldPingRadiusSqr = worldPingRadius * worldPingRadius;
disruptedDirections.Clear(); disruptedDirections.Clear();
if (Level.Loaded == null) { return; }
for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex) for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
{ {
@@ -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(); float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared();
if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr) if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr)
@@ -1923,7 +1926,7 @@ namespace Barotrauma.Items.Components
float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection)); float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8); 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; float zoomT = 1.0f;
bool directionalPing = useDirectionalPing; bool directionalPing = useDirectionalPing;
float directionT = 0.0f; float directionT = 0.0f;
bool mineralScanner = useMineralScanner; bool mineralScanner = UseMineralScanner;
if (isActive) if (isActive)
{ {
zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8); 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)); pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
} }
useDirectionalPing = directionalModeSwitch.Selected = directionalPing; useDirectionalPing = directionalModeSwitch.Selected = directionalPing;
useMineralScanner = mineralScanner; UseMineralScanner = mineralScanner;
if (mineralScannerSwitch != null) if (mineralScannerSwitch != null)
{ {
mineralScannerSwitch.Selected = mineralScanner; mineralScannerSwitch.Selected = mineralScanner;
@@ -1983,7 +1986,7 @@ namespace Barotrauma.Items.Components
directionalModeSwitch.Selected = useDirectionalPing; directionalModeSwitch.Selected = useDirectionalPing;
if (mineralScannerSwitch != null) if (mineralScannerSwitch != null)
{ {
mineralScannerSwitch.Selected = useMineralScanner; mineralScannerSwitch.Selected = UseMineralScanner;
} }
} }
} }
@@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components
User = Entity.FindEntityByID(userId) as Character; User = Entity.FindEntityByID(userId) as Character;
Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle()); Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle());
float rotation = msg.ReadSingle(); float rotation = msg.ReadSingle();
SpreadCounter = msg.ReadByte(); spreadIndex = msg.ReadByte();
if (User != null) if (User != null)
{ {
Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false); Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false);
@@ -21,7 +21,8 @@ namespace Barotrauma.Items.Components
public float FlashTimer { get; private set; } public float FlashTimer { get; private set; }
public static Wire DraggingConnected { 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) 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 //two passes: first the connector, then the wires to get the wires to render in front
for (int i = 0; i < 2; i++) 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 //outputs are drawn at the right side of the panel, inputs at the left
if (c.IsOutput) if (c.IsOutput)
{ {
@@ -127,7 +166,6 @@ namespace Barotrauma.Items.Components
} }
} }
if (DraggingConnected != null) if (DraggingConnected != null)
{ {
if (mouseInRect) if (mouseInRect)
@@ -225,7 +263,9 @@ namespace Barotrauma.Items.Components
GUI.DrawString(spriteBatch, labelPos, text, GUIStyle.TextColorBright, font: GUIStyle.SmallFont); GUI.DrawString(spriteBatch, labelPos, text, GUIStyle.TextColorBright, font: GUIStyle.SmallFont);
float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale; float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale;
connectionSprite.Draw(spriteBatch, position, scale: connectorSpriteScale); connectionSprite.Draw(spriteBatch, position, scale: connectorSpriteScale);
} }
private void DrawWires(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 wirePosition, bool mouseIn, Wire equippedWire, float wireInterval) 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; 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) private static void DrawWire(SpriteBatch spriteBatch, Wire wire, Vector2 end, Vector2 start, Wire equippedWire, ConnectionPanel panel, LocalizedString label)
{ {
int textX = (int)start.X; int textX = (int)start.X;
@@ -10,6 +10,10 @@ namespace Barotrauma.Items.Components
{ {
partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable 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 //how long the rewiring sound plays after doing changes to the wiring
const float RewireSoundDuration = 5.0f; const float RewireSoundDuration = 5.0f;
@@ -120,12 +124,15 @@ namespace Barotrauma.Items.Components
if (user != Character.Controlled || user == null) { return; } if (user != Character.Controlled || user == null) { return; }
HighlightedWire = null; 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]) foreach (UISprite sprite in GUIStyle.GetComponentStyle("ConnectionPanelFront").Sprites[GUIComponent.ComponentState.None])
{ {
sprite.Draw(spriteBatch, GuiFrame.Rect, Color.White, SpriteEffects.None); sprite.Draw(spriteBatch, GuiFrame.Rect, Color.White, SpriteEffects.None);
} }
if (!tooltip.text.IsNullOrEmpty())
{
GUIComponent.DrawToolTip(spriteBatch, tooltip.text, tooltip.tooltipPos);
}
} }
private void CheckForLabelOverlap() private void CheckForLabelOverlap()
@@ -250,7 +250,7 @@ namespace Barotrauma.Items.Components
int visibleElementCount = 0; int visibleElementCount = 0;
foreach (var uiElement in uiElements) foreach (var uiElement in uiElements)
{ {
if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; } if (uiElement.UserData is not CustomInterfaceElement element) { continue; }
bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0); bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0);
if (visible) { visibleElementCount++; } if (visible) { visibleElementCount++; }
if (uiElement.Visible != visible) if (uiElement.Visible != visible)
@@ -337,7 +337,9 @@ namespace Barotrauma.Items.Components
{ {
if (uiElements[i] is GUITextBox tb) if (uiElements[i] is GUITextBox tb)
{ {
tb.Text = customInterfaceElementList[i].Signal; tb.Text = Screen.Selected is { IsEditor: true } ?
customInterfaceElementList[i].Signal :
TextManager.Get(customInterfaceElementList[i].Signal).Value;
} }
else if (uiElements[i] is GUINumberInput ni) else if (uiElements[i] is GUINumberInput ni)
{ {
@@ -5,7 +5,6 @@ using Microsoft.Xna.Framework.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
{ {
@@ -119,6 +118,13 @@ namespace Barotrauma.Items.Components
get { return sectionExtents; } get { return sectionExtents; }
} }
public readonly record struct VisualSignal(
float TimeSent,
Color Color,
int Direction);
private VisualSignal lastReceivedSignal;
public static Wire DraggingWire public static Wire DraggingWire
{ {
get => draggingWire; get => draggingWire;
@@ -126,13 +132,11 @@ namespace Barotrauma.Items.Components
public static Sprite ExtractWireSprite(ContentXElement element) 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 Depth = 0.855f
}; };
}
Sprite overrideSprite = null; Sprite overrideSprite = null;
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
@@ -153,6 +157,35 @@ namespace Barotrauma.Items.Components
if (wireSprite != defaultWireSprite) { overrideSprite = wireSprite; } 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) public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1)
{ {
Draw(spriteBatch, editing, Vector2.Zero, itemDepth); Draw(spriteBatch, editing, Vector2.Zero, itemDepth);
@@ -166,20 +199,7 @@ namespace Barotrauma.Items.Components
return; return;
} }
Vector2 drawOffset = Vector2.Zero; Vector2 drawOffset = GetDrawOffset() + offset;
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;
float baseDepth = UseSpriteDepth ? item.SpriteDepth : wireSprite.Depth; 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); 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) 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) 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; } if (!editing || !GameMain.SubEditorScreen.WiringMode) { return; }
for (int i = 0; i < nodes.Count; i++) 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) private void DrawHangingWire(SpriteBatch spriteBatch, Vector2 start, float depth)
{ {
float angle = (float)Math.Sin(GameMain.GameScreen.GameTime * 2.0f + item.ID) * 0.2f; float angle = (float)Math.Sin(GameMain.GameScreen.GameTime * 2.0f + item.ID) * 0.2f;
@@ -46,6 +46,13 @@ namespace Barotrauma.Items.Components
private set; private set;
} }
[Serialize(false, IsPropertySaveable.No)]
public bool DebugWiring
{
get;
private set;
}
[Serialize(true, IsPropertySaveable.No)] [Serialize(true, IsPropertySaveable.No)]
public bool ShowDeadCharacters public bool ShowDeadCharacters
{ {
@@ -111,6 +118,11 @@ namespace Barotrauma.Items.Components
refEntity = item; refEntity = item;
} }
if (equipper != null && equipper == Character.Controlled && DebugWiring)
{
ConnectionPanel.DebugWiringEnabledUntil = Timing.TotalTimeUnpaused + 0.5;
}
thermalEffectState += deltaTime; thermalEffectState += deltaTime;
thermalEffectState %= 10000.0f; thermalEffectState %= 10000.0f;
@@ -153,6 +165,11 @@ namespace Barotrauma.Items.Components
IsActive = false; IsActive = false;
} }
public override void Drop(Character dropper, bool setTransform = true)
{
Unequip(dropper);
}
public override void DrawHUD(SpriteBatch spriteBatch, Character character) public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{ {
if (character == null) { return; } if (character == null) { return; }
@@ -1,4 +1,5 @@
using Barotrauma.Networking; using Barotrauma.Lights;
using Barotrauma.Networking;
using FarseerPhysics; using FarseerPhysics;
using FarseerPhysics.Collision; using FarseerPhysics.Collision;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
@@ -10,6 +11,8 @@ namespace Barotrauma.Items.Components
{ {
private GUIMessageBox autodockingVerification; private GUIMessageBox autodockingVerification;
private readonly ConvexHull[] convexHulls = new ConvexHull[2];
public Vector2 DrawSize public Vector2 DrawSize
{ {
//use the extents of the item as the draw size //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) public void ClientEventRead(IReadMessage msg, float sendingTime)
{ {
bool isDocked = msg.ReadBoolean(); bool isDocked = msg.ReadBoolean();
@@ -1398,7 +1398,7 @@ namespace Barotrauma
} }
else else
{ {
throw new Exception("Failed to read component state - " + components[componentIndex].GetType() + " is not IServerSerializable."); throw new Exception($"Failed to read component state - {components[componentIndex].GetType()} in item \"{Prefab.Identifier}\" is not IServerSerializable.");
} }
} }
break; break;
@@ -1411,7 +1411,7 @@ namespace Barotrauma
} }
else else
{ {
throw new Exception("Failed to read inventory state - " + components[containerIndex].GetType() + " is not an ItemContainer."); throw new Exception($"Failed to read inventory state - {components[containerIndex].GetType()} in item \"{Prefab.Identifier}\" is not an ItemContainer.");
} }
} }
break; break;
@@ -1460,9 +1460,9 @@ namespace Barotrauma
byte length = msg.ReadByte(); byte length = msg.ReadByte();
for (int i = 0; i < length; i++) for (int i = 0; i < length; i++)
{ {
var statIdentifier = INetSerializableStruct.Read<ItemStatManager.TalentStatIdentifier>(msg); var statIdentifier = INetSerializableStruct.Read<TalentStatIdentifier>(msg);
var statValue = msg.ReadSingle(); var statValue = msg.ReadSingle();
StatManager.ApplyStat(statIdentifier, statValue); StatManager.ApplyStatDirect(statIdentifier, statValue);
} }
break; break;
case EventType.Upgrade: case EventType.Upgrade:
@@ -9,15 +9,17 @@ namespace Barotrauma
{ {
private LightSource lightSource; private LightSource lightSource;
partial void UpdateProjSpecific(float growModifier) private float particleTimer;
partial void UpdateProjSpecific(float growModifier, float deltaTime)
{ {
if (this is DummyFireSource) if (this is DummyFireSource)
{ {
EmitParticles(size, WorldPosition, hull, growModifier, null); EmitParticles(size, WorldPosition, deltaTime, hull, growModifier, null);
} }
else 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); 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; } 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( Vector2 particlePos = new Vector2(
worldPosition.X + Rand.Range(0.0f, size.X), 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( Vector2 particleVel = new Vector2(
particlePos.X - (worldPosition.X + size.X / 2.0f), particlePos.X - (worldPosition.X + size.X / 2.0f),
Math.Max((float)Math.Sqrt(size.X) * Rand.Range(0.0f, 15.0f) * growModifier, 0.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); 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); particlePos, particleVel, 0.0f, hull);
if (particle == null) { continue; } if (particle == null) { continue; }
@@ -54,7 +62,7 @@ namespace Barotrauma
if (Rand.Int(5) == 1) if (Rand.Int(5) == 1)
{ {
var smokeParticle = GameMain.ParticleManager.CreateParticle("smoke", 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) if (smokeParticle != null)
{ {
@@ -27,7 +27,7 @@ namespace Barotrauma
{ {
foreach (var edge in cell.Edges) 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; intersectionFound = true;
particlePos = intersection; particlePos = intersection;
@@ -93,13 +93,16 @@ namespace Barotrauma.Lights
private readonly int thickness; private readonly int thickness;
public bool IsExteriorWall;
public VertexPositionColor[] ShadowVertices { get; private set; } public VertexPositionColor[] ShadowVertices { get; private set; }
public VertexPositionTexture[] PenumbraVertices { get; private set; } public VertexPositionTexture[] PenumbraVertices { get; private set; }
public int ShadowVertexCount { get; private set; } public int ShadowVertexCount { get; private set; }
public int PenumbraVertexCount { 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>(); private readonly HashSet<ConvexHull> overlappingHulls = new HashSet<ConvexHull>();
public MapEntity ParentEntity { get; private set; } public MapEntity ParentEntity { get; private set; }
@@ -130,7 +133,7 @@ namespace Barotrauma.Lights
public Rectangle BoundingBox { get; private set; } 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) shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice)
{ {
@@ -150,15 +153,15 @@ namespace Barotrauma.Lights
BoundingBox = rect; BoundingBox = rect;
this.isHorizontal = isHorizontal ?? BoundingBox.Width > BoundingBox.Height; this.isHorizontal = isHorizontal;
if (ParentEntity is Structure structure) if (ParentEntity is Structure structure)
{ {
System.Diagnostics.Debug.Assert(!structure.Removed); Debug.Assert(!structure.Removed);
isHorizontal = structure.IsHorizontal; isHorizontal = structure.IsHorizontal;
} }
else if (ParentEntity is Item item) else if (ParentEntity is Item item)
{ {
System.Diagnostics.Debug.Assert(!item.Removed); Debug.Assert(!item.Removed);
var door = item.GetComponent<Door>(); var door = item.GetComponent<Door>();
if (door != null) { isHorizontal = door.IsHorizontal; } if (door != null) { isHorizontal = door.IsHorizontal; }
} }
@@ -205,44 +208,97 @@ namespace Barotrauma.Lights
{ {
if (ch == this) { return; } 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) //merge dist in the direction parallel to the segment
float mergeDist = MathHelper.Clamp(ch.thickness * 0.55f, 16, 512); //(e.g. how far up/down we can stretch a vertical segment)
mergeDist = Math.Min(mergeDist, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) / 2); 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; bool changed = false;
for (int i = 0; i < losVertices.Length; i++) 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( Vector2 closest = MathUtils.GetClosestPointOnLineSegment(
ch.losVertices[0].Pos + ch.losOffsets[0], ch.losVertices[0].Pos + ch.losOffsets[0],
ch.losVertices[1].Pos + ch.losOffsets[1], ch.losVertices[1].Pos + ch.losOffsets[1],
losVertices[i].Pos); 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 //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 // 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) // the direction of the segment unchanged (i.e. vertical segment stays vertical)
if (MathUtils.GetLineIntersection( if (MathUtils.GetLineIntersection(
ch.losVertices[0].Pos + ch.losOffsets[0], ch.losVertices[0].Pos + ch.losOffsets[0], ch.losVertices[1].Pos + ch.losOffsets[1],
ch.losVertices[1].Pos + ch.losOffsets[1], losVertices[0].Pos, losVertices[1].Pos,
losVertices[0].Pos, areLinesInfinite: true, out Vector2 intersection) &&
losVertices[1].Pos, //the intersection needs to be outwards from the vertex we're checking
out Vector2 intersection)) 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 || closest = intersection;
Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f) }
{
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; losOffsets[i] = closest - losVertices[i].Pos;
overlappingHulls.Add(ch); overlappingHulls.Add(ch);
ch.overlappingHulls.Add(this); ch.overlappingHulls.Add(this);
changed = true; 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) 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) public void Rotate(Vector2 origin, float amount)
{ {
Matrix rotationMatrix = Matrix rotationMatrix =
@@ -347,6 +410,7 @@ namespace Barotrauma.Lights
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
{ {
losVertices[i] = new SegmentPoint(losPoints[i], this); losVertices[i] = new SegmentPoint(losPoints[i], this);
losOffsets[i] = Vector2.Zero;
} }
overlappingHulls.Clear(); overlappingHulls.Clear();
@@ -612,8 +676,11 @@ namespace Barotrauma.Lights
vertexPos0 += ParentEntity.Submarine.DrawPosition; vertexPos0 += ParentEntity.Submarine.DrawPosition;
vertexPos1 += ParentEntity.Submarine.DrawPosition; vertexPos1 += ParentEntity.Submarine.DrawPosition;
} }
Vector2 viewTargetPos = LightManager.ViewTarget.WorldPosition; float alpha = 1.0f;
float alpha = IsSegmentFacing(vertexPos0, vertexPos1, viewTargetPos) ? 1.0f : 0.5f; if (LightManager.ViewTarget != null)
{
alpha = IsSegmentFacing(vertexPos0, vertexPos1, LightManager.ViewTarget.WorldPosition) ? 1.0f : 0.5f;
}
vertexPos0.Y = -vertexPos0.Y; vertexPos0.Y = -vertexPos0.Y;
vertexPos1.Y = -vertexPos1.Y; vertexPos1.Y = -vertexPos1.Y;
GUI.DrawLine(spriteBatch, vertexPos0, vertexPos1, color * alpha, width: width); 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 LightSource LightSource;
public Vector2 DrawPos; public Vector2 DrawPos;
@@ -298,7 +298,8 @@ namespace Barotrauma.Lights
range * range *
((Character.Controlled?.Submarine != null && light.ParentSub == Character.Controlled?.Submarine) ? 2.0f : 1.0f) * ((Character.Controlled?.Submarine != null && light.ParentSub == Character.Controlled?.Submarine) ? 2.0f : 1.0f) *
(light.CastShadows ? 10.0f : 1.0f) * (light.CastShadows ? 10.0f : 1.0f) *
(light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)); (light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)) *
light.PriorityMultiplier;
} }
//find the lights with an active light volume //find the lights with an active light volume
@@ -477,6 +478,17 @@ namespace Barotrauma.Lights
light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); 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; lightEffect.World = transform;
GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.Additive); GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.Additive);
@@ -686,19 +698,42 @@ namespace Barotrauma.Lights
if (LosEnabled && LosMode != LosMode.None && ViewTarget != null) if (LosEnabled && LosMode != LosMode.None && ViewTarget != null)
{ {
Vector2 pos = ViewTarget.DrawPosition; 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 && character.AnimController?.GetLimb(LimbType.Head) is Limb head &&
!head.IsSevered && !head.Removed) !head.IsSevered && !head.Removed)
{ {
pos = head.body.DrawPosition; 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); Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height);
Matrix shadowTransform = cam.ShaderTransform Matrix shadowTransform = cam.ShaderTransform
* Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f; * Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f;
var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget.Submarine); 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) if (convexHulls != null)
{ {
List<VertexPositionColor> shadowVerts = new List<VertexPositionColor>(); List<VertexPositionColor> shadowVerts = new List<VertexPositionColor>();
@@ -706,7 +741,6 @@ namespace Barotrauma.Lights
foreach (ConvexHull convexHull in convexHulls) foreach (ConvexHull convexHull in convexHulls)
{ {
if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; }
if (LosMode == LosMode.BlockOutsideView && !convexHull.IsExteriorWall) { continue; };
Vector2 relativeLightPos = pos; Vector2 relativeLightPos = pos;
if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; } if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; }
@@ -744,13 +778,14 @@ namespace Barotrauma.Lights
public void DebugDrawLos(SpriteBatch spriteBatch, Camera cam) public void DebugDrawLos(SpriteBatch spriteBatch, Camera cam)
{ {
if (ViewTarget == null) { return; } Vector2 pos = ViewTarget?.Position ?? cam.Position;
spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform); 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); Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height);
foreach (ConvexHull convexHull in convexHulls) foreach (ConvexHull convexHull in convexHulls)
{ {
if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; } if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; }
if (convexHull.ParentEntity is Structure { CastShadow: false }) { continue; }
convexHull.DebugDraw(spriteBatch); convexHull.DebugDraw(spriteBatch);
} }
spriteBatch.End(); spriteBatch.End();
@@ -53,10 +53,6 @@ namespace Barotrauma.Lights
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)] [Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)]
public float Rotation { get; set; } 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))); public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation)));
private float flicker; private float flicker;
@@ -233,6 +229,7 @@ namespace Barotrauma.Lights
//do we need to recalculate the vertices of the light volume //do we need to recalculate the vertices of the light volume
private bool needsRecalculation; private bool needsRecalculation;
private bool needsRecalculationWhenUpToDate;
public bool NeedsRecalculation public bool NeedsRecalculation
{ {
get { return needsRecalculation; } get { return needsRecalculation; }
@@ -246,9 +243,15 @@ namespace Barotrauma.Lights
} }
} }
needsRecalculation = value; 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 //when were the vertices of the light volume last calculated
public float LastRecalculationTime { get; private set; } public float LastRecalculationTime { get; private set; }
@@ -309,8 +312,6 @@ namespace Barotrauma.Lights
if (Math.Abs(value - rotation) < 0.001f) { return; } if (Math.Abs(value - rotation) < 0.001f) { return; }
rotation = value; rotation = value;
dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation));
if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null)
{ {
return; return;
@@ -321,8 +322,6 @@ namespace Barotrauma.Lights
} }
} }
private Vector2 dir = Vector2.UnitX;
private Vector2 _spriteScale = Vector2.One; private Vector2 _spriteScale = Vector2.One;
public Vector2 SpriteScale public Vector2 SpriteScale
@@ -396,6 +395,8 @@ namespace Barotrauma.Lights
public float Priority; public float Priority;
public float PriorityMultiplier = 1.0f;
private Vector2 lightTextureTargetSize; private Vector2 lightTextureTargetSize;
public Vector2 LightTextureTargetSize public Vector2 LightTextureTargetSize
@@ -444,7 +445,7 @@ namespace Barotrauma.Lights
public bool Enabled = true; public bool Enabled = true;
private readonly ISerializableEntity conditionalTarget; private readonly ISerializableEntity conditionalTarget;
private readonly PropertyConditional.LogicalOperatorType logicalOperator; private readonly PropertyConditional.Comparison comparison;
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>(); private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null) public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null)
@@ -452,8 +453,11 @@ namespace Barotrauma.Lights
{ {
lightSourceParams = new LightSourceParams(element); lightSourceParams = new LightSourceParams(element);
CastShadows = element.GetAttributeBool("castshadows", true); CastShadows = element.GetAttributeBool("castshadows", true);
logicalOperator = element.GetAttributeEnum(nameof(logicalOperator), string comparison = element.GetAttributeString("comparison", null);
element.GetAttributeEnum("comparison", logicalOperator)); if (comparison != null)
{
Enum.TryParse(comparison, ignoreCase: true, out this.comparison);
}
if (lightSourceParams.DeformableLightSpriteElement != null) if (lightSourceParams.DeformableLightSpriteElement != null)
{ {
@@ -466,7 +470,13 @@ namespace Barotrauma.Lights
switch (subElement.Name.ToString().ToLowerInvariant()) switch (subElement.Name.ToString().ToLowerInvariant())
{ {
case "conditional": case "conditional":
conditionals.AddRange(PropertyConditional.FromXElement(subElement)); foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
conditionals.Add(new PropertyConditional(attribute));
}
}
break; break;
} }
} }
@@ -529,32 +539,11 @@ namespace Barotrauma.Lights
var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub); var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub);
if (fullChList == null) { return; } 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(); chList.List.Clear();
foreach (var convexHull in fullChList.List) foreach (var convexHull in fullChList.List)
{ {
if (!convexHull.Enabled) { continue; } if (!convexHull.Enabled) { continue; }
if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { 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.List.Add(convexHull);
} }
chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch)); chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch));
@@ -704,7 +693,7 @@ namespace Barotrauma.Lights
lock (mutex) lock (mutex)
{ {
hull.RefreshWorldPositions(); hull.RefreshWorldPositions();
hull.GetVisibleSegments(drawPos, visibleSegments); hull.GetVisibleSegments(drawPos, visibleSegments);
foreach (var visibleSegment in visibleSegments) foreach (var visibleSegment in visibleSegments)
{ {
if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null) if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null)
@@ -713,7 +702,6 @@ namespace Barotrauma.Lights
} }
} }
} }
} }
} }
foreach (ConvexHull hull in chList.List) foreach (ConvexHull hull in chList.List)
@@ -812,7 +800,7 @@ namespace Barotrauma.Lights
} }
else else
{ {
intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection); intersects = MathUtils.GetLineSegmentIntersection(p1a, p1b, p2a, p2b, out intersection);
} }
if (intersects) if (intersects)
@@ -1006,7 +994,7 @@ namespace Barotrauma.Lights
} }
else 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) if (intersects)
@@ -1041,9 +1029,11 @@ namespace Barotrauma.Lights
Vector2 drawPos = calculatedDrawPos; Vector2 drawPos = calculatedDrawPos;
float cosAngle = (float)Math.Cos(Rotation);
float sinAngle = -(float)Math.Sin(Rotation);
Vector2 uvOffset = Vector2.Zero; Vector2 uvOffset = Vector2.Zero;
Vector2 overrideTextureDims = Vector2.One; Vector2 overrideTextureDims = Vector2.One;
Vector2 dir = this.dir;
if (OverrideLightTexture != null) if (OverrideLightTexture != null)
{ {
overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
@@ -1052,7 +1042,8 @@ namespace Barotrauma.Lights
if (LightSpriteEffect == SpriteEffects.FlipHorizontally) if (LightSpriteEffect == SpriteEffects.FlipHorizontally)
{ {
origin.X = OverrideLightTexture.SourceRect.Width - origin.X; origin.X = OverrideLightTexture.SourceRect.Width - origin.X;
dir = -dir; cosAngle = -cosAngle;
sinAngle = -sinAngle;
} }
if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; } if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; }
uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f); 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 //calculate texture coordinates based on the light's rotation
Vector2 originDiff = diff; Vector2 originDiff = diff;
diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y; diff.X = originDiff.X * cosAngle - originDiff.Y * sinAngle;
diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X; 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 *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y)));
diff += uvOffset; diff += uvOffset;
} }
@@ -1231,6 +1222,9 @@ namespace Barotrauma.Lights
} }
drawPos.Y = -drawPos.Y; drawPos.Y = -drawPos.Y;
float cosAngle = (float)Math.Cos(Rotation);
float sinAngle = -(float)Math.Sin(Rotation);
float bounds = TextureRange; float bounds = TextureRange;
if (OverrideLightTexture != null) if (OverrideLightTexture != null)
@@ -1242,8 +1236,8 @@ namespace Barotrauma.Lights
origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y); origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y);
origin *= TextureRange; origin *= TextureRange;
drawPos.X += origin.X * dir.Y + origin.Y * dir.X; drawPos.X += origin.X * sinAngle + origin.Y * cosAngle;
drawPos.Y += origin.X * dir.X + origin.Y * dir.Y; 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 //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 (conditionals.None()) { return; }
if (conditionalTarget == null) { return; } if (conditionalTarget == null) { return; }
if (logicalOperator == PropertyConditional.LogicalOperatorType.And) if (comparison == PropertyConditional.Comparison.And)
{ {
Enabled = conditionals.All(c => c.Matches(conditionalTarget)); Enabled = conditionals.All(c => c.Matches(conditionalTarget));
} }
@@ -1405,7 +1399,9 @@ namespace Barotrauma.Lights
CalculateLightVertices(verts); CalculateLightVertices(verts);
LastRecalculationTime = (float)Timing.TotalTime; LastRecalculationTime = (float)Timing.TotalTime;
NeedsRecalculation = false; NeedsRecalculation = needsRecalculationWhenUpToDate;
needsRecalculationWhenUpToDate = false;
state = LightVertexState.UpToDate; state = LightVertexState.UpToDate;
} }
} }
@@ -56,12 +56,12 @@ namespace Barotrauma
} }
private static readonly List<RoundSound> roundSounds = new List<RoundSound>(); 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) public static RoundSound? Load(ContentXElement element, bool stream = false)
{ {
if (GameMain.SoundManager?.Disabled ?? true) { return null; } if (GameMain.SoundManager?.Disabled ?? true) { return null; }
var filename = element.GetAttributeContentPath("file") ?? element.GetAttributeContentPath("sound"); var filename = element.GetAttributeContentPath("file") ?? element.GetAttributeContentPath("sound");
if (filename is null) if (filename is null)
{ {
string errorMsg = "Error when loading round sound (" + element + ") - file path not set"; string errorMsg = "Error when loading round sound (" + element + ") - file path not set";
@@ -70,7 +70,11 @@ namespace Barotrauma
return null; 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) if (existingSound is null)
{ {
@@ -99,7 +103,10 @@ namespace Barotrauma
} }
RoundSound newSound = new RoundSound(element, existingSound); RoundSound newSound = new RoundSound(element, existingSound);
if (filename is not null && !newSound.Stream)
{
roundSoundByPath.TryAdd(filename.FullPath, newSound);
}
roundSounds.Add(newSound); roundSounds.Add(newSound);
return newSound; return newSound;
} }
@@ -124,24 +131,14 @@ namespace Barotrauma
roundSound.Sound = existingSound; 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() 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>(); convexHulls ??= new List<ConvexHull>();
var h = new ConvexHull( var h = new ConvexHull(
new Rectangle((position - size / 2).ToPoint(), size.ToPoint()), new Rectangle((position - size / 2).ToPoint(), size.ToPoint()),
IsHorizontal, IsHorizontal,
this) this);
{
IsExteriorWall = IsExteriorWall
};
if (Math.Abs(rotation) > 0.001f) if (Math.Abs(rotation) > 0.001f)
{ {
h.Rotate(position, rotation); h.Rotate(position, rotation);
@@ -501,7 +498,7 @@ namespace Barotrauma
private bool ConditionalMatches(PropertyConditional conditional) private bool ConditionalMatches(PropertyConditional conditional)
{ {
if (!string.IsNullOrEmpty(conditional.TargetItemComponent)) if (!string.IsNullOrEmpty(conditional.TargetItemComponentName))
{ {
return false; return false;
} }
@@ -116,11 +116,11 @@ namespace Barotrauma
foreach (MapEntity e in entitiesToRender) foreach (MapEntity e in entitiesToRender)
{ {
if (!e.DrawOverWater) continue; if (!e.DrawOverWater) { continue; }
if (predicate != null) if (predicate != null)
{ {
if (!predicate(e)) continue; if (!predicate(e)) { continue; }
} }
e.Draw(spriteBatch, editing, false); e.Draw(spriteBatch, editing, false);
@@ -1,6 +1,7 @@
using FarseerPhysics; using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Barotrauma namespace Barotrauma
{ {
@@ -18,7 +19,7 @@ namespace Barotrauma
return; return;
} }
List<Submarine> subsToMove = submarine.GetConnectedSubs(); var subsToMove = submarine.GetConnectedSubs();
foreach (Submarine dockedSub in subsToMove) foreach (Submarine dockedSub in subsToMove)
{ {
if (dockedSub == submarine) { continue; } if (dockedSub == submarine) { continue; }
@@ -51,7 +52,6 @@ namespace Barotrauma
sub.PhysicsBody.SetTransformIgnoreContacts(sub.PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(moveAmount), 0.0f); sub.PhysicsBody.SetTransformIgnoreContacts(sub.PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(moveAmount), 0.0f);
} }
} }
if (closestSub != null && subsToMove.Contains(closestSub)) if (closestSub != null && subsToMove.Contains(closestSub))
{ {
GameMain.GameScreen.Cam.Position += moveAmount; GameMain.GameScreen.Cam.Position += moveAmount;
@@ -67,6 +67,10 @@ namespace Barotrauma
else if (ConnectedDoor != null) else if (ConnectedDoor != null)
{ {
sprite = iconSprites["Door"]; sprite = iconSprites["Door"];
if (ConnectedDoor.IsHorizontal && Ladders == null)
{
clr = Color.Yellow;
}
} }
else if (Ladders != null) else if (Ladders != null)
{ {
@@ -8,7 +8,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
@@ -16,6 +15,10 @@ namespace Barotrauma.Networking
{ {
sealed class GameClient : NetworkMember sealed class GameClient : NetworkMember
{ {
public static readonly TimeSpan CampaignSaveTransferTimeOut = new TimeSpan(0, 0, seconds: 100);
//this should be longer than CampaignSaveTransferTimeOut - we shouldn't give up starting the round if we're still waiting for the save file
public static readonly TimeSpan LevelTransitionTimeOut = new TimeSpan(0, 0, seconds: 150);
public override bool IsClient => true; public override bool IsClient => true;
public override bool IsServer => false; public override bool IsServer => false;
@@ -514,6 +517,7 @@ namespace Barotrauma.Networking
DisplayInLoadingScreens = true DisplayInLoadingScreens = true
}; };
Quit(); Quit();
GUI.DisableHUD = false;
GameMain.ServerListScreen.Select(); GameMain.ServerListScreen.Select();
return; return;
} }
@@ -931,7 +935,7 @@ namespace Barotrauma.Networking
", level value count: " + levelEqualityCheckValues.Count + ", level value count: " + levelEqualityCheckValues.Count +
", seed: " + Level.Loaded.Seed + ", seed: " + Level.Loaded.Seed +
", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" + ", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" +
", mirrored: " + Level.Loaded.Mirrored + "). 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); GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg); throw new Exception(errorMsg);
} }
@@ -1487,16 +1491,17 @@ namespace Barotrauma.Networking
NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID)) NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID))
{ {
campaign.PendingSaveID = campaignSaveID; campaign.PendingSaveID = campaignSaveID;
DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0, 0, 60); DateTime saveFileTimeOut = DateTime.Now + CampaignSaveTransferTimeOut;
while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID)) while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID))
{ {
if (DateTime.Now > saveFileTimeOut) if (DateTime.Now > saveFileTimeOut)
{ {
GameStarted = true; 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(); GameMain.NetLobbyScreen.Select();
roundInitStatus = RoundInitStatus.Interrupted; 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); yield return new WaitForSeconds(0.1f);
} }
@@ -1712,7 +1717,7 @@ namespace Barotrauma.Networking
yield return CoroutineStatus.Success; yield return CoroutineStatus.Success;
} }
if (GameMain.GameSession != null) { GameMain.GameSession.EndRound(endMessage, traitorResults, transitionType); } GameMain.GameSession?.EndRound(endMessage, traitorResults, transitionType);
ServerSettings.ServerDetailsChanged = true; ServerSettings.ServerDetailsChanged = true;
@@ -2872,12 +2877,14 @@ namespace Barotrauma.Networking
ClientPeer.Send(msg, DeliveryMethod.Reliable); ClientPeer.Send(msg, DeliveryMethod.Reliable);
} }
public bool SpectateClicked(GUIButton button, object userData) public bool SpectateClicked(GUIButton button, object _)
{ {
MultiPlayerCampaign campaign = MultiPlayerCampaign campaign =
GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ? GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ?
GameMain.GameSession?.GameMode as MultiPlayerCampaign : null; GameMain.GameSession?.GameMode as MultiPlayerCampaign : null;
if (campaign != null && campaign.LastSaveID < campaign.PendingSaveID)
if (FileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave) ||
(campaign != null && NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID)))
{ {
new GUIMessageBox("", TextManager.Get("campaignfiletransferinprogress")); new GUIMessageBox("", TextManager.Get("campaignfiletransferinprogress"));
return false; return false;
@@ -3063,8 +3070,12 @@ namespace Barotrauma.Networking
if (votingInterface != null) if (votingInterface != null)
{ {
votingInterface.Update(deltaTime); votingInterface.Update(deltaTime);
if (!votingInterface.VoteRunning) if (!votingInterface.VoteRunning || votingInterface.TimedOut)
{ {
if (votingInterface.TimedOut)
{
DebugConsole.AddWarning($"Voting interface timed out.");
}
votingInterface.Remove(); votingInterface.Remove();
votingInterface = null; votingInterface = null;
} }
@@ -111,8 +111,25 @@ namespace Barotrauma.Networking
? NetworkConnection.TimeoutThresholdInGame ? NetworkConnection.TimeoutThresholdInGame
: NetworkConnection.TimeoutThreshold; : NetworkConnection.TimeoutThreshold;
IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection); try
{
IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection);
ProcessP2PData(inc);
}
catch (Exception e)
{
string errorMsg = $"Client failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}";
GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnP2PData:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#else
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
}
}
private void ProcessP2PData(IReadMessage inc)
{
var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc); var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (!packetHeader.IsServerMessage()) { return; } if (!packetHeader.IsServerMessage()) { return; }
@@ -141,15 +141,31 @@ namespace Barotrauma.Networking
if (remotePeer.DisconnectTime != null) { return; } if (remotePeer.DisconnectTime != null) { return; }
var peerPacketHeaders = INetSerializableStruct.Read<PeerPacketHeaders>(inc); try
{
PacketHeader packetHeader = peerPacketHeaders.PacketHeader; 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; remotePeer.DisconnectTime = null;
ConnectionInitialization initialization = peerPacketHeaders.Initialization ?? throw new Exception("Initialization step missing"); ConnectionInitialization initialization = connectionInitialization ?? throw new Exception("Initialization step missing");
if (initialization == ConnectionInitialization.SteamTicketAndVersion) if (initialization == ConnectionInitialization.SteamTicketAndVersion)
{ {
remotePeer.Authenticating = true; remotePeer.Authenticating = true;
@@ -181,6 +197,7 @@ namespace Barotrauma.Networking
ForwardToServerProcess(outMsg); ForwardToServerProcess(outMsg);
} }
} }
public override void Update(float deltaTime) public override void Update(float deltaTime)
@@ -53,7 +53,7 @@ namespace Barotrauma.Networking
} }
} }
private readonly ref struct LobbyDataChangedEventHandler private readonly struct LobbyDataChangedEventHandler : IDisposable
{ {
private readonly Action<Lobby> action; private readonly Action<Lobby> action;
@@ -187,7 +187,7 @@ namespace Barotrauma.Networking
int botSpawnMode = 0, int botSpawnMode = 0,
bool? useRespawnShuttle = null) bool? useRespawnShuttle = null)
{ {
if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return; if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return; }
IWriteMessage outMsg = new WriteOnlyMessage(); IWriteMessage outMsg = new WriteOnlyMessage();
@@ -253,12 +253,9 @@ namespace Barotrauma.Networking
//in push-to-talk mode, InputType.Voice uses the active chat mode //in push-to-talk mode, InputType.Voice uses the active chat mode
bool usingActiveMode = PlayerInput.KeyDown(InputType.Voice); bool usingActiveMode = PlayerInput.KeyDown(InputType.Voice);
bool pttDown = (usingActiveMode || usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null; bool pttDown = (usingActiveMode || usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null;
if (pttDown || captureTimer <= 0)
{
ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode;
}
if (pttDown) if (pttDown)
{ {
ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode;
allowEnqueue = true; allowEnqueue = true;
} }
} }
@@ -115,17 +115,20 @@ namespace Barotrauma.Networking
float speechImpedimentMultiplier = 1.0f - client.Character.SpeechImpediment / 100.0f; float speechImpedimentMultiplier = 1.0f - client.Character.SpeechImpediment / 100.0f;
bool spectating = Character.Controlled == null; bool spectating = Character.Controlled == null;
float rangeMultiplier = spectating ? 2.0f : 1.0f; float rangeMultiplier = spectating ? 2.0f : 1.0f;
WifiComponent radio = null; WifiComponent senderRadio = null;
var messageType = var messageType =
!client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) && ChatMessage.CanUseRadio(Character.Controlled) ? !client.VoipQueue.ForceLocal &&
ChatMessageType.Radio : ChatMessageType.Default; 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.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
client.RadioNoise = 0.0f; client.RadioNoise = 0.0f;
if (messageType == ChatMessageType.Radio) 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) if (distanceFactor > RangeNear && !spectating)
{ {
//noise starts increasing exponentially after 40% range //noise starts increasing exponentially after 40% range
@@ -348,11 +348,13 @@ namespace Barotrauma
switch (voteType) switch (voteType)
{ {
case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseAndSwitchSub:
GameMain.GameSession.PurchaseSubmarine(subInfo); if (GameMain.GameSession.TryPurchaseSubmarine(subInfo))
GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); {
GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems);
}
break; break;
case VoteType.PurchaseSub: case VoteType.PurchaseSub:
GameMain.GameSession.PurchaseSubmarine(subInfo); GameMain.GameSession.TryPurchaseSubmarine(subInfo);
break; break;
case VoteType.SwitchSub: case VoteType.SwitchSub:
GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems); 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;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
@@ -48,19 +49,23 @@ namespace Barotrauma.Particles
private Vector2 drawPosition; private Vector2 drawPosition;
private float drawRotation; private float drawRotation;
private Vector2 colliderRadius;
private Hull currentHull; private Hull currentHull;
private List<Gap> hullGaps; private List<Gap> hullGaps;
private bool hasSubEmitters; private bool hasSubEmitters;
private List<ParticleEmitter> subEmitters = new List<ParticleEmitter>(); private readonly List<ParticleEmitter> subEmitters = new List<ParticleEmitter>();
private float animState; private float animState;
private int animFrame; private int animFrame;
private float collisionUpdateTimer; private float collisionUpdateTimer;
private bool changesSize;
public bool HighQualityCollisionDetection; public bool HighQualityCollisionDetection;
public Vector4 ColorMultiplier; public Vector4 ColorMultiplier;
@@ -127,7 +132,10 @@ namespace Barotrauma.Particles
position = (tracerPoints.Item1 + tracerPoints.Item2) / 2; position = (tracerPoints.Item1 + tracerPoints.Item2) / 2;
} }
RefreshColliderSize();
sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f); sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f);
changesSize = !sizeChange.NearlyEquals(Vector2.Zero);
this.position = position; this.position = position;
prevPosition = position; prevPosition = position;
@@ -256,8 +264,12 @@ namespace Barotrauma.Particles
} }
} }
size.X += sizeChange.X * deltaTime; if (changesSize)
size.Y += sizeChange.Y * deltaTime; {
size.X += sizeChange.X * deltaTime;
size.Y += sizeChange.Y * deltaTime;
RefreshColliderSize();
}
if (UseMiddleColor) if (UseMiddleColor)
{ {
@@ -344,11 +356,11 @@ namespace Barotrauma.Particles
{ {
Rectangle hullRect = currentHull.WorldRect; Rectangle hullRect = currentHull.WorldRect;
Vector2 collisionNormal = Vector2.Zero; 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); 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); collisionNormal = new Vector2(0.0f, -1.0f);
} }
@@ -378,11 +390,11 @@ namespace Barotrauma.Particles
} }
collisionNormal = Vector2.Zero; 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); 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); collisionNormal = new Vector2(-1.0f, 0.0f);
} }
@@ -431,6 +443,13 @@ namespace Barotrauma.Particles
return UpdateResult.Normal; 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) private void ApplyDrag(float dragCoefficient, float deltaTime)
{ {
Vector2 relativeVel = velocity; Vector2 relativeVel = velocity;
@@ -475,11 +494,11 @@ namespace Barotrauma.Particles
{ {
if (collisionNormal.X > 0.0f) 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 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.X = Math.Sign(collisionNormal.X) * Math.Abs(velocity.X) * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction); velocity.Y *= (1.0f - prefab.Friction);
@@ -488,11 +507,11 @@ namespace Barotrauma.Particles
{ {
if (collisionNormal.Y > 0.0f) 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 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); velocity.X *= (1.0f - prefab.Friction);
@@ -513,26 +532,26 @@ namespace Barotrauma.Particles
if (position.Y < center.Y) 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.X *= (1.0f - prefab.Friction);
velocity.Y = -velocity.Y * prefab.Restitution; velocity.Y = -velocity.Y * prefab.Restitution;
} }
else if (position.Y > center.Y) 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.X *= (1.0f - prefab.Friction);
velocity.Y = -velocity.Y * prefab.Restitution; velocity.Y = -velocity.Y * prefab.Restitution;
} }
if (position.X < center.X) if (position.X < center.X)
{ {
position.X = hullRect.X - prefab.CollisionRadius; position.X = hullRect.X - colliderRadius.X;
velocity.X = -velocity.X * prefab.Restitution; velocity.X = -velocity.X * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction); velocity.Y *= (1.0f - prefab.Friction);
} }
else if (position.X > center.X) 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.X = -velocity.X * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction); velocity.Y *= (1.0f - prefab.Friction);
} }
@@ -559,11 +578,12 @@ namespace Barotrauma.Particles
Color currColor = new Color(color.ToVector4() * ColorMultiplier); 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, spriteBatch, animFrame,
new Vector2(drawPosition.X, -drawPosition.Y), drawPos,
currColor * (currColor.A / 255.0f), currColor * (currColor.A / 255.0f),
prefab.Sprites[spriteIndex].Origin, drawRotation, prefab.Sprites[spriteIndex].Origin, drawRotation,
drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth); drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth);
@@ -571,11 +591,23 @@ namespace Barotrauma.Particles
else else
{ {
prefab.Sprites[spriteIndex].Draw(spriteBatch, prefab.Sprites[spriteIndex].Draw(spriteBatch,
new Vector2(drawPosition.X, -drawPosition.Y), drawPos,
currColor * (currColor.A / 255.0f), currColor * (currColor.A / 255.0f),
prefab.Sprites[spriteIndex].Origin, drawRotation, prefab.Sprites[spriteIndex].Origin, drawRotation,
drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth); 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.")] [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; } 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.")] [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; } public bool UseCollision { get; private set; }
@@ -18,7 +18,7 @@ namespace Barotrauma
public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; } public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; }
private GUIButton nextButton; private GUIButton nextButton;
private GUILayoutGroup characterInfoColumns; private GUIListBox characterInfoColumns;
public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable<SubmarineInfo> submarines, IEnumerable<CampaignMode.SaveInfo> saveFiles = null) public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable<SubmarineInfo> submarines, IEnumerable<CampaignMode.SaveInfo> saveFiles = null)
: base(newGameContainer, loadGameContainer) : base(newGameContainer, loadGameContainer)
@@ -249,11 +249,7 @@ namespace Barotrauma
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform), new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform),
TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft); TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft);
characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true) characterInfoColumns = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true);
{
Stretch = true,
RelativeSpacing = 0.01f
};
var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f), var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f),
secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true) secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true)
@@ -306,8 +302,8 @@ namespace Barotrauma
for (int i = 0; i < characterInfos.Count; i++) for (int i = 0; i < characterInfos.Count; i++)
{ {
var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f / characterInfos.Count, 1.0f), var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(Math.Max(1.0f / characterInfos.Count, 0.33f), 1.0f),
characterInfoColumns.RectTransform)); characterInfoColumns.Content.RectTransform));
var (characterInfo, job) = characterInfos[i]; var (characterInfo, job) = characterInfos[i];
@@ -202,18 +202,21 @@ namespace Barotrauma
case CampaignMode.InteractionType.PurchaseSub: case CampaignMode.InteractionType.PurchaseSub:
submarineSelection?.Update(); submarineSelection?.Update();
break; break;
case CampaignMode.InteractionType.Crew: case CampaignMode.InteractionType.Crew:
CrewManagement?.Update(); CrewManagement?.Update();
break; break;
case CampaignMode.InteractionType.Store: case CampaignMode.InteractionType.Store:
Store?.Update(deltaTime); Store?.Update(deltaTime);
break; break;
case CampaignMode.InteractionType.MedicalClinic: case CampaignMode.InteractionType.MedicalClinic:
MedicalClinic?.Update(deltaTime); MedicalClinic?.Update(deltaTime);
break; break;
case CampaignMode.InteractionType.Map:
if (StartButton != null)
{
StartButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap) && Character.Controlled is { IsIncapacitated: false };
}
break;
} }
} }
@@ -568,7 +571,6 @@ namespace Barotrauma
StartButton.Visible = false; StartButton.Visible = false;
missionList.Enabled = false; missionList.Enabled = false;
} }
//locationInfoPanel?.UpdateAuto(1.0f);
} }
public void SelectTab(CampaignMode.InteractionType tab, Character npc = null) public void SelectTab(CampaignMode.InteractionType tab, Character npc = null)
@@ -435,8 +435,11 @@ namespace Barotrauma
graphics.BlendState = BlendState.NonPremultiplied; graphics.BlendState = BlendState.NonPremultiplied;
graphics.SamplerStates[0] = SamplerState.PointClamp; graphics.SamplerStates[0] = SamplerState.PointClamp;
graphics.SamplerStates[1] = SamplerState.PointClamp;
GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply(); GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply();
Quad.Render(); Quad.Render();
graphics.SamplerStates[0] = SamplerState.LinearWrap;
graphics.SamplerStates[1] = SamplerState.LinearWrap;
} }
if (Character.Controlled is { } character) if (Character.Controlled is { } character)
@@ -59,7 +59,6 @@ namespace Barotrauma
private GUIImage playstyleBanner; private GUIImage playstyleBanner;
private GUITextBlock playstyleDescription; private GUITextBlock playstyleDescription;
private const string RemoteContentUrl = "http://www.barotraumagame.com/gamedata/";
private readonly GUIComponent remoteContentContainer; private readonly GUIComponent remoteContentContainer;
private XDocument remoteContentDoc; private XDocument remoteContentDoc;
@@ -1525,10 +1524,11 @@ namespace Barotrauma
private void FetchRemoteContent() private void FetchRemoteContent()
{ {
if (string.IsNullOrEmpty(RemoteContentUrl)) { return; } string remoteContentUrl = GameSettings.CurrentConfig.RemoteMainMenuContentUrl;
if (string.IsNullOrEmpty(remoteContentUrl)) { return; }
try try
{ {
var client = new RestClient(RemoteContentUrl); var client = new RestClient(remoteContentUrl);
var request = new RestRequest("MenuContent.xml", Method.GET); var request = new RestRequest("MenuContent.xml", Method.GET);
TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request), TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request),
RemoteContentReceived); RemoteContentReceived);
@@ -751,12 +751,20 @@ namespace Barotrauma
}; };
ServerMessage.OnTextChanged += (textBox, text) => 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); serverMessageHint.Visible = !textBox.Selected && !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text);
RefreshServerInfoSize();
return true; 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) => ServerMessage.OnEnterPressed += (textBox, text) =>
{ {
string str = textBox.Text; string str = textBox.Text;
@@ -812,7 +812,7 @@ namespace Barotrauma
private bool SortList(GUIButton button, object obj) 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); SortList(sortBy, toggle: true);
return true; return true;
} }
@@ -848,8 +848,7 @@ namespace Barotrauma
{ {
if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; } if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; }
if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; } if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; }
int comparison = sortedAscending ? 1 : -1; return CompareServer(sortBy, s1, s2, sortedAscending);
return CompareServer(sortBy, s1, s2) * comparison;
}); });
} }
@@ -857,22 +856,31 @@ namespace Barotrauma
{ {
var children = serverList.Content.RectTransform.Children.Reverse().ToList(); var children = serverList.Content.RectTransform.Children.Reverse().ToList();
int comparison = sortedAscending ? 1 : -1;
foreach (var child in children) foreach (var child in children)
{ {
if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; } 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); var index = serverList.Content.RectTransform.GetChildIndex(child);
component.RectTransform.RepositionChildInHierarchy(index + 1); component.RectTransform.RepositionChildInHierarchy(Math.Min(index + 1, serverList.Content.CountChildren - 1));
return; return;
} }
} }
component.RectTransform.SetAsFirstChild(); 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) switch (sortBy)
{ {
case ColumnLabel.ServerListCompatible: case ColumnLabel.ServerListCompatible:
@@ -880,18 +888,18 @@ namespace Barotrauma
bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion); bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion);
if (s1Compatible == s2Compatible) { return 0; } if (s1Compatible == s2Compatible) { return 0; }
return s1Compatible ? -1 : 1; return (s1Compatible ? -1 : 1) * comparison;
case ColumnLabel.ServerListHasPassword: case ColumnLabel.ServerListHasPassword:
if (s1.HasPassword == s2.HasPassword) { return 0; } if (s1.HasPassword == s2.HasPassword) { return 0; }
return s1.HasPassword ? 1 : -1; return (s1.HasPassword ? 1 : -1) * comparison;
case ColumnLabel.ServerListName: case ColumnLabel.ServerListName:
// I think we actually want culture-specific sorting here? // 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: case ColumnLabel.ServerListRoundStarted:
if (s1.GameStarted == s2.GameStarted) { return 0; } if (s1.GameStarted == s2.GameStarted) { return 0; }
return s1.GameStarted ? 1 : -1; return (s1.GameStarted ? 1 : -1) * comparison;
case ColumnLabel.ServerListPlayers: case ColumnLabel.ServerListPlayers:
return s2.PlayerCount.CompareTo(s1.PlayerCount); return s2.PlayerCount.CompareTo(s1.PlayerCount) * comparison;
case ColumnLabel.ServerListPing: case ColumnLabel.ServerListPing:
return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch 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), (true, true) => s2Ping.CompareTo(s1Ping),
(false, true) => 1, (false, true) => 1,
(true, false) => -1 (true, false) => -1
}; } * comparison;
default: default:
return 0; return 0;
} }
@@ -1504,9 +1512,41 @@ namespace Barotrauma
private void AddToServerList(ServerInfo serverInfo, bool skipPing = false) 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 > serverInfo.MaxPlayers) { return; }
if (serverInfo.PlayerCount < 0) { return; } if (serverInfo.PlayerCount < 0) { return; }
if (serverInfo.MaxPlayers <= 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.RefreshingServerList);
RemoveMsgFromServerList(MsgUserData.NoServers); RemoveMsgFromServerList(MsgUserData.NoServers);
@@ -1522,7 +1562,6 @@ namespace Barotrauma
UpdateServerInfoUI(serverInfo); UpdateServerInfoUI(serverInfo);
if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); } if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); }
InsertServer(serverInfo, serverFrame);
} }
private void UpdateServerInfoUI(ServerInfo serverInfo) private void UpdateServerInfoUI(ServerInfo serverInfo)
@@ -1736,7 +1775,7 @@ namespace Barotrauma
AddToFavoriteServers(serverInfo); AddToFavoriteServers(serverInfo);
} }
SortList(sortedBy, toggle: false); InsertServer(serverInfo, serverFrame);
FilterServers(); FilterServers();
} }
@@ -1,16 +1,16 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Steam;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
using Microsoft.Xna.Framework.Input;
using Barotrauma.IO;
using Barotrauma.Steam;
namespace Barotrauma namespace Barotrauma
{ {
@@ -525,6 +525,11 @@ namespace Barotrauma
GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green); GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green);
} }
WayPoint.ShowWayPoints = true; WayPoint.ShowWayPoints = true;
var matchingTickBox = showEntitiesTickBoxes?.Find(tb => tb.UserData as string == "waypoint");
if (matchingTickBox != null)
{
matchingTickBox.Selected = true;
}
generateWaypointsVerification.Close(); generateWaypointsVerification.Close();
return true; return true;
}; };
@@ -2847,7 +2852,7 @@ namespace Barotrauma
{ {
OnClicked = (button, o) => 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) .Where(cp => cp != null)
.Distinct().OfType<ContentPackage>().Select(p => p.Name).ToHashSet(); .Distinct().OfType<ContentPackage>().Select(p => p.Name).ToHashSet();
var tickboxes = requiredContentPackList.Content.Children.OfType<GUITickBox>().ToArray(); var tickboxes = requiredContentPackList.Content.Children.OfType<GUITickBox>().ToArray();
@@ -3546,10 +3551,46 @@ namespace Barotrauma
TextManager.Get("LoadingVanillaSubmarineHeader"), TextManager.Get("LoadingVanillaSubmarineHeader"),
TextManager.Get("LoadingVanillaSubmarineDesc")); TextManager.Get("LoadingVanillaSubmarineDesc"));
public void LoadSub(SubmarineInfo info) public void LoadSub(SubmarineInfo info, bool checkIdConflicts = true)
{ {
Submarine.Unload(); Submarine.Unload();
Submarine selectedSub = null; Submarine selectedSub = null;
if (checkIdConflicts)
{
Dictionary<int, Identifier> entities = new Dictionary<int, Identifier>();
foreach (var subElement in info.SubmarineElement.Elements())
{
int id = subElement.GetAttributeInt("ID", -1);
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 try
{ {
selectedSub = new Submarine(info); selectedSub = new Submarine(info);
@@ -5755,7 +5796,10 @@ namespace Barotrauma
{ {
item.SetTransform(dummyCharacter.SimPosition, 0.0f); item.SetTransform(dummyCharacter.SimPosition, 0.0f);
item.UpdateTransform(); 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 //wires need to be updated for the last node to follow the player during rewiring
Wire wire = item.GetComponent<Wire>(); Wire wire = item.GetComponent<Wire>();
@@ -5868,6 +5912,11 @@ namespace Barotrauma
spriteBatch.End(); spriteBatch.End();
} }
if (GameMain.LightManager.DebugLos)
{
GameMain.LightManager.DebugDrawLos(spriteBatch, cam);
}
//-------------------- HUD ----------------------------- //-------------------- HUD -----------------------------
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState);
@@ -343,12 +343,11 @@ namespace Barotrauma
if (property.PropertyType == typeof(string) && value == null) if (property.PropertyType == typeof(string) && value == null)
{ {
value = ""; value = "";
} }
Identifier propertyTag = $"{property.PropertyInfo.DeclaringType.Name}.{property.PropertyInfo.Name}".ToIdentifier(); Identifier propertyTag = $"{property.PropertyInfo.DeclaringType.Name}.{property.PropertyInfo.Name}".ToIdentifier();
Identifier fallbackTag = property.PropertyInfo.Name.ToIdentifier(); Identifier fallbackTag = property.PropertyInfo.Name.ToIdentifier();
LocalizedString displayName = LocalizedString displayName = TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier());
TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier());
if (displayName.IsNullOrEmpty()) if (displayName.IsNullOrEmpty())
{ {
Editable editable = property.GetAttribute<Editable>(); Editable editable = property.GetAttribute<Editable>();
@@ -380,10 +379,14 @@ namespace Barotrauma
} }
LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description"); 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(); 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()) if (toolTip.IsNullOrEmpty())
{ {
@@ -163,7 +163,7 @@ namespace Barotrauma.Steam
CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.LocalPackages.Refresh()); 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(); await Task.Yield();
@@ -184,11 +184,13 @@ namespace Barotrauma.Steam
throw new Exception("Staging copy could not be loaded", throw new Exception("Staging copy could not be loaded",
result.TryUnwrapFailure(out var exception) ? exception : null); 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) ModProject modProject = new ModProject(tempPkg)
{ {
ModVersion = modVersion ModVersion = modVersion,
Name = title,
ExpectedHash = tempPkg.CalculateHash(name: title, modVersion: modVersion)
}; };
modProject.Save(stagingFileListPath); modProject.Save(stagingFileListPath);
} }
@@ -9,6 +9,7 @@ using Barotrauma.Extensions;
using Barotrauma.IO; using Barotrauma.IO;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using Steamworks;
using Directory = Barotrauma.IO.Directory; using Directory = Barotrauma.IO.Directory;
using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>; using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>;
using Path = Barotrauma.IO.Path; using Path = Barotrauma.IO.Path;
@@ -157,7 +158,7 @@ namespace Barotrauma.Steam
} }
var selectedTitle = 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); font: GUIStyle.LargeFont);
if (workshopItem.Id != 0) if (workshopItem.Id != 0)
{ {
@@ -212,7 +213,7 @@ namespace Barotrauma.Steam
}; };
Label(rightTop, TextManager.Get("WorkshopItemTitle"), GUIStyle.SubHeadingFont); 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); Label(rightTop, TextManager.Get("WorkshopItemDescription"), GUIStyle.SubHeadingFont);
var descriptionTextBox var descriptionTextBox
@@ -320,7 +321,9 @@ namespace Barotrauma.Steam
workshopItem.Id == 0 workshopItem.Id == 0
? Steamworks.Ugc.Editor.NewCommunityFile ? Steamworks.Ugc.Editor.NewCommunityFile
: new Steamworks.Ugc.Editor(workshopItem.Id); : new Steamworks.Ugc.Editor(workshopItem.Id);
ugcEditor = ugcEditor.WithTitle(titleTextBox.Text) ugcEditor = ugcEditor
.InLanguage(SteamUtils.SteamUILanguage ?? string.Empty)
.WithTitle(titleTextBox.Text)
.WithDescription(descriptionTextBox.Text) .WithDescription(descriptionTextBox.Text)
.WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value)) .WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value))
.WithChangeLog(changeNoteTextBox.Text) .WithChangeLog(changeNoteTextBox.Text)
@@ -478,7 +481,7 @@ namespace Barotrauma.Steam
bool stagingReady = false; bool stagingReady = false;
Exception? stagingException = null; Exception? stagingException = null;
TaskPool.Add("CreatePublishStagingCopy", TaskPool.Add("CreatePublishStagingCopy",
SteamManager.Workshop.CreatePublishStagingCopy(modVersion, localPackage), SteamManager.Workshop.CreatePublishStagingCopy(editor.Title ?? localPackage.Name, modVersion, localPackage),
(t) => (t) =>
{ {
stagingReady = true; 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> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.1.4.0</Version> <Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
+2 -2
View File
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.1.4.0</Version> <Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
@@ -30,10 +30,16 @@ float xLosAlpha;
float4 xColor; float4 xColor;
float blurDistance;
float4 mainPS(VertexShaderOutput input) : COLOR0 float4 mainPS(VertexShaderOutput input) : COLOR0
{ {
float4 sampleColor = tex2D(TextureSampler, input.TexCoords); 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; float obscureAmount = 1.0f - losColor.r;
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.1.4.0</Version> <Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.1.4.0</Version> <Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
+2 -2
View File
@@ -6,8 +6,8 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.1.4.0</Version> <Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
@@ -60,6 +60,10 @@ namespace Barotrauma
{ {
distance = Math.Min(distance, Vector2.Distance(recipient.Character.ViewTarget.WorldPosition, WorldPosition)); 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( float priority = 1.0f - MathUtils.InverseLerp(
NetConfig.HighPrioCharacterPositionUpdateDistance, NetConfig.HighPrioCharacterPositionUpdateDistance,
@@ -155,8 +159,6 @@ namespace Barotrauma
memInput.RemoveAt(memInput.Count - 1); 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) 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) while (memInput.Count > 5 && memInput[memInput.Count - 1].states == dequeuedInput)
@@ -14,7 +14,7 @@ namespace Barotrauma
static partial class DebugConsole static partial class DebugConsole
{ {
private static readonly RateLimiter rateLimiter = new( private static readonly RateLimiter rateLimiter = new(
maxRequests: 10, maxRequests: 50,
expiryInSeconds: 5, expiryInSeconds: 5,
punishmentRules: new[] punishmentRules: new[]
{ {
@@ -2526,14 +2526,14 @@ namespace Barotrauma
GameMain.Server.CreateEntityEvent(wall); GameMain.Server.CreateEntityEvent(wall);
} }
})); }));
commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that stalls each file transfer packet by the specified duration.", (string[] args) => commands.Add(new Command("stallfiletransfers", "stallfiletransfers [seconds]: A debug command that makes all file transfers take at least the specified duration.", (string[] args) =>
{ {
float seconds = 0.0f; float seconds = 0.0f;
if (args.Length > 0) if (args.Length > 0)
{ {
float.TryParse(args[0], out seconds); float.TryParse(args[0], out seconds);
} }
GameMain.Server.FileSender.StallPacketsTime = seconds; GameMain.Server.FileSender.ForceMinimumFileTransferDuration = seconds;
NewMessage("Set file transfer stall time to " + seconds); NewMessage("Set file transfer stall time to " + seconds);
})); }));
#endif #endif
@@ -4,6 +4,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Text;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.Networking; using Barotrauma.Networking;
@@ -11,10 +12,10 @@ namespace Barotrauma
{ {
internal partial class MedicalClinic 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( private readonly RateLimiter rateLimiter = new(
maxRequests: 10, maxRequests: RateLimitMaxRequests,
expiryInSeconds: 5, expiryInSeconds: RateLimitExpiry,
punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce)); punishmentRules: (RateLimitAction.OnLimitReached, RateLimitPunishment.Announce));
private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry); private readonly record struct AfflictionSubscriber(Client Subscriber, CharacterInfo Target, DateTimeOffset Expiry);
@@ -126,6 +127,17 @@ namespace Barotrauma
ImmutableArray<NetAffliction> pendingAfflictions = ImmutableArray<NetAffliction>.Empty; ImmutableArray<NetAffliction> pendingAfflictions = ImmutableArray<NetAffliction>.Empty;
int infoId = 0; int infoId = 0;
if (foundInfo is null)
{
StringBuilder sb = new();
foreach (CharacterInfo character in GetCrewCharacters())
{
sb.AppendLine($" - {character.DisplayName} ({character.ID})");
}
DebugConsole.ThrowError($"Could not find the requested crew member with ID {crewMember.CharacterInfoID}.\n{sb}");
}
if (foundInfo is { Character.CharacterHealth: { } health }) if (foundInfo is { Character.CharacterHealth: { } health })
{ {
pendingAfflictions = GetAllAfflictions(health); pendingAfflictions = GetAllAfflictions(health);
@@ -18,11 +18,11 @@ namespace Barotrauma.Items.Components
if (item.CanClientAccess(c)) if (item.CanClientAccess(c))
{ {
lastReceivedTargetForce = null;
if (Math.Abs(newTargetForce - targetForce) > 0.01f) 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); GameServer.Log(GameServer.CharacterLogName(c.Character) + " set the force of " + item.Name + " to " + (int)(newTargetForce) + " %", ServerLog.MessageType.ItemInteraction);
} }
targetForce = newTargetForce; targetForce = newTargetForce;
User = c.Character; User = c.Character;
} }
@@ -184,6 +184,8 @@ namespace Barotrauma
int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand), int leftHandSlot = charInv.FindLimbSlot(InvSlotType.LeftHand),
rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand); rightHandSlot = charInv.FindLimbSlot(InvSlotType.RightHand);
if (IsSlotIndexOutOfBound(leftHandSlot) || IsSlotIndexOutOfBound(rightHandSlot)) { return; }
TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot); TryPutInOppositeHandSlot(rightHandSlot, leftHandSlot);
TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot); TryPutInOppositeHandSlot(leftHandSlot, rightHandSlot);
@@ -198,6 +200,8 @@ namespace Barotrauma
TryPutItem(it, otherHandSlot, true, true, character, false); TryPutItem(it, otherHandSlot, true, true, character, false);
} }
} }
bool IsSlotIndexOutOfBound(int index) => index < 0 || index >= slots.Length;
} }
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null) public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
@@ -108,9 +108,7 @@ namespace Barotrauma.Networking
const int MaxTransferCount = 16; const int MaxTransferCount = 16;
const int MaxTransferCountPerRecipient = 5; const int MaxTransferCountPerRecipient = 5;
public static TimeSpan MaxTransferDuration = new TimeSpan(0, 2, 0);
public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver); public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver);
public FileTransferDelegate OnStarted; public FileTransferDelegate OnStarted;
public FileTransferDelegate OnEnded; public FileTransferDelegate OnEnded;
@@ -121,8 +119,9 @@ namespace Barotrauma.Networking
private readonly ServerPeer peer; private readonly ServerPeer peer;
public static DateTime StartTime;
#if DEBUG #if DEBUG
public float StallPacketsTime { get; set; } public float ForceMinimumFileTransferDuration { get; set; }
#endif #endif
public IReadOnlyList<FileTransferOut> ActiveTransfers => activeTransfers; public IReadOnlyList<FileTransferOut> ActiveTransfers => activeTransfers;
@@ -172,6 +171,8 @@ namespace Barotrauma.Networking
return null; return null;
} }
StartTime = DateTime.Now;
OnStarted(transfer); OnStarted(transfer);
GameMain.Server.LastClientListUpdateID++; GameMain.Server.LastClientListUpdateID++;
@@ -259,7 +260,18 @@ namespace Barotrauma.Networking
for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++) for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++)
{ {
long remaining = transfer.Data.Length - transfer.SentOffset; long remaining = transfer.Data.Length - transfer.SentOffset;
int sendByteCount = (remaining > chunkLen ? chunkLen : (int)remaining); #if DEBUG
bool stalling = false;
float elapsedTime = (float)(DateTime.Now - StartTime).TotalSeconds;
if (elapsedTime < ForceMinimumFileTransferDuration)
{
int remainingChunks = (int)Math.Max(remaining / chunkLen, 1);
transfer.WaitTimer =
Math.Max(transfer.WaitTimer, (ForceMinimumFileTransferDuration - elapsedTime) / remainingChunks);
if (remainingChunks <= 1) { break; }
}
#endif
int sendByteCount = remaining > chunkLen ? chunkLen : (int)remaining;
message = new WriteOnlyMessage(); message = new WriteOnlyMessage();
message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER); message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER);
@@ -293,11 +305,10 @@ namespace Barotrauma.Networking
//this gets reset when packet loss or disorder sets in //this gets reset when packet loss or disorder sets in
transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate, transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate,
transfer.PacketsPerUpdate + 0.05f); transfer.PacketsPerUpdate + 0.05f);
}
#if DEBUG #if DEBUG
transfer.WaitTimer = Math.Max(transfer.WaitTimer, StallPacketsTime); if (stalling) { break; }
#endif #endif
}
} }
catch (Exception e) catch (Exception e)
@@ -330,7 +341,7 @@ namespace Barotrauma.Networking
{ {
byte transferId = inc.ReadByte(); byte transferId = inc.ReadByte();
var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId); var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId);
if (matchingTransfer != null) CancelTransfer(matchingTransfer); if (matchingTransfer != null) { CancelTransfer(matchingTransfer); }
return; return;
} }
else if (messageType == FileTransferMessageType.Data) else if (messageType == FileTransferMessageType.Data)
@@ -359,6 +370,7 @@ namespace Barotrauma.Networking
if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length) if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length)
{ {
matchingTransfer.Status = FileTransferStatus.Finished; matchingTransfer.Status = FileTransferStatus.Finished;
DebugConsole.Log($"Finished sending file \"{matchingTransfer.FilePath}\" to \"{client.Name}\". Took {DateTime.Now - StartTime}");
} }
} }
return; return;
@@ -21,15 +21,13 @@ namespace Barotrauma.Networking
public override Voting Voting { get; } public override Voting Voting { get; }
private string serverName;
public string ServerName public string ServerName
{ {
get { return serverName; } get { return ServerSettings.ServerName; }
set set
{ {
if (string.IsNullOrEmpty(value)) { return; } if (string.IsNullOrEmpty(value)) { return; }
ServerSettings.ServerName = value;
serverName = value;
} }
} }
@@ -133,8 +131,6 @@ namespace Barotrauma.Networking
name = name.Substring(0, NetConfig.ServerNameMaxLength); name = name.Substring(0, NetConfig.ServerNameMaxLength);
} }
this.serverName = name;
LastClientListUpdateID = 0; LastClientListUpdateID = 0;
ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP); 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); string localSavePath = SaveUtil.CreateSavePath(SaveUtil.SaveType.Multiplayer, saveName);
if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
{ {
ServerSettings.CampaignSettings = settings; using (dosProtection.Pause(connectedClient))
ServerSettings.SaveSettings(); {
MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings); ServerSettings.CampaignSettings = settings;
ServerSettings.SaveSettings();
MultiPlayerCampaign.StartNewCampaign(localSavePath, matchingSub.FilePath, seed, settings);
}
} }
} }
} }
@@ -790,8 +789,11 @@ namespace Barotrauma.Networking
break; break;
} }
if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound)) if (CampaignMode.AllowedToManageCampaign(connectedClient, ClientPermissions.ManageRound))
{ {
MultiPlayerCampaign.LoadCampaign(saveName); using (dosProtection.Pause(connectedClient))
{
MultiPlayerCampaign.LoadCampaign(saveName);
}
} }
} }
break; break;
@@ -1661,38 +1663,54 @@ namespace Barotrauma.Networking
//characters or items spawned mid-round don't necessarily exist at the client's end yet //characters or items spawned mid-round don't necessarily exist at the client's end yet
if (!c.NeedsMidRoundSync) 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) if (c.SpectatePos == null)
{ {
float distSqr = Vector2.DistanceSquared(character.WorldPosition, c.Character.WorldPosition); //not spectating ->
if (c.Character.ViewTarget != null) // 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)) //spectating ->
{ // check if the position the client is viewing
continue; // 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); static float GetShortestDistance(Vector2 viewPos, Character targetCharacter)
c.PositionUpdateLastSent.TryGetValue(character, out float lastSent); {
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) if (lastSent > NetTime.Now)
{ {
//sent in the future -> can't be right, remove //sent in the future -> can't be right, remove
c.PositionUpdateLastSent.Remove(character); c.PositionUpdateLastSent.Remove(otherCharacter);
} }
else else
{ {
if (lastSent > NetTime.Now - updateInterval) { continue; } 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) foreach (Submarine sub in Submarine.Loaded)
@@ -2594,16 +2612,20 @@ namespace Barotrauma.Networking
{ {
if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; } if (!CampaignMode.AllowedToManageCampaign(client, ClientPermissions.ManageRound)) { return false; }
const int MaxSaves = 255; using (dosProtection.Pause(client))
var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false);
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO);
msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves));
for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++)
{ {
msg.WriteNetSerializableStruct(saveInfos[i]); const int MaxSaves = 255;
var saveInfos = SaveUtil.GetSaveFiles(SaveUtil.SaveType.Multiplayer, includeInCompatible: false);
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.CAMPAIGN_SETUP_INFO);
msg.WriteByte((byte)Math.Min(saveInfos.Count, MaxSaves));
for (int i = 0; i < saveInfos.Count && i < MaxSaves; i++)
{
msg.WriteNetSerializableStruct(saveInfos[i]);
}
serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
} }
serverPeer.Send(msg, client.Connection, DeliveryMethod.Reliable);
return true; return true;
} }
@@ -3084,7 +3106,7 @@ namespace Barotrauma.Networking
default: default:
if (command != "") if (command != "")
{ {
if (command.ToLower() == serverName.ToLower()) if (command.ToLower() == ServerName.ToLower())
{ {
//a private message to the host //a private message to the host
if (OwnerConnection != null) if (OwnerConnection != null)
@@ -3139,7 +3161,7 @@ namespace Barotrauma.Networking
//msg sent by the server //msg sent by the server
if (senderCharacter == null) if (senderCharacter == null)
{ {
senderName = serverName; senderName = ServerName;
} }
else //msg sent by an AI character else //msg sent by an AI character
{ {
@@ -3173,7 +3195,7 @@ namespace Barotrauma.Networking
//msg sent by the server //msg sent by the server
if (senderCharacter == null) if (senderCharacter == null)
{ {
senderName = serverName; senderName = ServerName;
} }
else //sent by an AI character, not allowed when the game is not running 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; SubmarineInfo targetSubmarine = subVote.Sub;
VoteType voteType = Voting.ActiveVote.VoteType; VoteType voteType = Voting.ActiveVote.VoteType;
Client starter = Voting.ActiveVote.VoteStarter; Client starter = Voting.ActiveVote.VoteStarter;
bool purchaseFailed = false;
switch (voteType) switch (voteType)
{ {
case VoteType.PurchaseAndSwitchSub: case VoteType.PurchaseAndSwitchSub:
case VoteType.PurchaseSub: case VoteType.PurchaseSub:
// Pay for submarine // Pay for submarine
GameMain.GameSession.PurchaseSubmarine(targetSubmarine, starter); purchaseFailed = !GameMain.GameSession.TryPurchaseSubmarine(targetSubmarine, starter);
break; break;
case VoteType.SwitchSub: case VoteType.SwitchSub:
break; break;
default: default:
return; return false;
} }
if (voteType != VoteType.PurchaseSub) if (voteType != VoteType.PurchaseSub && !purchaseFailed)
{ {
GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter); GameMain.GameSession.SwitchSubmarine(targetSubmarine, subVote.TransferItems, starter);
} }
Voting.StopSubmarineVote(true); Voting.StopSubmarineVote(passed: !purchaseFailed);
return !purchaseFailed;
} }
public void UpdateClientPermissions(Client client) public void UpdateClientPermissions(Client client)
@@ -24,11 +24,28 @@ namespace Barotrauma.Networking
} }
protected readonly Callbacks callbacks; protected readonly Callbacks callbacks;
private readonly ImmutableArray<ContentPackage> contentPackages;
protected ServerPeer(Callbacks callbacks) protected ServerPeer(Callbacks callbacks)
{ {
this.callbacks = callbacks; this.callbacks = callbacks;
}
List<ContentPackage> contentPackageList = new List<ContentPackage>();
foreach (var cp in ContentPackageManager.EnabledPackages.All)
{
if (!cp.Files.Any()) { continue; }
if (!cp.HasMultiplayerSyncedContent && !cp.Files.All(f => f is SubmarineFile)) { continue; }
if (cp.UgcId.TryUnwrap(out var id1) &&
contentPackageList.FirstOrDefault(cp => cp.UgcId.TryUnwrap(out var id2) && id1.Equals(id2)) is ContentPackage existingPackage)
{
//there can be multiple enabled mods with the same UgcId if the player has e.g. created a local copy of a workshop mod
DebugConsole.AddWarning($"The content package \"{existingPackage.Name}\" ({existingPackage.Path}) has the same id as \"{cp.Name}\" ({cp.Path}). Ignoring the latter package.");
continue;
}
contentPackageList.Add(cp);
}
contentPackages = contentPackageList.ToImmutableArray();
}
public abstract void InitializeSteamServerCallbacks(); public abstract void InitializeSteamServerCallbacks();
@@ -250,9 +267,7 @@ namespace Barotrauma.Networking
structToSend = new ServerPeerContentPackageOrderPacket structToSend = new ServerPeerContentPackageOrderPacket
{ {
ServerName = GameMain.Server.ServerName, ServerName = GameMain.Server.ServerName,
ContentPackages = ContentPackageManager.EnabledPackages.All ContentPackages = contentPackages
.Where(cp => cp.Files.Any())
.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile))
.Select(contentPackage => new ServerContentPackage(contentPackage, timeNow)) .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow))
.ToImmutableArray() .ToImmutableArray()
}; };
@@ -370,6 +370,8 @@ namespace Barotrauma.Networking
"192-255", "192-255",
"384-591", "384-591",
"1024-1279", "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 "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) if (passed)
{ {
GameMain.Server?.SwitchSubmarine(); if (GameMain.Server != null && !GameMain.Server.TrySwitchSubmarine())
{
passed = false;
State = VoteState.Failed;
}
} }
else else
{ {
@@ -20,19 +20,6 @@ namespace Barotrauma
} }
public delegate void MessageSender(string message); public delegate void MessageSender(string message);
public void Greet(GameServer server, string codeWords, string codeResponse, MessageSender messageSender)
{
string greetingMessage = TextManager.FormatServerMessage(Mission.StartText,
("[codewords]", codeWords),
("[coderesponse]", codeResponse));
messageSender(greetingMessage);
Client traitorClient = server.ConnectedClients.Find(c => c.Character == Character);
Client ownerClient = server.ConnectedClients.Find(c => c.Connection == server.OwnerConnection);
if (traitorClient != ownerClient && ownerClient != null && ownerClient.Character == null)
{
GameMain.Server.SendTraitorMessage(ownerClient, CurrentObjective.StartMessageServerText.Value, Mission.Identifier, TraitorMessageType.ServerMessageBox);
}
}
public void SendChatMessage(string serverText, Identifier iconIdentifier) public void SendChatMessage(string serverText, Identifier iconIdentifier)
{ {
@@ -224,10 +224,6 @@ namespace Barotrauma
{ {
pendingMessages.Add(traitor, new List<string>()); pendingMessages.Add(traitor, new List<string>());
} }
foreach (var traitor in Traitors.Values)
{
traitor.Greet(server, CodeWords, CodeResponse, message => pendingMessages[traitor].Add(message));
}
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier))); pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessage(message, Identifier)));
pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier))); pendingMessages.ForEach(traitor => traitor.Value.ForEach(message => traitor.Key.SendChatMessageBox(message, Identifier)));
@@ -71,6 +71,15 @@ namespace Barotrauma
StrikesResetInterval = 60, StrikesResetInterval = 60,
StrikeThreshold = 6; 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> /// <summary>
/// Called when the server receives a packet to start logging how much time it takes to process. /// Called when the server receives a packet to start logging how much time it takes to process.
/// </summary> /// </summary>
@@ -122,11 +131,7 @@ namespace Barotrauma
private void StartFor(Client client) private void StartFor(Client client)
{ {
if (!clients.ContainsKey(client)) clients.TryAdd(client, new OffenseData());
{
clients.Add(client, new OffenseData());
}
clients[client].Stopwatch.Start(); clients[client].Stopwatch.Start();
} }
@@ -149,7 +154,7 @@ namespace Barotrauma
if (GameMain.Server?.ServerSettings is not { } settings) { return; } if (GameMain.Server?.ServerSettings is not { } settings) { return; }
// client is sending too many packets, kick them // 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")); AttemptKickClient(client, TextManager.Get("PacketLimitKicked"));
clients.Remove(client); clients.Remove(client);
@@ -216,7 +221,7 @@ namespace Barotrauma
{ {
if (GameMain.Server?.ServerSettings is { MaxPacketAmount: > ServerSettings.PacketLimitMin } settings) 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); 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> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>1.1.4.0</Version> <Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
<ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon> <ApplicationIcon>..\BarotraumaShared\Icon.ico</ApplicationIcon>
@@ -95,10 +95,7 @@ namespace Barotrauma
{ {
get get
{ {
if (visibleHulls == null) visibleHulls ??= Character.GetVisibleHulls();
{
visibleHulls = Character.GetVisibleHulls();
}
return visibleHulls; return visibleHulls;
} }
private set private set
@@ -107,12 +104,26 @@ namespace Barotrauma
} }
} }
public bool HasValidPath(bool requireNonDirty = false, bool requireUnfinished = true) => /// <summary>
steeringManager is IndoorsSteeringManager pathSteering && /// Is the current path valid, using the provided parameters.
pathSteering.CurrentPath != null && /// </summary>
(!requireUnfinished || !pathSteering.CurrentPath.Finished) && /// <param name="requireNonDirty"></param>
!pathSteering.CurrentPath.Unreachable && /// <param name="requireUnfinished"></param>
(!requireNonDirty || !pathSteering.IsPathDirty); /// <param name="nodePredicate"></param>
/// <returns>When <paramref name="nodePredicate"/> is defined, returns false if any of the nodes fails to match the predicate.</returns>
public bool HasValidPath(bool requireNonDirty = true, bool requireUnfinished = true, Func<WayPoint, bool> nodePredicate = null)
{
if (SteeringManager is not IndoorsSteeringManager pathSteering) { return false; }
if (pathSteering.CurrentPath == null) { return false; }
if (pathSteering.CurrentPath.Unreachable) { return false; }
if (requireUnfinished && pathSteering.CurrentPath.Finished) { return false; }
if (requireNonDirty && pathSteering.IsPathDirty) { return false; }
if (nodePredicate != null)
{
return pathSteering.CurrentPath.Nodes.All(n => nodePredicate(n));
}
return true;
}
public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null; public bool IsCurrentPathNullOrUnreachable => IsCurrentPathUnreachable || steeringManager is IndoorsSteeringManager pathSteering && pathSteering.CurrentPath == null;
public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable; public bool IsCurrentPathUnreachable => steeringManager is IndoorsSteeringManager pathSteering && !pathSteering.IsPathDirty && pathSteering.CurrentPath != null && pathSteering.CurrentPath.Unreachable;
@@ -251,7 +262,7 @@ namespace Barotrauma
} }
private readonly HashSet<Item> unequippedItems = new HashSet<Item>(); private readonly HashSet<Item> unequippedItems = new HashSet<Item>();
public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false) public bool TakeItem(Item item, CharacterInventory targetInventory, bool equip, bool wear = false, bool dropOtherIfCannotMove = true, bool allowSwapping = false, bool storeUnequipped = false, IEnumerable<Identifier> targetTags = null)
{ {
var pickable = item.GetComponent<Pickable>(); var pickable = item.GetComponent<Pickable>();
if (pickable == null) { return false; } if (pickable == null) { return false; }
@@ -265,23 +276,28 @@ namespace Barotrauma
} }
else else
{ {
var holdable = item.GetComponent<Holdable>(); // Not allowed to wear -> don't use the Wearable component even when it's found.
if (holdable != null) pickable = item.GetComponent<Holdable>();
{
pickable = holdable;
}
} }
if (item.ParentInventory is ItemInventory itemInventory) if (item.ParentInventory is ItemInventory itemInventory)
{ {
if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; } if (!itemInventory.Container.HasRequiredItems(Character, addMessage: false)) { return false; }
} }
if (equip) if (equip && pickable != null)
{ {
int targetSlot = -1; int targetSlot = -1;
//check if all the slots required by the item are free //check if all the slots required by the item are free
foreach (InvSlotType slots in pickable.AllowedSlots) foreach (InvSlotType slots in pickable.AllowedSlots)
{ {
if (slots.HasFlag(InvSlotType.Any)) { continue; } if (slots.HasFlag(InvSlotType.Any)) { continue; }
if (!wear)
{
if (slots != InvSlotType.RightHand && slots != InvSlotType.LeftHand && slots != (InvSlotType.RightHand | InvSlotType.LeftHand))
{
// Don't allow other than hand slots if not allowed to wear.
continue;
}
}
for (int i = 0; i < targetInventory.Capacity; i++) for (int i = 0; i < targetInventory.Capacity; i++)
{ {
if (targetInventory is CharacterInventory characterInventory) if (targetInventory is CharacterInventory characterInventory)
@@ -294,7 +310,7 @@ namespace Barotrauma
var otherItem = targetInventory.GetItemAt(i); var otherItem = targetInventory.GetItemAt(i);
if (otherItem == null) { continue; } if (otherItem == null) { continue; }
//try to move the existing item to LimbSlot.Any and continue if successful //try to move the existing item to LimbSlot.Any and continue if successful
if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.anySlot)) if (otherItem.AllowedSlots.Contains(InvSlotType.Any) && targetInventory.TryPutItem(otherItem, Character, CharacterInventory.AnySlot))
{ {
if (storeUnequipped && targetInventory.Owner == Character) if (storeUnequipped && targetInventory.Owner == Character)
{ {
@@ -304,6 +320,11 @@ namespace Barotrauma
} }
if (dropOtherIfCannotMove) if (dropOtherIfCannotMove)
{ {
if (otherItem.Prefab.Identifier == item.Prefab.Identifier || otherItem.HasIdentifierOrTags(targetTags))
{
// Shouldn't try dropping identical items, because that causes infinite looping when trying to get multiple items of the same type and if can't fit them all in the inventory.
return false;
}
//if everything else fails, simply drop the existing item //if everything else fails, simply drop the existing item
otherItem.Drop(Character); otherItem.Drop(Character);
} }
@@ -314,7 +335,7 @@ namespace Barotrauma
} }
else 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 (avoidDroppingInSea && !character.IsInFriendlySub)
{ {
// If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it. // If we are not inside a friendly sub (= same team), try to put the item in the inventory instead dropping it.
if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.anySlot)) if (character.Inventory.TryPutItem(containedItem, character, CharacterInventory.AnySlot))
{ {
if (unequipMax.HasValue && ++removed >= unequipMax) { return; } if (unequipMax.HasValue && ++removed >= unequipMax) { return; }
continue; continue;
@@ -401,14 +422,9 @@ namespace Barotrauma
var door = gap.ConnectedDoor; var door = gap.ConnectedDoor;
if (door != null) if (door != null)
{ {
if (!door.CanBeTraversed) if (!pathSteering.CanAccessDoor(door))
{ {
if (!door.HasAccess(Character)) continue;
{
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;
}
} }
} }
else else
@@ -449,9 +465,10 @@ namespace Barotrauma
Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition; Vector2 diff = EscapeTarget.WorldPosition - Character.WorldPosition;
float sqrDist = diff.LengthSquared(); float sqrDist = diff.LengthSquared();
bool isClose = sqrDist < MathUtils.Pow2(100); bool isClose = sqrDist < MathUtils.Pow2(100);
if (Character.CurrentHull == null || isClose && !isClosedDoor || pathSteering == null || IsCurrentPathNullOrUnreachable || IsCurrentPathFinished) if (Character.CurrentHull == null || (isClose && !isClosedDoor) || pathSteering == null || IsCurrentPathUnreachable || IsCurrentPathFinished)
{ {
// Very close to the target, outside, or at the end of the path -> try to steer through the gap // Very close to the target, outside, or at the end of the path -> try to steer through the gap
Character.ReleaseSecondaryItem();
SteeringManager.Reset(); SteeringManager.Reset();
pathSteering?.ResetPath(); pathSteering?.ResetPath();
Vector2 dir = Vector2.Normalize(diff); Vector2 dir = Vector2.Normalize(diff);
@@ -369,7 +369,13 @@ namespace Barotrauma
} }
else if (targetCharacter.AIController is EnemyAIController enemy) 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"; targetingTag = "husk";
} }
@@ -480,7 +486,7 @@ namespace Barotrauma
if (SelectedAiTarget?.Entity != null || EscapeTarget != null) if (SelectedAiTarget?.Entity != null || EscapeTarget != null)
{ {
Entity t = SelectedAiTarget?.Entity ?? EscapeTarget; Entity t = SelectedAiTarget?.Entity ?? EscapeTarget;
float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath(requireNonDirty: true) ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X; float referencePos = Vector2.DistanceSquared(Character.WorldPosition, t.WorldPosition) > 100 * 100 && HasValidPath() ? PathSteering.CurrentPath.CurrentNode.WorldPosition.X : t.WorldPosition.X;
Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left; Character.AnimController.TargetDir = Character.WorldPosition.X < referencePos ? Direction.Right : Direction.Left;
} }
else else
@@ -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. // 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 (Character.IsSameSpeciesOrGroup(c)) { return false; }
if (targetCharacter.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)) if (c.IsPlayer || Character.IsOnFriendlyTeam(c))
{ {
return a.Damage >= selectedTargetingParams.Threshold; return a.Damage >= selectedTargetingParams.Threshold;
@@ -894,7 +903,7 @@ namespace Barotrauma
_previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0))) _previousAttackLimb?.attack is Attack previousAttack && (previousAttack.AfterAttack != AIBehaviorAfterAttack.FallBack || previousAttack.CoolDownTimer <= 0)))
{ {
// Keep heading to the last known position of the target // Keep heading to the last known position of the target
var memory = GetTargetMemory(target, false); var memory = GetTargetMemory(target);
if (memory != null) if (memory != null)
{ {
var location = memory.Location; var location = memory.Location;
@@ -981,7 +990,7 @@ namespace Barotrauma
} }
else else
{ {
PathSteering.SetPath(path); PathSteering.SetPath(patrolTarget.SimPosition, path);
patrolTimerMargin = 0; patrolTimerMargin = 0;
newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f); newPatrolTargetTimer = newPatrolTargetIntervalMax * Rand.Range(0.5f, 1.5f);
searchingNewHull = false; searchingNewHull = false;
@@ -1088,13 +1097,13 @@ namespace Barotrauma
Character owner = GetOwner(item); Character owner = GetOwner(item);
if (owner != null) if (owner != null)
{ {
if (Character.IsFriendly(owner)) if (Character.IsFriendly(owner) || owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{ {
ResetAITarget(); ResetAITarget();
State = AIState.Idle; State = AIState.Idle;
return; return;
} }
else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI)) else
{ {
SelectedAiTarget = owner.AiTarget; 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; 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 // 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 && if (Math.Abs(limbDiff.X) < itemBodyExtent &&
Math.Abs(limbDiff.Y) < Character.AnimController.Collider.GetMaxExtent() + Character.AnimController.ColliderHeightFromFloor) 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 *= 0.9f;
item.body.LinearVelocity -= limbDiff * 0.25f; item.body.LinearVelocity -= velocity * 0.25f;
bool wasBroken = item.Condition <= 0.0f; 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); 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); 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); 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 //if the target is very close, the distance doesn't make much difference
// -> just ignore the distance and target whatever has the highest priority // -> just ignore the distance and target whatever has the highest priority
dist = Math.Max(dist, 100.0f); 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) if (Character.Submarine != null && !Character.Submarine.Info.IsRuin && Character.CurrentHull != null)
{ {
float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y; float diff = Math.Abs(toTarget.Y) - Character.CurrentHull.Size.Y;
@@ -3090,12 +3102,20 @@ namespace Barotrauma
if (aiTarget.Entity is Item i) if (aiTarget.Entity is Item i)
{ {
Character owner = GetOwner(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 == 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) if (targetCharacter != null)
@@ -3418,7 +3438,7 @@ namespace Barotrauma
return false; 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)) if (!targetMemories.TryGetValue(target, out AITargetMemory memory))
{ {
@@ -3428,9 +3448,8 @@ namespace Barotrauma
targetMemories.Add(target, memory); targetMemories.Add(target, memory);
} }
} }
if (addIfNotFound) if (keepAlive)
{ {
// Keep the memory alive.
memory.Priority = Math.Max(memory.Priority, minPriority); memory.Priority = Math.Max(memory.Priority, minPriority);
} }
return memory; return memory;
@@ -3446,7 +3465,7 @@ namespace Barotrauma
} }
else if (CanPerceive(_selectedAiTarget, checkVisibility: false)) else if (CanPerceive(_selectedAiTarget, checkVisibility: false))
{ {
var memory = GetTargetMemory(_selectedAiTarget, false); var memory = GetTargetMemory(_selectedAiTarget);
if (memory != null) if (memory != null)
{ {
memory.Location = _selectedAiTarget.WorldPosition; memory.Location = _selectedAiTarget.WorldPosition;
@@ -3643,7 +3662,11 @@ namespace Barotrauma
{ {
isStateChanged = true; isStateChanged = true;
SetStateResetTimer(); 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) if (target.IsHuman)
{ {
priority = GetTargetParams("human")?.Priority; priority = GetTargetParams("human")?.Priority;
@@ -3934,7 +3957,7 @@ namespace Barotrauma
{ {
SteerAwayFromTheEnemy(); SteerAwayFromTheEnemy();
} }
else if (canAttackDoors && HasValidPath(requireNonDirty: true, requireUnfinished: true)) else if (canAttackDoors && HasValidPath())
{ {
var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor; var door = PathSteering.CurrentPath.CurrentNode?.ConnectedDoor ?? PathSteering.CurrentPath.NextNode?.ConnectedDoor;
if (door != null && !door.CanBeTraversed && !door.HasAccess(Character)) if (door != null && !door.CanBeTraversed && !door.HasAccess(Character))
@@ -10,7 +10,7 @@ namespace Barotrauma
{ {
partial class HumanAIController : AIController partial class HumanAIController : AIController
{ {
public static bool debugai; public static bool DebugAI;
public static bool DisableCrewAI; public static bool DisableCrewAI;
private readonly AIObjectiveManager objectiveManager; private readonly AIObjectiveManager objectiveManager;
@@ -42,6 +42,8 @@ namespace Barotrauma
public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>(); public readonly HashSet<Hull> UnsafeHulls = new HashSet<Hull>();
public readonly List<Item> IgnoredItems = new List<Item>(); public readonly List<Item> IgnoredItems = new List<Item>();
private readonly HashSet<Hull> dirtyHullSafetyCalculations = new HashSet<Hull>();
private float respondToAttackTimer; private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f; private const float RespondToAttackInterval = 1.0f;
private bool wasConscious; private bool wasConscious;
@@ -55,6 +57,7 @@ namespace Barotrauma
private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5; private readonly float obstacleRaycastIntervalShort = 1, obstacleRaycastIntervalLong = 5;
private float obstacleRaycastTimer; private float obstacleRaycastTimer;
private bool isBlocked;
private readonly float enemyCheckInterval = 0.2f; private readonly float enemyCheckInterval = 0.2f;
private readonly float enemySpotDistanceOutside = 800; private readonly float enemySpotDistanceOutside = 800;
@@ -92,7 +95,10 @@ namespace Barotrauma
private readonly SteeringManager outsideSteering, insideSteering; private readonly SteeringManager outsideSteering, insideSteering;
public bool UseIndoorSteeringOutside { get; set; } = false; /// <summary>
/// Waypoints that are not linked to a sub (e.g. main path).
/// </summary>
public bool UseOutsideWaypoints { get; private set; }
public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager; public IndoorsSteeringManager PathSteering => insideSteering as IndoorsSteeringManager;
public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController; public HumanoidAnimController AnimController => Character.AnimController as HumanoidAnimController;
@@ -225,14 +231,15 @@ namespace Barotrauma
IgnoredItems.Clear(); IgnoredItems.Clear();
} }
bool IsCloseEnoughToTarget(float threshold, bool useTargetSub = true) // Note: returns false when useTargetSub is 'true' and the target is outside (targetSub is 'null')
bool IsCloseEnoughToTarget(float threshold, bool targetSub = true)
{ {
Entity target = SelectedAiTarget?.Entity; Entity target = SelectedAiTarget?.Entity;
if (target == null) if (target == null)
{ {
return false; return false;
} }
if (useTargetSub) if (targetSub)
{ {
if (target.Submarine is Submarine sub) if (target.Submarine is Submarine sub)
{ {
@@ -244,62 +251,71 @@ namespace Barotrauma
return false; return false;
} }
} }
return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow(threshold, 2); return Vector2.DistanceSquared(Character.WorldPosition, target.WorldPosition) < MathUtils.Pow2(threshold);
} }
bool hasValidPath = HasValidPath(); bool isOutside = Character.Submarine == null;
if (isOutside)
if (Character.Submarine == null)
{ {
obstacleRaycastTimer -= deltaTime; obstacleRaycastTimer -= deltaTime;
if (obstacleRaycastTimer <= 0) if (obstacleRaycastTimer <= 0)
{ {
bool hasValidPath = HasValidPath();
isBlocked = false;
UseOutsideWaypoints = false;
obstacleRaycastTimer = obstacleRaycastIntervalLong; obstacleRaycastTimer = obstacleRaycastIntervalLong;
if (SelectedAiTarget?.Entity == null || SelectedAiTarget.Entity is ISpatialEntity target && target.Submarine == null || !IsCloseEnoughToTarget(2000, useTargetSub: false)) ISpatialEntity spatialTarget = SelectedAiTarget?.Entity ?? ObjectiveManager.GetLastActiveObjective<AIObjectiveGoTo>()?.Target;
if (spatialTarget != null && (spatialTarget.Submarine == null || !IsCloseEnoughToTarget(2000, targetSub: false)))
{ {
// If the target is behind a level wall, switch to the pathing to get around the obstacles. // If the target is behind a level wall, switch to the pathing to get around the obstacles.
ISpatialEntity spatialTarget = SelectedAiTarget?.Entity; IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null;
if (spatialTarget == null) Vector2 rayEnd = spatialTarget.SimPosition;
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
{ {
var gotoObjective = ObjectiveManager.GetActiveObjective<AIObjectiveGoTo>(); rayEnd += targetSub.SimPosition;
spatialTarget = gotoObjective?.Target; ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
} }
if (spatialTarget == null) var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall);
isBlocked = obstacle != null;
// Don't use outside waypoints when blocked by a sub, because we should use the waypoints linked to the sub instead.
UseOutsideWaypoints = isBlocked && (obstacle.UserData is not Submarine sub || sub.Info.IsRuin);
bool resetPath = false;
if (UseOutsideWaypoints)
{ {
UseIndoorSteeringOutside = false; bool isUsingInsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine != null || n.Ruin != null);
if (isUsingInsideWaypoints)
{
resetPath = true;
}
} }
else else
{ {
IEnumerable<FarseerPhysics.Dynamics.Body> ignoredBodies = null; bool isUsingOutsideWaypoints = hasValidPath && HasValidPath(nodePredicate: n => n.Submarine == null && n.Ruin == null);
Vector2 rayEnd = spatialTarget.SimPosition; if (isUsingOutsideWaypoints)
Submarine targetSub = spatialTarget.Submarine;
if (targetSub != null)
{ {
rayEnd += targetSub.SimPosition; resetPath = true;
ignoredBodies = targetSub.PhysicsBody.FarseerBody.ToEnumerable();
} }
var obstacle = Submarine.PickBody(SimPosition, rayEnd, ignoredBodies, collisionCategory: Physics.CollisionLevel | Physics.CollisionWall); }
UseIndoorSteeringOutside = obstacle != null; if (resetPath)
{
PathSteering.ResetPath();
} }
} }
else else if (hasValidPath)
{ {
UseIndoorSteeringOutside = false; obstacleRaycastTimer = obstacleRaycastIntervalShort;
if (hasValidPath) // Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs).
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs())
{ {
obstacleRaycastTimer = obstacleRaycastIntervalShort; if (connectedSub == Submarine.MainSub) { continue; }
// Swimming outside and using the path finder -> check that the path is not blocked with anything (the path finder doesn't know about other subs). Vector2 rayStart = SimPosition - connectedSub.SimPosition;
foreach (var connectedSub in Submarine.MainSub.GetConnectedSubs()) Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{ {
if (connectedSub == Submarine.MainSub) { continue; } PathSteering.CurrentPath.Unreachable = true;
Vector2 rayStart = SimPosition - connectedSub.SimPosition; break;
Vector2 dir = PathSteering.CurrentPath.CurrentNode.WorldPosition - WorldPosition;
Vector2 rayEnd = rayStart + dir.ClampLength(Character.AnimController.Collider.GetLocalFront().Length() * 5);
if (Submarine.CheckVisibility(rayStart, rayEnd, ignoreSubs: true) != null)
{
PathSteering.CurrentPath.Unreachable = true;
break;
}
} }
} }
} }
@@ -307,10 +323,11 @@ namespace Barotrauma
} }
else else
{ {
UseIndoorSteeringOutside = false; UseOutsideWaypoints = false;
isBlocked = false;
} }
if (Character.Submarine == null || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID)) if (isOutside || Character.IsOnPlayerTeam && !Character.IsEscorted && !Character.IsOnFriendlyTeam(Character.Submarine.TeamID))
{ {
// Spot enemies while staying outside or inside an enemy ship. // Spot enemies while staying outside or inside an enemy ship.
// does not apply for escorted characters, such as prisoners or terrorists who have their own behavior // does not apply for escorted characters, such as prisoners or terrorists who have their own behavior
@@ -352,12 +369,13 @@ namespace Barotrauma
} }
} }
} }
bool useInsideSteering = !isOutside || isBlocked || HasValidPath() || IsCloseEnoughToTarget(steeringBuffer);
if (UseIndoorSteeringOutside || Character.CurrentHull?.Submarine != null || hasValidPath || IsCloseEnoughToTarget(steeringBuffer)) if (useInsideSteering)
{ {
if (steeringManager != insideSteering) if (steeringManager != insideSteering)
{ {
insideSteering.Reset(); insideSteering.Reset();
PathSteering.ResetPath();
steeringManager = insideSteering; steeringManager = insideSteering;
} }
if (IsCloseEnoughToTarget(maxSteeringBuffer)) if (IsCloseEnoughToTarget(maxSteeringBuffer))
@@ -420,6 +438,7 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls) foreach (Hull h in VisibleHulls)
{ {
PropagateHullSafety(Character, h); PropagateHullSafety(Character, h);
dirtyHullSafetyCalculations.Remove(h);
} }
} }
else else
@@ -427,18 +446,33 @@ namespace Barotrauma
foreach (Hull h in VisibleHulls) foreach (Hull h in VisibleHulls)
{ {
RefreshHullSafety(h); RefreshHullSafety(h);
dirtyHullSafetyCalculations.Remove(h);
} }
} }
foreach (Hull h in dirtyHullSafetyCalculations)
{
RefreshHullSafety(h);
}
} }
dirtyHullSafetyCalculations.Clear();
if (reportProblemsTimer <= 0.0f) if (reportProblemsTimer <= 0.0f)
{ {
if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck) if (Character.Submarine != null && (Character.Submarine.TeamID == Character.TeamID || Character.Submarine.TeamID == Character.OriginalTeamID || Character.IsEscorted) && !Character.Submarine.Info.IsWreck)
{ {
ReportProblems(); ReportProblems();
}
else
{
// Allows bots to heal targets autonomously while swimming outside of the sub.
if (AIObjectiveRescueAll.IsValidTarget(Character, Character))
{
AddTargets<AIObjectiveRescueAll, Character>(Character, Character);
}
} }
reportProblemsTimer = reportProblemsInterval; reportProblemsTimer = reportProblemsInterval;
} }
UpdateSpeaking(); SpeakAboutIssues();
UnequipUnnecessaryItems(); UnequipUnnecessaryItems();
reactTimer = GetReactionTime(); reactTimer = GetReactionTime();
} }
@@ -590,7 +624,7 @@ namespace Barotrauma
ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) || ObjectiveManager.CurrentObjective.GetSubObjectivesRecursive(true).Any(o => o.KeepDivingGearOn) ||
Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 || Character.CurrentHull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 10 ||
Character.CurrentHull.IsWetRoom; 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(); bool removeDivingSuit = !shouldKeepTheGearOn && !IsOrderedToWait();
if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1)) if (shouldActOnSuffocation && Character.CurrentHull.Oxygen > 0 && (!isCurrentObjectiveFindSafety || Character.OxygenAvailable < 1))
{ {
@@ -875,7 +909,7 @@ namespace Barotrauma
var container = i.GetComponent<ItemContainer>(); var container = i.GetComponent<ItemContainer>();
if (container == null) { return 0; } if (container == null) { return 0; }
if (!container.Inventory.CanBePut(containableItem)) { 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 (rootContainer.GetComponent<Fabricator>() != null || rootContainer.GetComponent<Deconstructor>() != null) { return 0; }
if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined)) if (container.ShouldBeContained(containableItem, out bool isRestrictionsDefined))
{ {
@@ -906,20 +940,40 @@ namespace Barotrauma
} }
})) }))
{ {
suitableContainer = targetContainer; if (targetContainer != null &&
return true; character.AIController is HumanAIController humanAI &&
humanAI.PathSteering.PathFinder.FindPath(character.SimPosition, targetContainer.SimPosition, character.Submarine, errorMsgStr: $"FindSuitableContainer ({character.DisplayName})", nodeFilter: node => node.Waypoint.CurrentHull != null).Unreachable)
{
ignoredItems.Add(targetContainer);
itemIndex = 0;
return false;
}
else
{
suitableContainer = targetContainer;
return true;
}
} }
return false; return false;
} }
private float draggedTimer; private float draggedTimer;
private float refuseDraggingTimer; 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 const float RefuseDraggingDuration = 30.0f;
private void UpdateDragged(float deltaTime) private void UpdateDragged(float deltaTime)
{ {
if (Character.HumanPrefab is { AllowDraggingIndefinitely: true }) { return; } 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 //don't allow player characters who aren't in the same team to drag us for more than x seconds
if (Character.SelectedBy == null || if (Character.SelectedBy == null ||
@@ -931,8 +985,8 @@ namespace Barotrauma
} }
draggedTimer += deltaTime; draggedTimer += deltaTime;
if (draggedTimer > RefuseDraggingAfter || if (draggedTimer > RefuseDraggingThresholdHigh ||
(draggedTimer > 0.5f && refuseDraggingTimer > 0.0f)) (refuseDraggingTimer > 0.0f && draggedTimer > RefuseDraggingThresholdLow))
{ {
draggedTimer = 0.0f; draggedTimer = 0.0f;
refuseDraggingTimer = RefuseDraggingDuration; refuseDraggingTimer = RefuseDraggingDuration;
@@ -945,7 +999,8 @@ namespace Barotrauma
{ {
Order newOrder = null; Order newOrder = null;
Hull targetHull = null; Hull targetHull = null;
bool speak = Character.SpeechImpediment < 100; // for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented
bool speak = Character.SpeechImpediment < 100 && !Character.IsEscorted;
if (Character.CurrentHull != null) if (Character.CurrentHull != null)
{ {
bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>(); bool isFighting = ObjectiveManager.HasActiveObjective<AIObjectiveCombat>();
@@ -1044,25 +1099,21 @@ namespace Barotrauma
} }
if (newOrder != null && speak) if (newOrder != null && speak)
{ {
// for now, escorted characters use the report system to get targets but do not speak. escort-character specific dialogue could be implemented if (Character.TeamID == CharacterTeamType.FriendlyNPC)
if (!Character.IsEscorted)
{ {
if (Character.TeamID == CharacterTeamType.FriendlyNPC) Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default,
{ identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(),
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Default, minDurationBetweenSimilar: 60.0f);
identifier: $"{newOrder.Prefab.Identifier}{targetHull?.RoomName ?? "null"}".ToIdentifier(), }
minDurationBetweenSimilar: 60.0f); else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
} {
else if (Character.IsOnPlayerTeam && GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime)) Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
{
Character.Speak(newOrder.GetChatMessage("", targetHull?.DisplayName?.Value ?? "", givingOrderToSelf: false), ChatMessageType.Order);
#if SERVER #if SERVER
GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder GameMain.Server.SendOrderChatMessage(new OrderChatMessage(newOrder
.WithManualPriority(CharacterInfo.HighestManualOrderPriority) .WithManualPriority(CharacterInfo.HighestManualOrderPriority)
.WithTargetEntity(targetHull) .WithTargetEntity(targetHull)
.WithOrderGiver(Character), "", null, Character)); .WithOrderGiver(Character), "", null, Character));
#endif #endif
}
} }
} }
} }
@@ -1093,21 +1144,33 @@ namespace Barotrauma
} }
} }
private void UpdateSpeaking() private void SpeakAboutIssues()
{ {
if (!Character.IsOnPlayerTeam) { return; } if (!Character.IsOnPlayerTeam) { return; }
if (Character.SpeechImpediment >= 100) { return; } if (Character.SpeechImpediment >= 100) { return; }
if (Character.Oxygen < 20.0f) float minDelay = 0.5f, maxDelay = 2f;
if (Character.Oxygen < CharacterHealth.InsufficientOxygenThreshold)
{ {
Character.Speak(TextManager.Get("DialogLowOxygen").Value, null, Rand.Range(0.5f, 5.0f), "lowoxygen".ToIdentifier(), 30.0f); string msgId = "DialogLowOxygen";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
} }
if (Character.Bleeding > 2.0f) if (Character.Bleeding > 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; bool isAccidental = attacker.IsBot && !IsMentallyUnstable && !attacker.AIController.IsMentallyUnstable && attacker.CombatAction == null;
if (isAccidental) if (isAccidental)
{ {
if (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold) if (attacker.TeamID != Character.TeamID || (!Character.IsSecurity && cumulativeDamage > minorDamageThreshold))
{ {
AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker); AddCombatObjective(AIObjectiveCombat.CombatMode.Retreat, attacker);
} }
@@ -1402,7 +1465,7 @@ namespace Barotrauma
} }
else else
{ {
if (humanAI.ObjectiveManager.GetActiveObjective<AIObjectiveCombat>()?.Enemy == attacker) if (humanAI.ObjectiveManager.GetLastActiveObjective<AIObjectiveCombat>()?.Enemy == attacker)
{ {
// Already targeting the attacker -> treat as a more serious threat. // Already targeting the attacker -> treat as a more serious threat.
cumulativeDamage *= 2; cumulativeDamage *= 2;
@@ -1587,7 +1650,7 @@ namespace Barotrauma
hull.LethalPressure > 0 || hull.LethalPressure > 0 ||
hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f)) hull.ConnectedGaps.Any(gap => !gap.IsRoomToRoom && gap.Open > 0.9f))
{ {
needsSuit = !Character.IsProtectedFromPressure; needsSuit = (hull == null || hull.LethalPressure > 0) && !Character.IsImmuneToPressure;
return needsAir || needsSuit; return needsAir || needsSuit;
} }
if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1) if (hull.WaterPercentage > 60 || hull.OxygenPercentage < HULL_LOW_OXYGEN_PERCENTAGE + 1)
@@ -1604,7 +1667,7 @@ namespace Barotrauma
/// </summary> /// </summary>
public static bool HasDivingSuit(Character character, float conditionPercentage = 0, bool requireOxygenTank = true) 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, => 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> /// <summary>
/// Check whether the character has a diving mask in usable condition plus some oxygen. /// 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); private static float GetReactionTime() => reactionTime * Rand.Range(0.75f, 1.25f);
/// <summary> /// <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. /// 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. /// There's now a cached value per each hull, which should prevent too frequent calculations.
/// </summary> /// </summary>
public static void PropagateHullSafety(Character character, Hull hull) public static void PropagateHullSafety(Character character, Hull hull)
{ {
DoForEachCrewMember(character, (humanAi) => humanAi.RefreshHullSafety(hull)); DoForEachBot(character, (humanAi) => humanAi.RefreshHullSafety(hull));
} }
public void AskToRecalculateHullSafety(Hull hull) => dirtyHullSafetyCalculations.Add(hull);
private void RefreshHullSafety(Hull 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); UnsafeHulls.Remove(hull);
} }
@@ -1916,7 +1983,7 @@ namespace Barotrauma
private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2> private static bool AddTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{ {
bool targetAdded = false; bool targetAdded = false;
DoForEachCrewMember(caller, humanAI => DoForEachBot(caller, humanAI =>
{ {
if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; } if (caller != humanAI.Character && caller.SpeechImpediment >= 100) { return; }
var objective = humanAI.ObjectiveManager.GetObjective<T1>(); var objective = humanAI.ObjectiveManager.GetObjective<T1>();
@@ -1933,7 +2000,7 @@ namespace Barotrauma
public static void RemoveTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2> public static void RemoveTargets<T1, T2>(Character caller, T2 target) where T1 : AIObjectiveLoop<T2>
{ {
DoForEachCrewMember(caller, humanAI => DoForEachBot(caller, humanAI =>
humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target)); humanAI.ObjectiveManager.GetObjective<T1>()?.ReportedTargets.Remove(target));
} }
@@ -2108,7 +2175,7 @@ namespace Barotrauma
if (other.IsPet) if (other.IsPet)
{ {
// Hostile NPCs are hostile to all pets, unless they are in the same team. // Hostile NPCs are hostile to all pets, unless they are in the same team.
if (!sameTeam && me.TeamID == CharacterTeamType.None) { return false; } return sameTeam || me.TeamID != CharacterTeamType.None;
} }
else else
{ {
@@ -2120,9 +2187,11 @@ namespace Barotrauma
(me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC)) (me.TeamID == CharacterTeamType.Team1 && other.TeamID == CharacterTeamType.FriendlyNPC))
{ {
Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other; Character npc = me.TeamID == CharacterTeamType.FriendlyNPC ? me : other;
//NPCs that allow some campaign interaction are not turned hostile by low reputation //NPCs that allow some campaign interaction are not turned hostile by low reputation
if (npc.CampaignInteractionType != CampaignMode.InteractionType.None) { return true; } 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(); return !npcAI.IsInHostileFaction();
} }
@@ -2134,6 +2203,7 @@ namespace Barotrauma
public bool IsInHostileFaction() public bool IsInHostileFaction()
{ {
if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; } if (GameMain.GameSession?.GameMode is not CampaignMode campaign) { return false; }
if (Character.IsEscorted) { return false; }
Identifier npcFaction = Character.Faction; Identifier npcFaction = Character.Faction;
Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty; Identifier currentLocationFaction = campaign.Map?.CurrentLocation?.Faction?.Prefab.Identifier ?? Identifier.Empty;
@@ -2145,8 +2215,7 @@ namespace Barotrauma
} }
if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction) if (!currentLocationFaction.IsEmpty && npcFaction == currentLocationFaction)
{ {
var reputation = campaign.Map?.CurrentLocation?.Reputation; if (campaign.CurrentLocation is { IsFactionHostile: true })
if (reputation != null && reputation.NormalizedValue < Reputation.HostileThreshold)
{ {
return true; return true;
} }
@@ -2154,71 +2223,89 @@ namespace Barotrauma
return false; 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; } if (character == null) { return false; }
foreach (var c in Character.CharacterList) 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; 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; } if (character == null) { return false; }
foreach (var c in Character.CharacterList) 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; 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; } if (character == null) { return 0; }
int count = 0; int count = 0;
foreach (var other in Character.CharacterList) foreach (var other in Character.CharacterList)
{ {
if (onlyActive && !IsActive(other)) if (!IsBotInTheCrew(character, other)) { continue; }
if (predicate == null || predicate(other.AIController as HumanAIController))
{ {
continue; count++;
}
if (onlyBots && other.IsPlayer)
{
continue;
}
if (FilterCrewMember(character, other))
{
if (predicate == null || predicate(other.AIController as HumanAIController))
{
count++;
}
} }
} }
return count; return count;
} }
public static void DoForEachCrewMember(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity) /// <summary>
/// Including the player characters in the same team.
/// </summary>
public bool IsTrueForAnyCrewMember(Func<Character, bool> predicate, bool onlyActive = true, bool onlyConnectedSubs = false)
{
foreach (var c in Character.CharacterList)
{
if (!IsActive(c)) { continue; }
if (c.TeamID != Character.TeamID) { continue; }
if (onlyActive && c.IsIncapacitated) { continue; }
if (onlyConnectedSubs)
{
if (Character.Submarine == null)
{
if (c.Submarine != null)
{
return false;
}
}
else if (c.Submarine != Character.Submarine && !Character.Submarine.GetConnectedSubs().Contains(c.Submarine))
{
return false;
}
}
if (predicate(c))
{
return true;
}
}
return false;
}
private static void DoForEachBot(Character character, Action<HumanAIController> action, float range = float.PositiveInfinity)
{ {
if (character == null) { return; } if (character == null) { return; }
foreach (var c in Character.CharacterList) foreach (var c in Character.CharacterList)
{ {
if (FilterCrewMember(character, c) && CheckReportRange(character, c, range)) if (IsBotInTheCrew(character, c) && CheckReportRange(character, c, range))
{ {
action(c.AIController as HumanAIController); action(c.AIController as HumanAIController);
} }
@@ -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) public static bool IsItemTargetedBySomeone(ItemComponent target, CharacterTeamType team, out Character operatingCharacter)
{ {
@@ -2283,10 +2370,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToOperateThis(Character.AIController); bool isOrder = IsOrderedToOperateThis(Character.AIController);
foreach (Character c in Character.CharacterList) foreach (Character c in Character.CharacterList)
{ {
if (!IsActive(c)) { continue; }
if (c == Character) { continue; } if (c == Character) { continue; }
if (c.Removed) { continue; }
if (c.TeamID != Character.TeamID) { continue; } if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
if (c.IsPlayer) if (c.IsPlayer)
{ {
if (c.SelectedItem == target.Item) if (c.SelectedItem == target.Item)
@@ -2354,9 +2440,9 @@ namespace Barotrauma
bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController); bool isOrder = IsOrderedToRepairThis(Character.AIController as HumanAIController);
foreach (var c in Character.CharacterList) foreach (var c in Character.CharacterList)
{ {
if (!IsActive(c)) { continue; }
if (c == Character) { continue; } if (c == Character) { continue; }
if (c.TeamID != Character.TeamID) { continue; } if (c.TeamID != Character.TeamID) { continue; }
if (c.IsIncapacitated) { continue; }
other = c; other = c;
if (c.IsPlayer) if (c.IsPlayer)
{ {
@@ -2370,7 +2456,7 @@ namespace Barotrauma
{ {
var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>(); var repairItemsObjective = operatingAI.ObjectiveManager.GetObjective<AIObjectiveRepairItems>();
if (repairItemsObjective == null) { continue; } if (repairItemsObjective == null) { continue; }
if (!(repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is AIObjectiveRepairItem activeObjective) || activeObjective.Item != target) if (repairItemsObjective.SubObjectives.FirstOrDefault(o => o is AIObjectiveRepairItem) is not AIObjectiveRepairItem activeObjective || activeObjective.Item != target)
{ {
// Not targeting the same item. // Not targeting the same item.
continue; continue;
@@ -2405,11 +2491,10 @@ namespace Barotrauma
} }
#region Wrappers #region Wrappers
public bool IsFriendly(Character other) => IsFriendly(Character, other); public bool IsFriendly(Character other, bool onlySameTeam = false) => IsFriendly(Character, other, onlySameTeam);
public void DoForEachCrewMember(Action<HumanAIController> action) => DoForEachCrewMember(Character, action); public bool IsTrueForAnyBotInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAnyBotInTheCrew(Character, predicate);
public bool IsTrueForAnyCrewMember(Func<HumanAIController, bool> predicate) => IsTrueForAnyCrewMember(Character, predicate); public bool IsTrueForAllBotsInTheCrew(Func<HumanAIController, bool> predicate) => IsTrueForAllBotsInTheCrew(Character, predicate);
public bool IsTrueForAllCrewMembers(Func<HumanAIController, bool> predicate) => IsTrueForAllCrewMembers(Character, predicate); public int CountBotsInTheCrew(Func<HumanAIController, bool> predicate = null) => CountBotsInTheCrew(Character, predicate);
public int CountCrew(Func<HumanAIController, bool> predicate = null, bool onlyActive = true, bool onlyBots = false) => CountCrew(Character, predicate, onlyActive, onlyBots);
#endregion #endregion
} }
} }
@@ -22,7 +22,10 @@ namespace Barotrauma
private readonly Character character; private readonly Character character;
private Vector2 currentTarget; /// <summary>
/// In sim units.
/// </summary>
private Vector2 currentTargetPos;
private float findPathTimer; private float findPathTimer;
@@ -40,11 +43,6 @@ namespace Barotrauma
get { return pathFinder; } get { return pathFinder; }
} }
public Vector2 CurrentTarget
{
get { return currentTarget; }
}
public bool IsPathDirty public bool IsPathDirty
{ {
get; get;
@@ -54,9 +52,9 @@ namespace Barotrauma
/// <summary> /// <summary>
/// Returns true if any node in the path is in stairs /// Returns true if any node in the path is in stairs
/// </summary> /// </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; public bool IsNextNodeLadder => GetNextLadder() != null;
@@ -64,14 +62,9 @@ namespace Barotrauma
{ {
get get
{ {
if (currentPath == null) { return false; } var currentLadder = GetCurrentLadder();
if (currentPath.CurrentNode == null) { return false; }
if (currentPath.NextNode == null) { return false; }
var currentLadder = currentPath.CurrentNode.Ladders;
if (currentLadder == null) { return false; } if (currentLadder == null) { return false; }
if (!currentLadder.Item.IsInteractable(character)) { return false; } return currentLadder == GetNextLadder();
var nextLadder = GetNextLadder();
return nextLadder != null && nextLadder == currentLadder;
} }
} }
@@ -107,13 +100,10 @@ namespace Barotrauma
findPathTimer -= step; findPathTimer -= step;
} }
public void SetPath(SteeringPath path) public void SetPath(Vector2 targetPos, SteeringPath path)
{ {
currentTargetPos = targetPos;
currentPath = path; currentPath = path;
if (path.Nodes.Any())
{
currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
}
findPathTimer = Math.Min(findPathTimer, 1.0f); findPathTimer = Math.Min(findPathTimer, 1.0f);
IsPathDirty = false; IsPathDirty = false;
} }
@@ -136,66 +126,28 @@ namespace Barotrauma
steering += addition; steering += addition;
} }
/// <summary> public Ladder GetCurrentLadder() => GetLadder(currentPath?.CurrentNode);
/// 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 GetNextLadder() => GetLadder(currentPath?.NextNode);
return null;
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) private Vector2 CalculateSteeringSeek(Vector2 target, float weight, float minGapSize = 0, Func<PathNode, bool> startNodeFilter = null, Func<PathNode, bool> endNodeFilter = null, Func<PathNode, bool> nodeFilter = null, bool checkVisibility = true)
{ {
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished; bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null;
if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f) if (!needsNewPath && character.Submarine != null && character.Params.PathFinderPriority > 0.5f)
{ {
Vector2 targetDiff = target - currentTarget; // If the target has moved, we need a new path.
if (currentPath != null && currentPath.Nodes.Any() && character.Submarine != null) // 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).
//target in a different sub than where the character is now Vector2 targetDiff = target - currentTargetPos;
//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 (targetDiff.LengthSquared() > 1) if (targetDiff.LengthSquared() > 1)
{ {
needsNewPath = true; needsNewPath = true;
@@ -205,15 +157,33 @@ namespace Barotrauma
if (needsNewPath || findPathTimer < -1.0f) if (needsNewPath || findPathTimer < -1.0f)
{ {
IsPathDirty = true; 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) if (findPathTimer < 0)
{ {
SkipCurrentPathNodes(); SkipCurrentPathNodes();
currentTarget = target; currentTargetPos = target;
Vector2 currentPos = host.SimPosition; Vector2 currentPos = host.SimPosition;
pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin; pathFinder.InsideSubmarine = character.Submarine != null && !character.Submarine.Info.IsRuin;
pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure; pathFinder.ApplyPenaltyToOutsideNodes = character.Submarine != null && !character.IsProtectedFromPressure;
var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility); var newPath = pathFinder.FindPath(currentPos, target, character.Submarine, "(Character: " + character.Name + ")", minGapSize, startNodeFilter, endNodeFilter, nodeFilter, checkVisibility: checkVisibility);
bool useNewPath = needsNewPath || currentPath == null || currentPath.CurrentNode == null || character.Submarine != null && findPathTimer < -1 && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0; bool useNewPath = needsNewPath;
if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable) if (!useNewPath && currentPath?.CurrentNode != null && newPath.Nodes.Any() && !newPath.Unreachable)
{ {
// Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset). // Check if the new path is the same as the old, in which case we just ignore it and continue using the old path (or the progress would reset).
@@ -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); 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() 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 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) 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; diff.Y = 0.0f;
} }
if (diff == Vector2.Zero) { return Vector2.Zero; } if (diff == Vector2.Zero) { return Vector2.Zero; }
@@ -328,12 +307,12 @@ namespace Barotrauma
} }
if (currentPath.Finished) 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) 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; bool doorsChecked = false;
checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime()); checkDoorsTimer = Math.Min(checkDoorsTimer, GetDoorCheckTime());
@@ -353,14 +332,46 @@ namespace Barotrauma
bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater; bool isDiving = character.AnimController.InWater && character.AnimController.HeadInWater;
// Only humanoids can climb ladders // Only humanoids can climb ladders
bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands; bool canClimb = character.AnimController is HumanoidAnimController && !character.LockHands;
Ladder currentLadder = currentPath.CurrentNode.Ladders; Ladder currentLadder = GetCurrentLadder();
if (currentLadder != null && !currentLadder.Item.IsInteractable(character))
{
currentLadder = null;
}
Ladder nextLadder = GetNextLadder(); Ladder nextLadder = GetNextLadder();
var ladders = currentLadder ?? nextLadder; 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 (useLadders && character.SelectedSecondaryItem != ladders.Item)
{ {
if (character.CanInteractWith(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) if (character.IsClimbing && useLadders)
{ {
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50)
{ {
//climbing ladders -> don't move horizontally // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
diff.X = 0.0f; NextNode(!doorsChecked);
} }
//at the same height as the waypoint else
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f;
if (heightDiff < colliderSize)
{ {
float heightFromFloor = character.AnimController.GetHeightFromFloor(); bool nextLadderSameAsCurrent = currentLadder == nextLadder;
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative. if (currentLadder != null && nextLadder != null)
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{ {
character.StopClimbing(); //climbing ladders -> don't move horizontally
diff.X = 0.0f;
} }
else if (nextLadder != null && !nextLadderSameAsCurrent) //at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float 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 (nextLadder != null && !nextLadderSameAsCurrent)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
{ {
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); 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); if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
} {
} //if the current node is below the character and the next one is above (or vice versa)
else if (nextLadder != null) //and both are on ladders, we can skip directly to the next one
{ //e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
//if the current node is below the character and the next one is above (or vice versa) NextNode(!doorsChecked);
//and both are on ladders, we can skip directly to the next one }
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{
NextNode(!doorsChecked);
} }
} }
return ConvertUnits.ToSimUnits(diff); return ConvertUnits.ToSimUnits(diff);
@@ -440,7 +471,6 @@ namespace Barotrauma
if (door == null || door.CanBeTraversed) if (door == null || door.CanBeTraversed)
{ {
float margin = MathHelper.Lerp(1, 5, MathHelper.Clamp(collider.LinearVelocity.Length() / 10, 0, 1)); 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 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 horizontalDistance = Math.Abs(character.WorldPosition.X - currentPath.CurrentNode.WorldPosition.X);
float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y); float verticalDistance = Math.Abs(character.WorldPosition.Y - currentPath.CurrentNode.WorldPosition.Y);
@@ -459,7 +489,6 @@ namespace Barotrauma
{ {
// Walking horizontally // Walking horizontally
Vector2 colliderBottom = character.AnimController.GetColliderBottom(); Vector2 colliderBottom = character.AnimController.GetColliderBottom();
Vector2 colliderSize = collider.GetSize();
Vector2 velocity = collider.LinearVelocity; 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 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. // 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); 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) if (currentPath.CurrentNode == null)
@@ -507,9 +539,9 @@ namespace Barotrauma
currentPath.SkipToNextNode(); 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.IsClosed)
{ {
if (!door.Item.IsInteractable(character)) { return false; } 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 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 //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.Submarine == null ||
if (currentWaypoint.ConnectedDoor.LinkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false } || currentWaypoint.Submarine == null || currentWaypoint.Submarine.Info.IsRuin || currentWaypoint.Submarine.Info.IsWreck) currentWaypoint.Submarine.Info is { IsPlayer: false } ||
!linkedGap.IsRoomToRoom ||
(linkedGap.IsRoomToRoom && currentWaypoint.CurrentHull is { IsWetRoom: false }))
{ {
shouldBeOpen = true; shouldBeOpen = true;
door = currentWaypoint.ConnectedDoor; door = currentWaypoint.ConnectedDoor;
@@ -213,7 +213,7 @@ namespace Barotrauma
{ {
foreach (Voronoi2.GraphEdge edge in cell.Edges) 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); Vector2 potentialAttachPos = ConvertUnits.ToSimUnits(intersection);
float distSqr = Vector2.DistanceSquared(character.SimPosition, potentialAttachPos); 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 (item == null) { return false; }
if (canEquip && !item.AllowedSlots.Contains(InvSlotType.Any)) bool canEquip = false;
if (item.AllowedSlots.Contains(InvSlotType.Any))
{
if (character.Inventory.IsAnySlotAvailable(item))
{
canEquip = true;
}
}
if (!canEquip)
{ {
canEquip = false;
var inv = character.Inventory; var inv = character.Inventory;
foreach (var allowedSlot in item.AllowedSlots) foreach (var allowedSlot in item.AllowedSlots)
{ {
if (!allowWearing)
{
if (!allowedSlot.HasFlag(InvSlotType.RightHand) && !allowedSlot.HasFlag(InvSlotType.LeftHand))
{
continue;
}
}
foreach (var slotType in inv.SlotTypes) foreach (var slotType in inv.SlotTypes)
{ {
if (!allowedSlot.HasFlag(slotType)) { continue; } if (!allowedSlot.HasFlag(slotType)) { continue; }
@@ -530,18 +546,9 @@ namespace Barotrauma
} }
} }
} }
return canEquip; return canEquip && character.Inventory.CanBePut(item);
}
protected bool CheckItemIdentifiersOrTags(Item item, ImmutableHashSet<Identifier> identifiersOrTags)
{
if (identifiersOrTags.Contains(item.Prefab.Identifier)) { return true; }
foreach (var identifier in identifiersOrTags)
{
if (item.HasTag(identifier)) { return true; }
}
return false;
} }
protected bool CanEquip(Item item) => CanEquip(character, item); protected bool CanEquip(Item item, bool allowWearing) => CanEquip(character, item, allowWearing);
} }
} }
@@ -64,7 +64,6 @@ namespace Barotrauma
if (subObjectives.Any()) { return; } if (subObjectives.Any()) { return; }
if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer)) if (HumanAIController.FindSuitableContainer(character, item, ignoredContainers, ref itemIndex, out Item suitableContainer))
{ {
itemIndex = 0;
if (suitableContainer != null) if (suitableContainer != null)
{ {
bool equip = item.GetComponent<Holdable>() != null || bool equip = item.GetComponent<Holdable>() != null ||
@@ -112,10 +111,7 @@ namespace Barotrauma
Abandon = true; Abandon = true;
} }
} }
else objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
{
objectiveManager.GetObjective<AIObjectiveIdle>().Wander(deltaTime);
}
} }
protected override bool CheckObjectiveSpecific() protected override bool CheckObjectiveSpecific()

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