This commit is contained in:
EvilFactory
2023-06-15 12:13:50 -03:00
210 changed files with 4491 additions and 2580 deletions
+1 -1
View File
@@ -54,7 +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:
- v1.0.13.2 - v1.0.20.1
- 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;
@@ -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;
@@ -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;
@@ -1156,6 +1157,26 @@ namespace Barotrauma
}); });
AssignRelayToServer("debugdraw", false); AssignRelayToServer("debugdraw", false);
AssignOnExecute("debugdrawlos", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
{
state = !GameMain.LightManager.DebugLos;
}
GameMain.LightManager.DebugLos = state;
NewMessage("Los debug draw mode " + (GameMain.LightManager.DebugLos ? "enabled" : "disabled"), Color.Yellow);
});
AssignOnExecute("debugwiring", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
{
state = !ConnectionPanel.DebugWiringMode;
}
ConnectionPanel.DebugWiringMode = state;
NewMessage("Wiring debug mode " + (ConnectionPanel.DebugWiringMode ? "enabled" : "disabled"), Color.Yellow);
});
AssignRelayToServer("debugdraw", false);
AssignOnExecute("devmode", (string[] args) => AssignOnExecute("devmode", (string[] args) =>
{ {
if (args.None() || !bool.TryParse(args[0], out bool state)) if (args.None() || !bool.TryParse(args[0], out bool state))
@@ -2824,7 +2845,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);
}
} }
} }
} }
@@ -29,7 +29,7 @@ namespace Barotrauma
} }
} }
for (int i = 0; i < resourceClusters.Count; i++) for (int i = 0; i < resourceAmounts.Count; i++)
{ {
var amount = msg.ReadByte(); var amount = msg.ReadByte();
var rotation = msg.ReadSingle(); var rotation = msg.ReadSingle();
@@ -54,7 +54,7 @@ namespace Barotrauma
CalculateMissionClusterPositions(); CalculateMissionClusterPositions();
for(int i = 0; i < resourceClusters.Count; i++) for(int i = 0; i < resourceAmounts.Count; i++)
{ {
var identifier = msg.ReadIdentifier(); var identifier = msg.ReadIdentifier();
var count = msg.ReadByte(); var count = msg.ReadByte();
@@ -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);
@@ -1,9 +1,9 @@
using Microsoft.Xna.Framework; using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
@@ -199,8 +199,8 @@ namespace Barotrauma
if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y)
{ {
size = new Point( size = new Point(
subElement.GetAttributeInt("width", 0), ParseSize(subElement, "width"),
subElement.GetAttributeInt("height", 0)); ParseSize(subElement, "height"));
break; break;
} }
} }
@@ -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;
} }
@@ -1,14 +1,12 @@
using Microsoft.Xna.Framework; using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
@@ -20,6 +18,26 @@ namespace Barotrauma
{ {
return element.NameAsIdentifier(); return element.NameAsIdentifier();
} }
protected int ParseSize(XElement element, string attributeName)
{
string valueStr = element.GetAttributeString(attributeName, string.Empty);
bool relativeToWidth = valueStr.EndsWith("vw");
bool relativeToHeight = valueStr.EndsWith("vh");
if (relativeToWidth || relativeToHeight)
{
string floatStr = valueStr.Substring(0, valueStr.Length - 2);
if (!float.TryParse(floatStr, NumberStyles.Any, CultureInfo.InvariantCulture, out float relativeHeight))
{
DebugConsole.ThrowError($"Error while parsing a {nameof(GUIComponentStyle)}: {valueStr} is not a valid size.");
}
return (int)(relativeHeight / 100.0f * (relativeToWidth ? GameMain.GraphicsWidth : GameMain.GraphicsHeight));
}
else
{
return element.GetAttributeInt(attributeName, 0);
}
}
} }
public abstract class GUISelector<T> where T : GUIPrefab public abstract class GUISelector<T> where T : GUIPrefab
@@ -166,7 +184,8 @@ namespace Barotrauma
Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue)); Point maxResolution = subElement.GetAttributePoint("maxresolution", new Point(int.MaxValue, int.MaxValue));
if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y) if (GameMain.GraphicsWidth <= maxResolution.X && GameMain.GraphicsHeight <= maxResolution.Y)
{ {
return (uint)Math.Round(subElement.GetAttributeInt("size", 14) * GameSettings.CurrentConfig.Graphics.TextScale); int rawSize = ParseSize(subElement, "size");
return (uint)Math.Round(rawSize * GameSettings.CurrentConfig.Graphics.TextScale);
} }
} }
return (uint)Math.Round(defaultSize * GameSettings.CurrentConfig.Graphics.TextScale); return (uint)Math.Round(defaultSize * GameSettings.CurrentConfig.Graphics.TextScale);
@@ -542,6 +542,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,
@@ -159,8 +159,9 @@ namespace Barotrauma
int crewAreaY = ButtonAreaTop.Bottom + Padding; int crewAreaY = ButtonAreaTop.Bottom + Padding;
int crewAreaHeight = ObjectiveAnchor.Top - Padding - crewAreaY; int crewAreaHeight = ObjectiveAnchor.Top - Padding - crewAreaY;
CrewArea = new Rectangle(Padding, crewAreaY, (int)Math.Max(400 * GUI.Scale, 220), crewAreaHeight);
float crewAreaWidthMultiplier = GUI.IsUltrawide ? GUI.HorizontalAspectRatio : 1.0f;
CrewArea = new Rectangle(Padding, crewAreaY, (int)(Math.Max(400 * GUI.Scale, 220) * crewAreaWidthMultiplier), crewAreaHeight);
InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY); InventoryAreaLower = new Rectangle(ChatBoxArea.Right + Padding * 7, inventoryTopY, GameMain.GraphicsWidth - Padding * 9 - ChatBoxArea.Width, GameMain.GraphicsHeight - inventoryTopY);
int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f); int healthWindowWidth = (int)(GameMain.GraphicsWidth * 0.5f);
@@ -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
@@ -1722,7 +1722,7 @@ namespace Barotrauma
static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel) static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel)
{ {
list.Content.ClearChildren(); list.Content.ClearChildren();
List<Item> allItems = Character.Controlled?.Inventory?.FindAllItems(recursive: true) ?? new List<Item>(); var allItems = CargoManager.FindAllItemsOnPlayerAndSub(Character.Controlled);
var resources = prefab.GetApplicableResources(targetLevel); var resources = prefab.GetApplicableResources(targetLevel);
@@ -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; }
@@ -672,7 +672,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();
@@ -956,7 +959,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();
@@ -376,6 +376,7 @@ namespace Barotrauma
- (0.1f * iconRelativeWidth) - (0.1f * iconRelativeWidth)
// Spacing // Spacing
- (7 * layoutGroup.RelativeSpacing); - (7 * layoutGroup.RelativeSpacing);
nameRelativeWidth = Math.Max(nameRelativeWidth, 0.25f);
var font = layoutGroup.Rect.Width < 150 ? GUIStyle.SmallFont : GUIStyle.Font; var font = layoutGroup.Rect.Width < 150 ? GUIStyle.SmallFont : GUIStyle.Font;
var nameBlock = new GUITextBlock( var nameBlock = new GUITextBlock(
@@ -121,6 +121,23 @@ namespace Barotrauma
{ {
return AllowedToManageCampaign(ClientPermissions.ManageMoney); return AllowedToManageCampaign(ClientPermissions.ManageMoney);
} }
protected GUIButton CreateEndRoundButton()
{
int buttonWidth = (int)(450 * GUI.xScale * (GUI.IsUltrawide ? 3.0f : 1.0f));
int buttonHeight = (int)(40 * GUI.yScale);
var rectT = HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y, buttonWidth, buttonHeight), GUI.Canvas);
rectT.Pivot = Pivot.Center;
return new GUIButton(rectT, TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton")
{
Pulse = true,
TextBlock =
{
Shadow = true,
AutoScaleHorizontal = true
}
};
}
public override void Draw(SpriteBatch spriteBatch) public override void Draw(SpriteBatch spriteBatch)
{ {
@@ -125,38 +125,25 @@ namespace Barotrauma
private void CreateButtons() private void CreateButtons()
{ {
int buttonHeight = (int) (GUI.Scale * 40), endRoundButton = CreateEndRoundButton();
buttonWidth = GUI.IntScale(450), endRoundButton.OnClicked = (btn, userdata) =>
buttonCenter = buttonHeight / 2,
screenMiddle = GameMain.GraphicsWidth / 2;
endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle - buttonWidth / 2, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, buttonWidth, buttonHeight), GUI.Canvas),
TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton")
{ {
Pulse = true, TryEndRoundWithFuelCheck(
TextBlock = onConfirm: () => GameMain.Client.RequestStartRound(),
{ onReturnToMapScreen: () =>
Shadow = true, {
AutoScaleHorizontal = true ShowCampaignUI = true;
}, if (CampaignUI == null) { InitCampaignUI(); }
OnClicked = (btn, userdata) => CampaignUI.SelectTab(InteractionType.Map);
{ });
TryEndRoundWithFuelCheck( return true;
onConfirm: () => GameMain.Client.RequestStartRound(),
onReturnToMapScreen: () =>
{
ShowCampaignUI = true;
if (CampaignUI == null) { InitCampaignUI(); }
CampaignUI.SelectTab(InteractionType.Map);
});
return true;
}
}; };
int readyButtonHeight = buttonHeight; int readyButtonWidth = (int)(GUI.Scale * 50 * (GUI.IsUltrawide ? 3.0f : 1.0f));
int readyButtonWidth = (int) (GUI.Scale * 50); int readyButtonHeight = (int)(GUI.Scale * 40);
int readyButtonCenter = readyButtonHeight / 2,
ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (buttonWidth / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas), screenMiddle = GameMain.GraphicsWidth / 2;
ReadyCheckButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(screenMiddle + (endRoundButton.Rect.Width / 2) + GUI.IntScale(16), HUDLayoutSettings.ButtonAreaTop.Center.Y - readyButtonCenter, readyButtonWidth, readyButtonHeight), GUI.Canvas),
style: "RepairBuyButton") style: "RepairBuyButton")
{ {
ToolTip = TextManager.Get("ReadyCheck.Tooltip"), ToolTip = TextManager.Get("ReadyCheck.Tooltip"),
@@ -206,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)
@@ -148,6 +148,9 @@ namespace Barotrauma
case "stats": case "stats":
LoadStats(subElement); LoadStats(subElement);
break; break;
case "eventmanager":
GameMain.GameSession.EventManager.Load(subElement);
break;
} }
} }
@@ -210,28 +213,14 @@ namespace Barotrauma
{ {
StartRound = () => { TryEndRound(); } StartRound = () => { TryEndRound(); }
}; };
}
private void CreateEndRoundButton() endRoundButton = CreateEndRoundButton();
{ endRoundButton.OnClicked = (btn, userdata) =>
int buttonHeight = (int)(GUI.Scale * 40);
int buttonWidth = GUI.IntScale(450);
endRoundButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle((GameMain.GraphicsWidth / 2) - (buttonWidth / 2), HUDLayoutSettings.ButtonAreaTop.Center.Y - (buttonHeight / 2), buttonWidth, buttonHeight), GUI.Canvas),
TextManager.Get("EndRound"), textAlignment: Alignment.Center, style: "EndRoundButton")
{ {
Pulse = true, TryEndRoundWithFuelCheck(
TextBlock = onConfirm: () => TryEndRound(),
{ onReturnToMapScreen: () => { ShowCampaignUI = true; CampaignUI.SelectTab(InteractionType.Map); });
Shadow = true, return true;
AutoScaleHorizontal = true
},
OnClicked = (btn, userdata) =>
{
TryEndRoundWithFuelCheck(
onConfirm: () => TryEndRound(),
onReturnToMapScreen: () => { ShowCampaignUI = true; CampaignUI.SelectTab(InteractionType.Map); });
return true;
}
}; };
} }
@@ -699,6 +688,11 @@ namespace Barotrauma
modeElement.Add(Settings.Save()); modeElement.Add(Settings.Save());
modeElement.Add(SaveStats()); modeElement.Add(SaveStats());
if (GameMain.GameSession?.EventManager != null)
{
modeElement.Add(GameMain.GameSession?.EventManager.Save());
}
//save and remove all items that are in someone's inventory so they don't get included in the sub file as well //save and remove all items that are in someone's inventory so they don't get included in the sub file as well
foreach (Character c in Character.CharacterList) foreach (Character c in Character.CharacterList)
{ {
@@ -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)
@@ -92,9 +92,6 @@ namespace Barotrauma.Items.Components
rect.Height = (int)(rect.Height * (1.0f - openState)); rect.Height = (int)(rect.Height * (1.0f - openState));
} }
//only merge the door's convex hull with overlapping wall segments if it's fully open or fully closed
//it's the heaviest part of changing the convex hull, and doesn't need to be done while the door is still in motion
bool mergeOverlappingSegments = openState <= 0.0f || openState >= 1.0f;
if (Window.Height > 0 && Window.Width > 0) if (Window.Height > 0 && Window.Width > 0)
{ {
if (IsHorizontal) if (IsHorizontal)
@@ -117,7 +114,7 @@ namespace Barotrauma.Items.Components
else else
{ {
convexHull2.Enabled = true; convexHull2.Enabled = true;
convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); SetVertices(convexHull2, rect2);
} }
} }
} }
@@ -141,7 +138,7 @@ namespace Barotrauma.Items.Components
else else
{ {
convexHull2.Enabled = true; convexHull2.Enabled = true;
convexHull2.SetVertices(GetConvexHullCorners(rect2), mergeOverlappingSegments); SetVertices(convexHull2, rect2);
} }
} }
} }
@@ -156,11 +153,23 @@ namespace Barotrauma.Items.Components
else else
{ {
convexHull.Enabled = true; convexHull.Enabled = true;
convexHull.SetVertices(GetConvexHullCorners(rect), mergeOverlappingSegments); SetVertices(convexHull, rect);
} }
} }
private void SetVertices(ConvexHull convexHull, Rectangle rect)
{
var verts = GetConvexHullCorners(rect);
Vector2 center = (verts[0] + verts[2]) / 2;
convexHull.SetVertices(
verts,
IsHorizontal ?
new Vector2[] { new Vector2(verts[0].X, center.Y), new Vector2(verts[2].X, center.Y) } :
new Vector2[] { new Vector2(center.X, verts[0].Y), new Vector2(center.X, verts[2].Y) });
convexHull.MaxMergeLosVerticesDist = 35.0f;
}
partial void UpdateProjSpecific(float deltaTime) partial void UpdateProjSpecific(float deltaTime)
{ {
if (shakeTimer > 0.0f) if (shakeTimer > 0.0f)
@@ -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,14 +555,36 @@ 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)
{ {
@@ -552,7 +640,11 @@ 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' + requiredItem.OverrideDescription;
}
else if (!requiredItemPrefab.Description.IsNullOrEmpty())
{ {
toolTipText += '\n' + requiredItemPrefab.Description; toolTipText += '\n' + requiredItemPrefab.Description;
} }
@@ -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;
} }
@@ -437,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 };
@@ -445,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>();
@@ -536,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();
@@ -551,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
@@ -731,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)
{ {
@@ -915,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;
@@ -1305,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);
@@ -1526,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
@@ -1539,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;
@@ -1686,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)
@@ -1512,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)
@@ -1922,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);
} }
} }
@@ -1934,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);
@@ -1965,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;
@@ -1982,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;
} }
} }
} }
@@ -178,8 +178,9 @@ namespace Barotrauma.Items.Components
var autoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.62f), paddedControlContainer.RectTransform, Anchor.BottomCenter), "OutlineFrame"); var autoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.62f), paddedControlContainer.RectTransform, Anchor.BottomCenter), "OutlineFrame");
var paddedAutoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.92f, 0.88f), autoPilotControls.RectTransform, Anchor.Center), style: null); var paddedAutoPilotControls = new GUIFrame(new RectTransform(new Vector2(0.92f, 0.88f), autoPilotControls.RectTransform, Anchor.Center), style: null);
int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f);
maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.TopCenter), maintainPosTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.TopCenter),
TextManager.Get("SteeringMaintainPos"), font: GUIStyle.SmallFont, style: "GUIRadioButton") ToolBox.LimitString(TextManager.Get("SteeringMaintainPos"), GUIStyle.SmallFont, textLimit), font: GUIStyle.SmallFont, style: "GUIRadioButton")
{ {
UserData = UIHighlightAction.ElementId.MaintainPosTickBox, UserData = UIHighlightAction.ElementId.MaintainPosTickBox,
Enabled = autoPilot, Enabled = autoPilot,
@@ -214,7 +215,6 @@ namespace Barotrauma.Items.Components
return true; return true;
} }
}; };
int textLimit = (int)(paddedAutoPilotControls.Rect.Width * 0.75f);
levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center), levelStartTickBox = new GUITickBox(new RectTransform(new Vector2(1, 0.333f), paddedAutoPilotControls.RectTransform, Anchor.Center),
GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, GUIStyle.SmallFont, textLimit), GameMain.GameSession?.StartLocation == null ? "" : ToolBox.LimitString(GameMain.GameSession.StartLocation.Name, GUIStyle.SmallFont, textLimit),
font: GUIStyle.SmallFont, style: "GUIRadioButton") font: GUIStyle.SmallFont, style: "GUIRadioButton")
@@ -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)
@@ -259,7 +299,7 @@ namespace Barotrauma.Items.Components
{ {
bool alreadyConnected = DraggingConnected.IsConnectedTo(panel.Item); bool alreadyConnected = DraggingConnected.IsConnectedTo(panel.Item);
DraggingConnected.RemoveConnection(panel.Item); DraggingConnected.RemoveConnection(panel.Item);
if (DraggingConnected.Connect(this, !alreadyConnected, true)) if (DraggingConnected.TryConnect(this, !alreadyConnected, true))
{ {
var otherConnection = DraggingConnected.OtherConnection(this); var otherConnection = DraggingConnected.OtherConnection(this);
ConnectWire(DraggingConnected); ConnectWire(DraggingConnected);
@@ -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()
@@ -225,7 +232,7 @@ namespace Barotrauma.Items.Components
foreach (var wire in newWires.Where(w => !connection.Wires.Contains(w)).ToArray()) foreach (var wire in newWires.Where(w => !connection.Wires.Contains(w)).ToArray())
{ {
connection.ConnectWire(wire); connection.ConnectWire(wire);
wire.Connect(connection, false); wire.TryConnect(connection, false);
} }
} }
@@ -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();
@@ -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,15 +27,21 @@ 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),
@@ -41,7 +49,7 @@ namespace Barotrauma
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)
{ {
@@ -324,13 +324,13 @@ namespace Barotrauma
} }
/*GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f); GUI.DrawLine(spriteBatch, new Vector2(drawRect.X, -WorldSurface), new Vector2(drawRect.Right, -WorldSurface), Color.Cyan * 0.5f);
for (int i = 0; i < waveY.Length - 1; i++) for (int i = 0; i < waveY.Length - 1; i++)
{ {
GUI.DrawLine(spriteBatch, GUI.DrawLine(spriteBatch,
new Vector2(drawRect.X + WaveWidth * i, -WorldSurface - waveY[i] - 10), new Vector2(drawRect.X + WaveWidth * i, -WorldSurface - waveY[i] - 10),
new Vector2(drawRect.X + WaveWidth * (i + 1), -WorldSurface - waveY[i + 1] - 10), Color.Blue * 0.5f); new Vector2(drawRect.X + WaveWidth * (i + 1), -WorldSurface - waveY[i + 1] - 10), Color.Blue * 0.5f);
}*/ }
} }
foreach (MapEntity e in linkedTo) foreach (MapEntity e in linkedTo)
@@ -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;
@@ -1,5 +1,4 @@
using Barotrauma.Extensions; using Barotrauma.Items.Components;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using System; using System;
@@ -32,13 +31,13 @@ namespace Barotrauma.Lights
public bool IsHorizontal; public bool IsHorizontal;
public bool IsAxisAligned; public bool IsAxisAligned;
public Vector2 SubmarineDrawPos;
public Segment(SegmentPoint start, SegmentPoint end, ConvexHull convexHull) public Segment(SegmentPoint start, SegmentPoint end, ConvexHull convexHull)
{ {
if (start.Pos.Y > end.Pos.Y) if (start.Pos.Y > end.Pos.Y)
{ {
var temp = start; (end, start) = (start, end);
start = end;
end = temp;
} }
Start = start; Start = start;
@@ -87,19 +86,23 @@ namespace Barotrauma.Lights
private readonly Segment[] segments = new Segment[4]; private readonly Segment[] segments = new Segment[4];
private readonly SegmentPoint[] vertices = new SegmentPoint[4]; private readonly SegmentPoint[] vertices = new SegmentPoint[4];
private readonly SegmentPoint[] losVertices = new SegmentPoint[4]; private readonly SegmentPoint[] losVertices = new SegmentPoint[2];
private readonly VectorPair[] losOffsets = new VectorPair[4]; private readonly Vector2[] losOffsets = new Vector2[2];
private readonly bool[] backFacing;
private readonly bool[] ignoreEdge;
private readonly bool isHorizontal; private readonly bool isHorizontal;
private readonly int thickness;
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,61 +133,59 @@ namespace Barotrauma.Lights
public Rectangle BoundingBox { get; private set; } public Rectangle BoundingBox { get; private set; }
public ConvexHull(Vector2[] points, Color color, MapEntity parent) public ConvexHull(Rectangle rect, bool isHorizontal, MapEntity parent)
{ {
if (shadowEffect == null) shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice)
{
shadowEffect = new BasicEffect(GameMain.Instance.GraphicsDevice)
{ {
VertexColorEnabled = true VertexColorEnabled = true
}; };
} penumbraEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice)
if (penumbraEffect == null)
{
penumbraEffect = new BasicEffect(GameMain.Instance.GraphicsDevice)
{ {
TextureEnabled = true, TextureEnabled = true,
LightingEnabled = false, LightingEnabled = false,
Texture = TextureLoader.FromFile("Content/Lights/penumbra.png") Texture = TextureLoader.FromFile("Content/Lights/penumbra.png")
}; };
}
ParentEntity = parent; ParentEntity = parent;
ShadowVertices = new VertexPositionColor[6 * 4]; ShadowVertices = new VertexPositionColor[6 * 4];
PenumbraVertices = new VertexPositionTexture[6 * 4]; PenumbraVertices = new VertexPositionTexture[6 * 4];
backFacing = new bool[4]; BoundingBox = rect;
ignoreEdge = new bool[4];
float minX = points[0].X, minY = points[0].Y, maxX = points[0].X, maxY = points[0].Y; this.isHorizontal = isHorizontal;
for (int i = 1; i < vertices.Length; i++)
{
if (points[i].X < minX) minX = points[i].X;
if (points[i].Y < minY) minY = points[i].Y;
if (points[i].X > maxX) maxX = points[i].X;
if (points[i].Y > minY) maxY = points[i].Y;
}
BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
isHorizontal = BoundingBox.Width > BoundingBox.Height;
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; }
} }
SetVertices(points); Vector2[] verts = new Vector2[]
{
new Vector2(rect.X, rect.Bottom),
new Vector2(rect.Right, rect.Bottom),
new Vector2(rect.Right, rect.Y),
new Vector2(rect.X, rect.Y),
};
Vector2[] losVerts;
if (this.isHorizontal)
{
thickness = rect.Height;
losVerts = new Vector2[] { new Vector2(rect.X, rect.Center.Y), new Vector2(rect.Right, rect.Center.Y) };
}
else
{
thickness = rect.Width;
losVerts = new Vector2[] { new Vector2(rect.Center.X, rect.Y), new Vector2(rect.Center.X, rect.Bottom) };
}
SetVertices(verts, losVerts);
Enabled = true; Enabled = true;
var chList = HullLists.Find(h => h.Submarine == parent.Submarine); var chList = HullLists.Find(h => h.Submarine == parent.Submarine);
@@ -196,249 +197,123 @@ namespace Barotrauma.Lights
foreach (ConvexHull ch in chList.List) foreach (ConvexHull ch in chList.List)
{ {
MergeOverlappingSegments(ch); MergeLosVertices(ch);
ch.MergeOverlappingSegments(this); ch.MergeLosVertices(this);
} }
chList.List.Add(this); chList.List.Add(this);
} }
private void MergeOverlappingSegments(ConvexHull ch) private void MergeLosVertices(ConvexHull ch, bool refreshOtherOverlappingHulls = true)
{ {
if (ch == this) { return; } if (ch == this) { return; }
if (isHorizontal == ch.isHorizontal) //merge dist in the direction parallel to the segment
//(e.g. how far up/down we can stretch a vertical segment)
float mergeDistParallel = MathHelper.Clamp(ch.thickness * 0.65f, 16, 512);
if (MaxMergeLosVerticesDist.HasValue)
{ {
//hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces) mergeDistParallel = Math.Max(mergeDistParallel, MaxMergeLosVerticesDist.Value);
float mergeDist = 16;
float mergeDistSqr = mergeDist * mergeDist;
Rectangle intersection = Rectangle.Intersect(BoundingBox, ch.BoundingBox);
int intersectionArea = intersection.Width * intersection.Height;
int bboxArea = BoundingBox.Width * BoundingBox.Height;
int otherBboxArea = ch.BoundingBox.Width * ch.BoundingBox.Height;
if (Math.Abs(intersectionArea - bboxArea) < mergeDistSqr) { return; }
if (Math.Abs(intersectionArea - otherBboxArea) < mergeDistSqr) { return; }
for (int i = 0; i < segments.Length; i++)
{
for (int j = 0; j < ch.segments.Length; j++)
{
if (segments[i].IsHorizontal != ch.segments[j].IsHorizontal) { continue; }
if (ignoreEdge[i] || ch.ignoreEdge[j]) { continue; }
//the segments must be at different sides of the convex hulls to be merged
//(e.g. the right edge of a wall piece and the left edge of another one)
var segment1Center = (segments[i].Start.Pos + segments[i].End.Pos) / 2.0f;
var segment2Center = (ch.segments[j].Start.Pos + ch.segments[j].End.Pos) / 2.0f;
if (Vector2.Dot(segment1Center - BoundingBox.Center.ToVector2(), segment2Center - ch.BoundingBox.Center.ToVector2()) > 0) { continue; }
if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < mergeDistSqr &&
Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < mergeDistSqr)
{
ignoreEdge[i] = true;
ch.ignoreEdge[j] = true;
MergeSegments(segments[i], ch.segments[j], true);
}
else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < mergeDistSqr &&
Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < mergeDistSqr)
{
ignoreEdge[i] = true;
ch.ignoreEdge[j] = true;
MergeSegments(segments[i], ch.segments[j], false);
}
}
}
}
for (int i = 0; i < segments.Length; i++)
{
if (ignoreEdge[i]) { continue; }
if (Vector2.DistanceSquared(segments[i].Start.Pos, segments[i].End.Pos) < 1.0f) { continue; }
for (int j = 0; j < ch.segments.Length; j++)
{
if (ch.ignoreEdge[j]) { continue; }
if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, ch.segments[j].End.Pos) < 1.0f) { continue; }
if (IsSegmentAInB(segments[i], ch.segments[j]))
{
ignoreEdge[i] = true;
if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].Start.Pos) < 4.0f)
{
ch.ShiftSegmentPoint(j, false, segments[i].End.Pos);
}
else if (Vector2.DistanceSquared(ch.segments[j].Start.Pos, segments[i].End.Pos) < 4.0f)
{
ch.ShiftSegmentPoint(j, false, segments[i].Start.Pos);
}
if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].Start.Pos) < 4.0f)
{
ch.ShiftSegmentPoint(j, true, segments[i].End.Pos);
}
else if (Vector2.DistanceSquared(ch.segments[j].End.Pos, segments[i].End.Pos) < 4.0f)
{
ch.ShiftSegmentPoint(j, true, segments[i].Start.Pos);
}
}
else if (IsSegmentAInB(ch.segments[j], segments[i]))
{
ch.ignoreEdge[j] = true;
if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].Start.Pos) < 4.0f)
{
ShiftSegmentPoint(i, false, ch.segments[j].End.Pos);
}
else if (Vector2.DistanceSquared(segments[i].Start.Pos, ch.segments[j].End.Pos) < 4.0f)
{
ShiftSegmentPoint(i, false, ch.segments[j].Start.Pos);
}
if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].Start.Pos) < 4.0f)
{
ShiftSegmentPoint(i, true, ch.segments[j].End.Pos);
}
else if (Vector2.DistanceSquared(segments[i].End.Pos, ch.segments[j].End.Pos) < 4.0f)
{
ShiftSegmentPoint(i, true, ch.segments[j].Start.Pos);
}
}
}
}
//ignore edges that are inside some other convex hull
for (int i = 0; i < vertices.Length; i++)
{
if (ch.IsPointInside(vertices[i].Pos))
{
if (ch.IsPointInside(vertices[(i + 1) % vertices.Length].Pos))
{
ignoreEdge[i] = true;
overlappingHulls.Add(ch);
}
}
}
}
private void ShiftSegmentPoint(int segmentIndex, bool end, Vector2 newPos)
{
var segment = segments[segmentIndex];
losOffsets[segmentIndex] ??= new VectorPair();
bool flipped = false;
if (Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.Start.Pos) > Vector2.DistanceSquared(vertices[segmentIndex].Pos, segment.End.Pos))
{
flipped = true;
}
if (end == !flipped)
{
losOffsets[segmentIndex].B = newPos;
} }
else else
{ {
losOffsets[segmentIndex].A = newPos; 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
public static bool IsSegmentAInB(Segment a, Segment b) if (!inflatedAABB.Contains(losVertices[0].Pos) &&
{ !inflatedAABB.Contains(losVertices[1].Pos))
if (Vector2.DistanceSquared(a.Start.Pos, a.End.Pos) > Vector2.DistanceSquared(b.Start.Pos, b.End.Pos))
{
return false;
}
Vector2 min = new Vector2(Math.Min(b.Start.Pos.X, b.End.Pos.X), Math.Min(b.Start.Pos.Y, b.End.Pos.Y));
min.X -= 1.0f; min.Y -= 1.0f;
if (a.Start.Pos.X < min.X) { return false; }
if (a.Start.Pos.Y < min.Y) { return false; }
if (a.End.Pos.X < min.X) { return false; }
if (a.End.Pos.Y < min.Y) { return false; }
Vector2 max = new Vector2(Math.Max(b.Start.Pos.X, b.End.Pos.X), Math.Max(b.Start.Pos.Y, b.End.Pos.Y));
max.X += 1.0f; max.Y += 1.0f;
if (a.Start.Pos.X > max.X) { return false; }
if (a.Start.Pos.Y > max.Y) { return false; }
if (a.End.Pos.X > max.X) { return false; }
if (a.End.Pos.Y > max.Y) { return false; }
float startDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.Start.Pos);
if (startDist > 1.0f) { return false; }
float endDist = MathUtils.LineToPointDistanceSquared(b.Start.Pos, b.End.Pos, a.End.Pos);
if (endDist > 1.0f) { return false; }
return true;
}
public bool IsPointInside(Vector2 point)
{
if (!BoundingBox.Contains(point)) { return false; }
Vector2 center = (vertices[0].Pos + vertices[1].Pos + vertices[2].Pos + vertices[3].Pos) * 0.25f;
for (int i = 0; i < 4; i++)
{
Vector2 segmentVector = vertices[(i + 1) % 4].Pos - vertices[i].Pos;
Vector2 centerToVertex = center - vertices[i].Pos;
Vector2 pointToVertex = point - vertices[i].Pos;
float dotCenter = Vector2.Dot(centerToVertex, segmentVector);
float dotPoint = Vector2.Dot(pointToVertex, segmentVector);
if ((dotCenter > 0f && dotPoint < 0f) || (dotCenter < 0f && dotPoint > 0f)) { return false; }
}
return true;
}
private void MergeSegments(Segment segment1, Segment segment2, bool startPointsMatch)
{
int startPointIndex = -1, endPointIndex = -1;
for (int i = 0; i < vertices.Length; i++)
{
if (vertices[i].Pos.NearlyEquals(segment1.Start.Pos))
startPointIndex = i;
else if (vertices[i].Pos.NearlyEquals(segment1.End.Pos))
endPointIndex = i;
}
if (startPointIndex == -1 || endPointIndex == -1) { return; }
int startPoint2Index = -1, endPoint2Index = -1;
for (int i = 0; i < segment2.ConvexHull.vertices.Length; i++)
{
if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.Start.Pos))
startPoint2Index = i;
else if (segment2.ConvexHull.vertices[i].Pos.NearlyEquals(segment2.End.Pos))
endPoint2Index = i;
}
if (startPoint2Index == -1 || endPoint2Index == -1) { return; }
if (startPointsMatch)
{
losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos =
(segment1.Start.Pos + segment2.Start.Pos) / 2.0f;
losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos =
(segment1.End.Pos + segment2.End.Pos) / 2.0f;
}
else
{
if (Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.Start.Pos) <
Vector2.DistanceSquared(losVertices[startPointIndex].Pos, segment1.End.Pos))
{ {
losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos = mergeDistParallel = Math.Min(mergeDistParallel, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) * 0.5f);
(segment1.Start.Pos + segment2.End.Pos) / 2.0f;
losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos =
(segment1.End.Pos + segment2.Start.Pos) / 2.0f;
}
else
{
losVertices[startPointIndex].Pos = segment2.ConvexHull.losVertices[startPoint2Index].Pos =
(segment1.End.Pos + segment2.Start.Pos) / 2.0f;
losVertices[endPointIndex].Pos = segment2.ConvexHull.losVertices[endPoint2Index].Pos =
(segment1.Start.Pos + segment2.End.Pos) / 2.0f;
} }
} }
//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);
overlappingHulls.Add(segment2.ConvexHull); Vector2 center = (losVertices[0].Pos + losVertices[1].Pos) / 2;
segment2.ConvexHull.overlappingHulls.Add(this);
bool changed = false;
for (int i = 0; i < losVertices.Length; i++)
{
Vector2 segmentDir = Vector2.Normalize(losVertices[i].Pos - center);
//check if the closest point on the other convex hull segment is close enough, disregarding any offsets
//otherwise we might end up moving the vertex too much if we stretch it to an already-offset segment
if (!isCloseEnough(
MathUtils.GetClosestPointOnLineSegment(ch.losVertices[0].Pos, ch.losVertices[1].Pos, losVertices[i].Pos),
losVertices[i].Pos))
{
continue;
}
//check the offset position of the segment next
Vector2 closest = MathUtils.GetClosestPointOnLineSegment(
ch.losVertices[0].Pos + ch.losOffsets[0],
ch.losVertices[1].Pos + ch.losOffsets[1],
losVertices[i].Pos);
if (!isCloseEnough(closest, losVertices[i].Pos)) { continue; }
//find where the segments would intersect if they had infinite length
// if it's close to the closest point, let's use that instead to keep
// the direction of the segment unchanged (i.e. vertical segment stays vertical)
if (MathUtils.GetLineIntersection(
ch.losVertices[0].Pos + ch.losOffsets[0], ch.losVertices[1].Pos + ch.losOffsets[1],
losVertices[0].Pos, losVertices[1].Pos,
areLinesInfinite: true, out Vector2 intersection) &&
//the intersection needs to be outwards from the vertex we're checking
Vector2.Dot(segmentDir, intersection - losVertices[i].Pos) > 0 &&
//the intersection needs to be close enough to the default position of the vertex and the closest point
//(we don't want to merge the segments somewhere close to infinity!)
(Vector2.DistanceSquared(intersection, losVertices[i].Pos) < mergeDistParallel * mergeDistParallel ||
Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f))
{
closest = intersection;
}
//don't move the vertices of the segment too close to each other
if (Vector2.DistanceSquared(losVertices[1 - i].Pos + losOffsets[1 - i], closest) < mergeDistPerpendicular * mergeDistPerpendicular)
{
continue;
}
losOffsets[i] = closest - losVertices[i].Pos;
overlappingHulls.Add(ch);
ch.overlappingHulls.Add(this);
changed = true;
bool isCloseEnough(Vector2 closest, Vector2 vertex)
{
float dist = Vector2.Distance(closest, vertex);
if (dist < 0.001f) { return true; }
if (dist > mergeDistParallel) { return false; }
Vector2 closestDir = (closest - vertex) / dist;
float dot = Math.Abs(Vector2.Dot(segmentDir, closestDir));
float distAlongAxis = dist * dot;
if (distAlongAxis > mergeDistParallel) { return false; }
float distPerpendicular = dist * (1.0f - dot);
if (distPerpendicular > mergeDistPerpendicular) { return false; }
return true;
}
}
if (changed && refreshOtherOverlappingHulls)
{
foreach (var overlapping in overlappingHulls)
{
overlapping.MergeLosVertices(this, refreshOtherOverlappingHulls: false);
}
}
}
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)
@@ -447,7 +322,7 @@ namespace Barotrauma.Lights
Matrix.CreateTranslation(-origin.X, -origin.Y, 0.0f) * Matrix.CreateTranslation(-origin.X, -origin.Y, 0.0f) *
Matrix.CreateRotationZ(amount) * Matrix.CreateRotationZ(amount) *
Matrix.CreateTranslation(origin.X, origin.Y, 0.0f); Matrix.CreateTranslation(origin.X, origin.Y, 0.0f);
SetVertices(vertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix); SetVertices(vertices.Select(v => v.Pos).ToArray(), losVertices.Select(v => v.Pos).ToArray(), rotationMatrix: rotationMatrix);
} }
private void CalculateDimensions() private void CalculateDimensions()
@@ -456,11 +331,10 @@ namespace Barotrauma.Lights
for (int i = 1; i < vertices.Length; i++) for (int i = 1; i < vertices.Length; i++)
{ {
if (vertices[i].Pos.X < minX) minX = vertices[i].Pos.X; minX = Math.Min(minX, vertices[i].Pos.X);
if (vertices[i].Pos.Y < minY) minY = vertices[i].Pos.Y; minY = Math.Min(minY, vertices[i].Pos.Y);
maxX = Math.Max(maxX, vertices[i].Pos.X);
if (vertices[i].Pos.X > maxX) maxX = vertices[i].Pos.X; maxY = Math.Max(maxY, vertices[i].Pos.Y);
if (vertices[i].Pos.Y > minY) maxY = vertices[i].Pos.Y;
} }
BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY)); BoundingBox = new Rectangle((int)minX, (int)minY, (int)(maxX - minX), (int)(maxY - minY));
@@ -471,21 +345,17 @@ namespace Barotrauma.Lights
for (int i = 0; i < vertices.Length; i++) for (int i = 0; i < vertices.Length; i++)
{ {
vertices[i].Pos += amount; vertices[i].Pos += amount;
losVertices[i].Pos += amount;
losOffsets[i] = null;
segments[i].Start.Pos += amount; segments[i].Start.Pos += amount;
segments[i].End.Pos += amount; segments[i].End.Pos += amount;
} }
for (int i = 0; i < losVertices.Length; i++)
{
losVertices[i].Pos += amount;
}
LastVertexChangeTime = (float)Timing.TotalTime; LastVertexChangeTime = (float)Timing.TotalTime;
overlappingHulls.Clear(); overlappingHulls.Clear();
for (int i = 0; i < 4; i++)
{
ignoreEdge[i] = false;
}
CalculateDimensions(); CalculateDimensions();
@@ -497,8 +367,8 @@ namespace Barotrauma.Lights
overlappingHulls.Clear(); overlappingHulls.Clear();
foreach (ConvexHull ch in chList.List) foreach (ConvexHull ch in chList.List)
{ {
MergeOverlappingSegments(ch); MergeLosVertices(ch);
ch.MergeOverlappingSegments(this); ch.MergeLosVertices(this);
} }
} }
} }
@@ -511,23 +381,23 @@ namespace Barotrauma.Lights
foreach (ConvexHull ch in chList.List) foreach (ConvexHull ch in chList.List)
{ {
ch.overlappingHulls.Clear(); ch.overlappingHulls.Clear();
for (int i = 0; i < 4; i++) for (int i = 0; i < ch.losOffsets.Length; i++)
{ {
ch.ignoreEdge[i] = false; ch.losOffsets[i] = Vector2.Zero;
} }
} }
for (int i = 0; i < chList.List.Count; i++) for (int i = 0; i < chList.List.Count; i++)
{ {
for (int j = i + 1; j < chList.List.Count; j++) for (int j = i + 1; j < chList.List.Count; j++)
{ {
chList.List[i].MergeOverlappingSegments(chList.List[j]); chList.List[i].MergeLosVertices(chList.List[j]);
chList.List[j].MergeOverlappingSegments(chList.List[i]); chList.List[j].MergeLosVertices(chList.List[i]);
} }
} }
} }
} }
public void SetVertices(Vector2[] points, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null) public void SetVertices(Vector2[] points, Vector2[] losPoints, bool mergeOverlappingSegments = true, Matrix? rotationMatrix = null)
{ {
Debug.Assert(points.Length == 4, "Only rectangular convex hulls are supported"); Debug.Assert(points.Length == 4, "Only rectangular convex hulls are supported");
@@ -535,39 +405,24 @@ namespace Barotrauma.Lights
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
vertices[i] = new SegmentPoint(points[i], this); vertices[i] = new SegmentPoint(points[i], this);
losVertices[i] = new SegmentPoint(points[i], this);
losOffsets[i] = null;
} }
for (int i = 0; i < 2; i++)
for (int i = 0; i < 4; i++)
{ {
ignoreEdge[i] = false; losVertices[i] = new SegmentPoint(losPoints[i], this);
losOffsets[i] = Vector2.Zero;
} }
overlappingHulls.Clear(); overlappingHulls.Clear();
int margin = 0;
if (Math.Abs(points[0].X - points[2].X) < Math.Abs(points[0].Y - points[2].Y))
{
losVertices[0].Pos = new Vector2(points[0].X + margin, points[0].Y);
losVertices[1].Pos = new Vector2(points[1].X + margin, points[1].Y);
losVertices[2].Pos = new Vector2(points[2].X - margin, points[2].Y);
losVertices[3].Pos = new Vector2(points[3].X - margin, points[3].Y);
}
else
{
losVertices[0].Pos = new Vector2(points[0].X, points[0].Y + margin);
losVertices[1].Pos = new Vector2(points[1].X, points[1].Y - margin);
losVertices[2].Pos = new Vector2(points[2].X, points[2].Y - margin);
losVertices[3].Pos = new Vector2(points[3].X, points[3].Y + margin);
}
if (rotationMatrix.HasValue) if (rotationMatrix.HasValue)
{ {
for (int i = 0; i < vertices.Length; i++) for (int i = 0; i < vertices.Length; i++)
{ {
vertices[i].Pos = Vector2.Transform(vertices[i].Pos, rotationMatrix.Value); vertices[i].Pos = Vector2.Transform(vertices[i].Pos, rotationMatrix.Value);
}
for (int i = 0; i < losVertices.Length; i++)
{
losVertices[i].Pos = Vector2.Transform(losVertices[i].Pos, rotationMatrix.Value); losVertices[i].Pos = Vector2.Transform(losVertices[i].Pos, rotationMatrix.Value);
} }
} }
@@ -588,7 +443,7 @@ namespace Barotrauma.Lights
overlappingHulls.Clear(); overlappingHulls.Clear();
foreach (ConvexHull ch in chList.List) foreach (ConvexHull ch in chList.List)
{ {
MergeOverlappingSegments(ch); MergeLosVertices(ch);
} }
} }
} }
@@ -610,31 +465,17 @@ namespace Barotrauma.Lights
/// <summary> /// <summary>
/// Returns the segments that are facing towards viewPosition /// Returns the segments that are facing towards viewPosition
/// </summary> /// </summary>
public void GetVisibleSegments(Vector2 viewPosition, List<Segment> visibleSegments, bool ignoreEdges) public void GetVisibleSegments(Vector2 viewPosition, List<Segment> visibleSegments)
{ {
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
if (ignoreEdge[i] && ignoreEdges) { continue; } if (IsSegmentFacing(vertices[i].WorldPos, vertices[(i + 1) % 4].WorldPos, viewPosition))
Vector2 pos1 = vertices[i].WorldPos;
Vector2 pos2 = vertices[(i + 1) % 4].WorldPos;
Vector2 middle = (pos1 + pos2) / 2;
Vector2 L = viewPosition - middle;
Vector2 N = new Vector2(
-(pos2.Y - pos1.Y),
pos2.X - pos1.X);
if (Vector2.Dot(N, L) > 0)
{ {
visibleSegments.Add(segments[i]); visibleSegments.Add(segments[i]);
} }
} }
} }
public void RefreshWorldPositions() public void RefreshWorldPositions()
{ {
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
@@ -662,34 +503,12 @@ namespace Barotrauma.Lights
ShadowVertexCount = 0; ShadowVertexCount = 0;
//compute facing of each edge, using N*L for (int i = 0; i < losVertices.Length; i++)
for (int i = 0; i < 4; i++)
{ {
if (ignoreEdge[i])
{
backFacing[i] = false;
continue;
}
Vector2 firstVertex = losVertices[i].Pos;
Vector2 secondVertex = losVertices[(i+1) % 4].Pos;
Vector2 L = lightSourcePos - ((firstVertex + secondVertex) / 2.0f);
Vector2 N = new Vector2(
-(secondVertex.Y - firstVertex.Y),
secondVertex.X - firstVertex.X);
backFacing[i] = (Vector2.Dot(N, L) < 0);
}
ShadowVertexCount = 0;
for (int i = 0; i < 4; i++)
{
if (!backFacing[i]) { continue; }
int currentIndex = i; int currentIndex = i;
Vector3 vertexPos0 = new Vector3(losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos, 0.0f); int nextIndex = (currentIndex + 1) % 2;
Vector3 vertexPos1 = new Vector3(losOffsets[currentIndex]?.B ?? losVertices[(currentIndex + 1) % 4].Pos, 0.0f); Vector3 vertexPos0 = new Vector3(losVertices[currentIndex].Pos + losOffsets[currentIndex], 0.0f);
Vector3 vertexPos1 = new Vector3(losVertices[nextIndex].Pos + losOffsets[nextIndex], 0.0f);
if (Vector3.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } if (Vector3.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; }
@@ -740,9 +559,24 @@ namespace Barotrauma.Lights
ShadowVertexCount += 6; ShadowVertexCount += 6;
} }
if (IsSegmentFacing(losVertices[0].Pos, losVertices[1].Pos, lightSourcePos))
{
Array.Reverse(ShadowVertices);
}
CalculateLosPenumbraVertices(lightSourcePos); CalculateLosPenumbraVertices(lightSourcePos);
} }
private static bool IsSegmentFacing(Vector2 segmentPos1, Vector2 segmentPos2, Vector2 viewPosition)
{
Vector2 segmentMid = (segmentPos1 + segmentPos2) / 2;
Vector2 segmentDiff = segmentPos2 - segmentPos1;
Vector2 segmentNormal = new Vector2(-segmentDiff.Y, segmentDiff.X);
Vector2 viewDirection = viewPosition - segmentMid;
return Vector2.Dot(segmentNormal, viewDirection) > 0;
}
private void CalculateLosPenumbraVertices(Vector2 lightSourcePos) private void CalculateLosPenumbraVertices(Vector2 lightSourcePos)
{ {
Vector3 offset = Vector3.Zero; Vector3 offset = Vector3.Zero;
@@ -752,73 +586,104 @@ namespace Barotrauma.Lights
} }
PenumbraVertexCount = 0; PenumbraVertexCount = 0;
for (int i = 0; i < 4; i++) for (int i = 0; i < losVertices.Length; i++)
{ {
int currentIndex = i; int currentIndex = i;
int prevIndex = (i + 3) % 4; int nextIndex = (i + 1) % 2;
int nextIndex = (i + 1) % 4; Vector2 vertexPos0 = losVertices[currentIndex].Pos + losOffsets[currentIndex];
bool disjointed = losOffsets[i]?.A != null; Vector2 vertexPos1 = losVertices[nextIndex].Pos + losOffsets[nextIndex];
Vector2 vertexPos0 = losOffsets[currentIndex]?.A ?? losVertices[currentIndex].Pos;
Vector2 vertexPos1 = losOffsets[currentIndex]?.B ?? losVertices[nextIndex].Pos;
if (Vector2.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; } if (Vector2.DistanceSquared(vertexPos0, vertexPos1) < 1.0f) { continue; }
if (backFacing[currentIndex] && (disjointed || (!backFacing[prevIndex]))) Vector3 penumbraStart = new Vector3(vertexPos0, 0.0f);
PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture
{ {
Vector3 penumbraStart = new Vector3(vertexPos0, 0.0f); Position = penumbraStart + offset,
TextureCoordinate = new Vector2(0.0f, 1.0f)
};
PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture for (int j = 0; j < 2; j++)
{ {
Position = penumbraStart + offset, PenumbraVertices[PenumbraVertexCount + j + 1] = new VertexPositionTexture();
TextureCoordinate = new Vector2(0.0f, 1.0f) Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0);
}; vertexDir.Normalize();
for (int j = 0; j < 2; j++) Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f;
{
PenumbraVertices[PenumbraVertexCount + j + 1] = new VertexPositionTexture();
Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0);
vertexDir.Normalize();
Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f);
vertexDir.Normalize();
PenumbraVertices[PenumbraVertexCount + j + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset;
vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) - normal * 20.0f); PenumbraVertices[PenumbraVertexCount + j + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f);
vertexDir.Normalize();
PenumbraVertices[PenumbraVertexCount + j + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset;
PenumbraVertices[PenumbraVertexCount + j + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f);
}
PenumbraVertexCount += 3;
} }
disjointed = losOffsets[i]?.B != null; PenumbraVertexCount += 3;
if (backFacing[currentIndex] && (disjointed || (!backFacing[nextIndex])))
penumbraStart = new Vector3(vertexPos1, 0.0f);
PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture
{ {
Vector3 penumbraStart = new Vector3(vertexPos1, 0.0f); Position = penumbraStart + offset,
TextureCoordinate = new Vector2(0.0f, 1.0f)
};
PenumbraVertices[PenumbraVertexCount] = new VertexPositionTexture for (int j = 0; j < 2; j++)
{ {
Position = penumbraStart + offset, PenumbraVertices[PenumbraVertexCount + (1 - j) + 1] = new VertexPositionTexture();
TextureCoordinate = new Vector2(0.0f, 1.0f) Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0);
}; vertexDir.Normalize();
for (int j = 0; j < 2; j++) Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f;
{
PenumbraVertices[PenumbraVertexCount + (1 - j) + 1] = new VertexPositionTexture();
Vector3 vertexDir = penumbraStart - new Vector3(lightSourcePos, 0);
vertexDir.Normalize();
Vector3 normal = (j == 0) ? new Vector3(-vertexDir.Y, vertexDir.X, 0.0f) : new Vector3(vertexDir.Y, -vertexDir.X, 0.0f) * 0.05f; vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) + normal * 20.0f);
vertexDir.Normalize();
PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset;
vertexDir = penumbraStart - (new Vector3(lightSourcePos, 0) + normal * 20.0f); PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f);
vertexDir.Normalize();
PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].Position = new Vector3(lightSourcePos, 0) + vertexDir * 9000 + offset;
PenumbraVertices[PenumbraVertexCount + (1 - j) + 1].TextureCoordinate = (j == 0) ? new Vector2(0.05f, 0.0f) : new Vector2(1.0f, 0.0f);
}
PenumbraVertexCount += 3;
} }
PenumbraVertexCount += 3;
}
}
public void DebugDraw(SpriteBatch spriteBatch)
{
//RecalculateAll(Submarine.MainSub);
//RefreshWorldPositions();
DrawLine(losVertices[0].Pos, losVertices[1].Pos, Color.Gray * 0.5f, width: 3);
DrawLine(losVertices[0].Pos + losOffsets[0], losVertices[1].Pos + losOffsets[1], Color.LightGreen, width: 2);
DrawLine(GameMain.GameScreen.Cam.Position + Vector2.One * 1000, GameMain.GameScreen.Cam.Position - Vector2.One * 1000, Color.Magenta, width: 2);
if (GameMain.LightManager.LightingEnabled)
{
for (int i = 0; i < vertices.Length; i++)
{
Vector2 start = vertices[i].Pos;
Vector2 end = vertices[(i + 1) % 4].Pos;
DrawLine(
start,
end, Color.Yellow * 0.5f,
width: 4);
}
}
void DrawLine(Vector2 vertexPos0, Vector2 vertexPos1, Color color, int width)
{
if (ParentEntity != null && ParentEntity.Submarine != null)
{
vertexPos0 += ParentEntity.Submarine.DrawPosition;
vertexPos1 += ParentEntity.Submarine.DrawPosition;
}
float alpha = 1.0f;
if (LightManager.ViewTarget != null)
{
alpha = IsSegmentFacing(vertexPos0, vertexPos1, LightManager.ViewTarget.WorldPosition) ? 1.0f : 0.5f;
}
vertexPos0.Y = -vertexPos0.Y;
vertexPos1.Y = -vertexPos1.Y;
GUI.DrawLine(spriteBatch, vertexPos0, vertexPos1, color * alpha, width: width);
} }
} }
@@ -889,16 +754,13 @@ namespace Barotrauma.Lights
{ {
HullLists.Remove(chList); HullLists.Remove(chList);
} }
foreach (ConvexHull ch2 in overlappingHulls) //create a new list because MergeLosVertices can edit overlappingHulls
foreach (ConvexHull ch2 in overlappingHulls.ToList())
{ {
for (int i = 0; i < 4; i++)
{
ch2.ignoreEdge[i] = false;
}
ch2.overlappingHulls.Remove(this); ch2.overlappingHulls.Remove(this);
foreach (ConvexHull ch in chList.List) foreach (ConvexHull ch in chList.List)
{ {
ch.MergeOverlappingSegments(ch2); ch.MergeLosVertices(ch2);
} }
} }
} }
@@ -5,6 +5,7 @@ using System.Linq;
using System; using System;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using System.Threading;
namespace Barotrauma.Lights namespace Barotrauma.Lights
{ {
@@ -22,6 +23,9 @@ namespace Barotrauma.Lights
/// </summary> /// </summary>
const float ObstructLightsBehindCharactersZoomThreshold = 0.5f; const float ObstructLightsBehindCharactersZoomThreshold = 0.5f;
private Thread rayCastThread;
private Queue<RayCastTask> pendingRayCasts = new Queue<RayCastTask>();
public static Entity ViewTarget { get; set; } public static Entity ViewTarget { get; set; }
private float currLightMapScale; private float currLightMapScale;
@@ -58,6 +62,8 @@ namespace Barotrauma.Lights
private readonly List<LightSource> lights; private readonly List<LightSource> lights;
public bool DebugLos;
public bool LosEnabled = true; public bool LosEnabled = true;
public float LosAlpha = 1f; public float LosAlpha = 1f;
public LosMode LosMode = LosMode.Transparent; public LosMode LosMode = LosMode.Transparent;
@@ -68,6 +74,8 @@ namespace Barotrauma.Lights
private readonly Texture2D visionCircle; private readonly Texture2D visionCircle;
private readonly Texture2D gapGlowTexture;
private Vector2 losOffset; private Vector2 losOffset;
private int recalculationCount; private int recalculationCount;
@@ -85,8 +93,16 @@ namespace Barotrauma.Lights
AmbientLight = new Color(20, 20, 20, 255); AmbientLight = new Color(20, 20, 20, 255);
rayCastThread = new Thread(UpdateRayCasts)
{
Name = "LightManager Raycast thread",
IsBackground = true //this should kill the thread if the game crashes
};
rayCastThread.Start();
visionCircle = Sprite.LoadTexture("Content/Lights/visioncircle.png"); visionCircle = Sprite.LoadTexture("Content/Lights/visioncircle.png");
highlightRaster = Sprite.LoadTexture("Content/UI/HighlightRaster.png"); highlightRaster = Sprite.LoadTexture("Content/UI/HighlightRaster.png");
gapGlowTexture = Sprite.LoadTexture("Content/Lights/pointlight_rays.png");
GameMain.Instance.ResolutionChanged += () => GameMain.Instance.ResolutionChanged += () =>
{ {
@@ -100,15 +116,12 @@ namespace Barotrauma.Lights
LosEffect = EffectLoader.Load("Effects/losshader"); LosEffect = EffectLoader.Load("Effects/losshader");
SolidColorEffect = EffectLoader.Load("Effects/solidcolor"); SolidColorEffect = EffectLoader.Load("Effects/solidcolor");
if (lightEffect == null) lightEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice)
{
lightEffect = new BasicEffect(GameMain.Instance.GraphicsDevice)
{ {
VertexColorEnabled = true, VertexColorEnabled = true,
TextureEnabled = true, TextureEnabled = true,
Texture = LightSource.LightTexture Texture = LightSource.LightTexture
}; };
}
}); });
} }
@@ -176,6 +189,51 @@ namespace Barotrauma.Lights
} }
} }
private sealed class RayCastTask
{
public LightSource LightSource;
public Vector2 DrawPos;
public float Rotation;
public RayCastTask(LightSource lightSource, Vector2 drawPos, float rotation)
{
LightSource = lightSource;
DrawPos = drawPos;
Rotation = rotation;
}
public void Calculate()
{
LightSource.RayCastTask(DrawPos, Rotation);
}
}
private static readonly object mutex = new object();
public void AddRayCastTask(LightSource lightSource, Vector2 drawPos, float rotation)
{
lock (mutex)
{
if (pendingRayCasts.Any(p => p.LightSource == lightSource)) { return; }
pendingRayCasts.Enqueue(new RayCastTask(lightSource, drawPos, rotation));
}
}
private void UpdateRayCasts()
{
while (true)
{
lock (mutex)
{
while (pendingRayCasts.Count > 0)
{
pendingRayCasts.Dequeue().Calculate();
}
}
Thread.Sleep(10);
}
}
public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null) public void RenderLightMap(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, RenderTarget2D backgroundObstructor = null)
{ {
if (!LightingEnabled) { return; } if (!LightingEnabled) { return; }
@@ -288,8 +346,8 @@ namespace Barotrauma.Lights
foreach (LightSource light in activeLights) foreach (LightSource light in activeLights)
{ {
if (!light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; } if (!light.IsBackground || light.CurrentBrightness <= 0.0f) { continue; }
light.DrawSprite(spriteBatch, cam);
light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount); light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount);
light.DrawSprite(spriteBatch, cam);
} }
GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive); GameMain.ParticleManager.Draw(spriteBatch, true, null, Particles.ParticleBlendState.Additive);
spriteBatch.End(); spriteBatch.End();
@@ -308,15 +366,46 @@ namespace Barotrauma.Lights
} }
spriteBatch.End(); spriteBatch.End();
SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidColor"]; spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive, transformMatrix: spriteBatchTransform);
SolidColorEffect.Parameters["color"].SetValue(AmbientLight.Opaque().ToVector4()); Vector3 glowColorHSV = ToolBox.RGBToHSV(AmbientLight);
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform, effect: SolidColorEffect); glowColorHSV.Z = Math.Max(glowColorHSV.Z, 0.4f);
Submarine.DrawDamageable(spriteBatch, null); Color glowColor = ToolBox.HSVToRGB(glowColorHSV.X, glowColorHSV.Y, glowColorHSV.Z);
Vector2 glowSpriteSize = new Vector2(gapGlowTexture.Width, gapGlowTexture.Height);
foreach (var gap in Gap.GapList)
{
if (gap.IsRoomToRoom || gap.Open <= 0.0f || gap.ConnectedWall == null) { continue; }
float a = MathHelper.Lerp(0.5f, 1.0f,
PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.05f, gap.GlowEffectT));
float scale = MathHelper.Lerp(0.5f, 2.0f,
PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.01f, gap.GlowEffectT));
float rot = PerlinNoise.GetPerlin((float)Timing.TotalTime * 0.001f, gap.GlowEffectT) * MathHelper.TwoPi;
Vector2 spriteScale = new Vector2(gap.Rect.Width, gap.Rect.Height) / glowSpriteSize;
Vector2 drawPos = new Vector2(gap.DrawPosition.X, -gap.DrawPosition.Y);
spriteBatch.Draw(gapGlowTexture,
drawPos,
null,
glowColor * a,
rot,
glowSpriteSize / 2,
scale: Math.Max(spriteScale.X, spriteScale.Y) * scale,
SpriteEffects.None,
layerDepth: 0);
}
spriteBatch.End();
GameMain.GameScreen.DamageEffect.CurrentTechnique = GameMain.GameScreen.DamageEffect.Techniques["StencilShaderSolidColor"];
GameMain.GameScreen.DamageEffect.Parameters["solidColor"].SetValue(Color.Black.ToVector4());
spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.NonPremultiplied, SamplerState.LinearWrap, transformMatrix: spriteBatchTransform, effect: GameMain.GameScreen.DamageEffect);
Submarine.DrawDamageable(spriteBatch, GameMain.GameScreen.DamageEffect);
spriteBatch.End(); spriteBatch.End();
graphics.BlendState = BlendState.Additive; graphics.BlendState = BlendState.Additive;
//draw the focused item and character to highlight them, //draw the focused item and character to highlight them,
//and light sprites (done before drawing the actual light volumes so we can make characters obstruct the highlights and sprites) //and light sprites (done before drawing the actual light volumes so we can make characters obstruct the highlights and sprites)
//--------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------
@@ -389,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);
@@ -566,7 +666,7 @@ namespace Barotrauma.Lights
public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, Vector2 lookAtPosition) public void UpdateObstructVision(GraphicsDevice graphics, SpriteBatch spriteBatch, Camera cam, Vector2 lookAtPosition)
{ {
if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) return; if ((!LosEnabled || LosMode == LosMode.None) && !ObstructVision) { return; }
if (ViewTarget == null) return; if (ViewTarget == null) return;
graphics.SetRenderTarget(LosTexture); graphics.SetRenderTarget(LosTexture);
@@ -598,23 +698,52 @@ 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;
bool centeredOnHead = false;
if (ViewTarget is Character character &&
character.AnimController?.GetLimb(LimbType.Head) is Limb head &&
!head.IsSevered && !head.Removed)
{
pos = head.body.DrawPosition;
centeredOnHead = true;
}
Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height); 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>();
List<VertexPositionTexture> penumbraVerts = new List<VertexPositionTexture>(); List<VertexPositionTexture> penumbraVerts = new List<VertexPositionTexture>();
foreach (ConvexHull convexHull in convexHulls) foreach (ConvexHull convexHull in convexHulls)
{ {
if (!convexHull.Enabled || !convexHull.Intersects(camView)) continue; if (!convexHull.Enabled || !convexHull.Intersects(camView)) { 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; }
convexHull.CalculateLosVertices(relativeLightPos); convexHull.CalculateLosVertices(relativeLightPos);
@@ -647,6 +776,21 @@ namespace Barotrauma.Lights
graphics.SetRenderTarget(null); graphics.SetRenderTarget(null);
} }
public void DebugDrawLos(SpriteBatch spriteBatch, Camera cam)
{
Vector2 pos = ViewTarget?.Position ?? cam.Position;
spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform);
var convexHulls = ConvexHull.GetHullsInRange(pos, cam.WorldView.Width * 0.75f, ViewTarget?.Submarine);
Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height);
foreach (ConvexHull convexHull in convexHulls)
{
if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; }
if (convexHull.ParentEntity is Structure { CastShadow: false }) { continue; }
convexHull.DebugDraw(spriteBatch);
}
spriteBatch.End();
}
public void ClearLights() public void ClearLights()
{ {
lights.Clear(); lights.Clear();
@@ -15,6 +15,7 @@ namespace Barotrauma.Lights
public bool Persistent; public bool Persistent;
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; } = new Dictionary<Identifier, SerializableProperty>(); public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; private set; } = new Dictionary<Identifier, SerializableProperty>();
[Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable] [Serialize("1.0,1.0,1.0,1.0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), Editable]
@@ -228,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; }
@@ -241,12 +243,30 @@ 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; }
private enum LightVertexState
{
UpToDate,
PendingRayCasts,
PendingVertexRecalculation,
}
private LightVertexState state;
private Vector2 calculatedDrawPos;
private readonly Dictionary<Submarine, Vector2> diffToSub; private readonly Dictionary<Submarine, Vector2> diffToSub;
private DynamicVertexBuffer lightVolumeBuffer; private DynamicVertexBuffer lightVolumeBuffer;
@@ -255,7 +275,6 @@ namespace Barotrauma.Lights
private int indexCount; private int indexCount;
private Vector2 translateVertices; private Vector2 translateVertices;
private float rotateVertices;
private readonly LightSourceParams lightSourceParams; private readonly LightSourceParams lightSourceParams;
@@ -295,7 +314,6 @@ namespace Barotrauma.Lights
if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null) if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null)
{ {
rotateVertices = rotation - prevCalculatedRotation;
return; return;
} }
@@ -647,13 +665,19 @@ namespace Barotrauma.Lights
} }
} }
private static readonly List<Segment> visibleSegments = new List<Segment>(); private static readonly object mutex = new object();
private static readonly List<SegmentPoint> points = new List<SegmentPoint>();
private static readonly List<Vector2> output = new List<Vector2>(); private readonly List<Segment> visibleSegments = new List<Segment>();
private static readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4]; private readonly List<SegmentPoint> points = new List<SegmentPoint>();
private List<Vector2> FindRaycastHits() private readonly List<Vector2> verts = new List<Vector2>();
private readonly SegmentPoint[] boundaryCorners = new SegmentPoint[4];
private void FindRaycastHits()
{ {
if (!CastShadows || Range < 1.0f || Color.A < 1) { return null; } if (!CastShadows || Range < 1.0f || Color.A < 1)
{
state = LightVertexState.PendingVertexRecalculation;
return;
}
Vector2 drawPos = position; Vector2 drawPos = position;
if (ParentSub != null) { drawPos += ParentSub.DrawPosition; } if (ParentSub != null) { drawPos += ParentSub.DrawPosition; }
@@ -666,8 +690,18 @@ namespace Barotrauma.Lights
if (!chList.IsHidden.Contains(hull)) if (!chList.IsHidden.Contains(hull))
{ {
//find convexhull segments that are close enough and facing towards the light source //find convexhull segments that are close enough and facing towards the light source
hull.RefreshWorldPositions(); lock (mutex)
hull.GetVisibleSegments(drawPos, visibleSegments, ignoreEdges: false); {
hull.RefreshWorldPositions();
hull.GetVisibleSegments(drawPos, visibleSegments);
foreach (var visibleSegment in visibleSegments)
{
if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null)
{
visibleSegment.SubmarineDrawPos = visibleSegment.ConvexHull.ParentEntity.Submarine.DrawPosition;
}
}
}
} }
} }
foreach (ConvexHull hull in chList.List) foreach (ConvexHull hull in chList.List)
@@ -676,17 +710,19 @@ namespace Barotrauma.Lights
} }
} }
//add a square-shaped boundary to make sure we've got something to construct the triangles from state = LightVertexState.PendingRayCasts;
//even if there aren't enough hull segments around the light source GameMain.LightManager.AddRayCastTask(this, drawPos, rotation);
}
//(might be more effective to calculate if we actually need these extra points)
public void RayCastTask(Vector2 drawPos, float rotation)
{
Vector2 drawOffset = Vector2.Zero; Vector2 drawOffset = Vector2.Zero;
float boundsExtended = TextureRange; float boundsExtended = TextureRange;
if (OverrideLightTexture != null) if (OverrideLightTexture != null)
{ {
float cosAngle = (float)Math.Cos(Rotation); float cosAngle = (float)Math.Cos(rotation);
float sinAngle = -(float)Math.Sin(Rotation); float sinAngle = -(float)Math.Sin(rotation);
var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height); var overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
@@ -706,6 +742,10 @@ namespace Barotrauma.Lights
drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle; drawOffset.Y = origin.X * sinAngle + origin.Y * cosAngle;
} }
//add a square-shaped boundary to make sure we've got something to construct the triangles from
//even if there aren't enough hull segments around the light source
//(might be more effective to calculate if we actually need these extra points)
Vector2 boundsMin = drawPos + drawOffset + new Vector2(-boundsExtended, -boundsExtended); Vector2 boundsMin = drawPos + drawOffset + new Vector2(-boundsExtended, -boundsExtended);
Vector2 boundsMax = drawPos + drawOffset + new Vector2(boundsExtended, boundsExtended); Vector2 boundsMax = drawPos + drawOffset + new Vector2(boundsExtended, boundsExtended);
boundaryCorners[0] = new SegmentPoint(boundsMax, null); boundaryCorners[0] = new SegmentPoint(boundsMax, null);
@@ -719,197 +759,197 @@ namespace Barotrauma.Lights
visibleSegments.Add(s); visibleSegments.Add(s);
} }
//Generate new points at the intersections between segments lock (mutex)
//This is necessary for the light volume to generate properly on some subs
for (int i = 0; i < visibleSegments.Count; i++)
{ {
Vector2 p1a = visibleSegments[i].Start.WorldPos; //Generate new points at the intersections between segments
Vector2 p1b = visibleSegments[i].End.WorldPos; //This is necessary for the light volume to generate properly on some subs
for (int i = 0; i < visibleSegments.Count; i++)
for (int j = i + 1; j < visibleSegments.Count; j++)
{ {
//ignore intersections between parallel axis-aligned segments Vector2 p1a = visibleSegments[i].Start.WorldPos;
if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned && Vector2 p1b = visibleSegments[i].End.WorldPos;
visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal)
{
continue;
}
Vector2 p2a = visibleSegments[j].Start.WorldPos; for (int j = i + 1; j < visibleSegments.Count; j++)
Vector2 p2b = visibleSegments[j].End.WorldPos;
if (Vector2.DistanceSquared(p1a, p2a) < 5.0f ||
Vector2.DistanceSquared(p1a, p2b) < 5.0f ||
Vector2.DistanceSquared(p1b, p2a) < 5.0f ||
Vector2.DistanceSquared(p1b, p2b) < 5.0f)
{ {
continue; //ignore intersections between parallel axis-aligned segments
} if (visibleSegments[i].IsAxisAligned && visibleSegments[j].IsAxisAligned &&
visibleSegments[i].IsHorizontal == visibleSegments[j].IsHorizontal)
bool intersects;
Vector2 intersection = Vector2.Zero;
if (visibleSegments[i].IsAxisAligned)
{
intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection);
}
else if (visibleSegments[j].IsAxisAligned)
{
intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection);
}
else
{
intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection);
}
if (intersects)
{
SegmentPoint start = visibleSegments[i].Start;
SegmentPoint end = visibleSegments[i].End;
SegmentPoint mid = new SegmentPoint(intersection, null);
if (visibleSegments[i].ConvexHull?.ParentEntity?.Submarine != null)
{
mid.Pos -= visibleSegments[i].ConvexHull.ParentEntity.Submarine.DrawPosition;
}
if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f ||
Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f)
{ {
continue; continue;
} }
Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull) Vector2 p2a = visibleSegments[j].Start.WorldPos;
{ Vector2 p2b = visibleSegments[j].End.WorldPos;
IsHorizontal = visibleSegments[i].IsHorizontal,
};
Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull) if (Vector2.DistanceSquared(p1a, p2a) < 5.0f ||
Vector2.DistanceSquared(p1a, p2b) < 5.0f ||
Vector2.DistanceSquared(p1b, p2a) < 5.0f ||
Vector2.DistanceSquared(p1b, p2b) < 5.0f)
{ {
IsHorizontal = visibleSegments[i].IsHorizontal continue;
}; }
visibleSegments[i] = seg1; bool intersects;
visibleSegments.Insert(i + 1, seg2); Vector2 intersection = Vector2.Zero;
if (visibleSegments[i].IsAxisAligned)
{
intersects = MathUtils.GetAxisAlignedLineIntersection(p2a, p2b, p1a, p1b, visibleSegments[i].IsHorizontal, out intersection);
}
else if (visibleSegments[j].IsAxisAligned)
{
intersects = MathUtils.GetAxisAlignedLineIntersection(p1a, p1b, p2a, p2b, visibleSegments[j].IsHorizontal, out intersection);
}
else
{
intersects = MathUtils.GetLineSegmentIntersection(p1a, p1b, p2a, p2b, out intersection);
}
if (intersects)
{
SegmentPoint start = visibleSegments[i].Start;
SegmentPoint end = visibleSegments[i].End;
SegmentPoint mid = new SegmentPoint(intersection, null);
mid.Pos -= visibleSegments[i].SubmarineDrawPos;
if (Vector2.DistanceSquared(start.WorldPos, mid.WorldPos) < 5.0f ||
Vector2.DistanceSquared(end.WorldPos, mid.WorldPos) < 5.0f)
{
continue;
}
Segment seg1 = new Segment(start, mid, visibleSegments[i].ConvexHull)
{
IsHorizontal = visibleSegments[i].IsHorizontal,
};
Segment seg2 = new Segment(mid, end, visibleSegments[i].ConvexHull)
{
IsHorizontal = visibleSegments[i].IsHorizontal
};
visibleSegments[i] = seg1;
visibleSegments.Insert(i + 1, seg2);
i--;
break;
}
}
}
points.Clear();
//remove segments that fall out of bounds
for (int i = 0; i < visibleSegments.Count; i++)
{
Segment s = visibleSegments[i];
if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f ||
Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f ||
Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f ||
Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f)
{
visibleSegments.RemoveAt(i);
i--; i--;
break; }
else
{
points.Add(s.Start);
points.Add(s.End);
} }
} }
}
points.Clear(); //remove points that are very close to each other
//remove segments that fall out of bounds for (int i = 0; i < points.Count; i++)
for (int i = 0; i < visibleSegments.Count; i++)
{
Segment s = visibleSegments[i];
if (Math.Abs(s.Start.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f ||
Math.Abs(s.Start.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f ||
Math.Abs(s.End.WorldPos.X - drawPos.X - drawOffset.X) > boundsExtended + 1.0f ||
Math.Abs(s.End.WorldPos.Y - drawPos.Y - drawOffset.Y) > boundsExtended + 1.0f)
{ {
visibleSegments.RemoveAt(i); for (int j = Math.Min(i + 4, points.Count - 1); j > i; j--)
i--; {
if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 &&
Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6)
{
points.RemoveAt(j);
}
}
} }
else
var compareCCW = new CompareSegmentPointCW(drawPos);
try
{ {
points.Add(s.Start); points.Sort(compareCCW);
points.Add(s.End); }
catch (Exception e)
{
StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n");
foreach (SegmentPoint sp in points)
{
sb.AppendLine(sp.Pos.ToString());
}
DebugConsole.ThrowError(sb.ToString(), e);
}
visibleSegments.Sort((s1, s2) =>
MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos)
.CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos)));
verts.Clear();
foreach (SegmentPoint p in points)
{
Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos);
Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3;
//do two slightly offset raycasts to hit the segment itself and whatever's behind it
var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments);
if (intersection1.index < 0) { return; }
var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments);
if (intersection2.index < 0) { return; }
Segment seg1 = visibleSegments[intersection1.index];
Segment seg2 = visibleSegments[intersection2.index];
bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f;
bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f;
if (isPoint1 && isPoint2)
{
//hit at the current segmentpoint -> place the segmentpoint into the list
verts.Add(p.WorldPos);
foreach (ConvexHullList hullList in convexHullsInRange)
{
hullList.IsHidden.Remove(p.ConvexHull);
hullList.IsHidden.Remove(seg1.ConvexHull);
hullList.IsHidden.Remove(seg2.ConvexHull);
}
}
else if (intersection1.index != intersection2.index)
{
//the raycasts landed on different segments
//we definitely want to generate new geometry here
verts.Add(isPoint1 ? p.WorldPos : intersection1.pos);
verts.Add(isPoint2 ? p.WorldPos : intersection2.pos);
foreach (ConvexHullList hullList in convexHullsInRange)
{
hullList.IsHidden.Remove(p.ConvexHull);
hullList.IsHidden.Remove(seg1.ConvexHull);
hullList.IsHidden.Remove(seg2.ConvexHull);
}
}
//if neither of the conditions above are met, we just assume
//that the raycasts both resulted on the same segment
//and creating geometry here would be wasteful
} }
} }
//remove points that are very close to each other //remove points that are very close to each other
for (int i = 0; i < points.Count; i++) for (int i = 0; i < verts.Count - 1; i++)
{ {
for (int j = Math.Min(i + 4, points.Count-1); j > i; j--) for (int j = Math.Min(i + 4, verts.Count - 1); j > i; j--)
{ {
if (Math.Abs(points[i].WorldPos.X - points[j].WorldPos.X) < 6 && if (Math.Abs(verts[i].X - verts[j].X) < 6 &&
Math.Abs(points[i].WorldPos.Y - points[j].WorldPos.Y) < 6) Math.Abs(verts[i].Y - verts[j].Y) < 6)
{ {
points.RemoveAt(j); verts.RemoveAt(j);
} }
} }
} }
calculatedDrawPos = drawPos;
var compareCCW = new CompareSegmentPointCW(drawPos); state = LightVertexState.PendingVertexRecalculation;
try
{
points.Sort(compareCCW);
}
catch (Exception e)
{
StringBuilder sb = new StringBuilder("Constructing light volumes failed! Light pos: " + drawPos + ", Hull verts:\n");
foreach (SegmentPoint sp in points)
{
sb.AppendLine(sp.Pos.ToString());
}
DebugConsole.ThrowError(sb.ToString(), e);
}
visibleSegments.Sort((s1, s2) =>
MathUtils.LineToPointDistanceSquared(s1.Start.WorldPos, s1.End.WorldPos, drawPos)
.CompareTo(MathUtils.LineToPointDistanceSquared(s2.Start.WorldPos, s2.End.WorldPos, drawPos)));
output.Clear();
foreach (SegmentPoint p in points)
{
Vector2 dir = Vector2.Normalize(p.WorldPos - drawPos);
Vector2 dirNormal = new Vector2(-dir.Y, dir.X) * 3;
//do two slightly offset raycasts to hit the segment itself and whatever's behind it
var intersection1 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 - dirNormal, visibleSegments);
if (intersection1.index < 0) { return null; }
var intersection2 = RayCast(drawPos, drawPos + dir * boundsExtended * 2 + dirNormal, visibleSegments);
if (intersection2.index < 0) { return null; }
Segment seg1 = visibleSegments[intersection1.index];
Segment seg2 = visibleSegments[intersection2.index];
bool isPoint1 = MathUtils.LineToPointDistanceSquared(seg1.Start.WorldPos, seg1.End.WorldPos, p.WorldPos) < 25.0f;
bool isPoint2 = MathUtils.LineToPointDistanceSquared(seg2.Start.WorldPos, seg2.End.WorldPos, p.WorldPos) < 25.0f;
if (isPoint1 && isPoint2)
{
//hit at the current segmentpoint -> place the segmentpoint into the list
output.Add(p.WorldPos);
foreach (ConvexHullList hullList in convexHullsInRange)
{
hullList.IsHidden.Remove(p.ConvexHull);
hullList.IsHidden.Remove(seg1.ConvexHull);
hullList.IsHidden.Remove(seg2.ConvexHull);
}
}
else if (intersection1.index != intersection2.index)
{
//the raycasts landed on different segments
//we definitely want to generate new geometry here
output.Add(isPoint1 ? p.WorldPos : intersection1.pos);
output.Add(isPoint2 ? p.WorldPos : intersection2.pos);
foreach (ConvexHullList hullList in convexHullsInRange)
{
hullList.IsHidden.Remove(p.ConvexHull);
hullList.IsHidden.Remove(seg1.ConvexHull);
hullList.IsHidden.Remove(seg2.ConvexHull);
}
}
//if neither of the conditions above are met, we just assume
//that the raycasts both resulted on the same segment
//and creating geometry here would be wasteful
}
//remove points that are very close to each other
for (int i = 0; i < output.Count - 1; i++)
{
for (int j = Math.Min(i + 4, output.Count - 1); j > i; j--)
{
if (Math.Abs(output[i].X - output[j].X) < 6 &&
Math.Abs(output[i].Y - output[j].Y) < 6)
{
output.RemoveAt(j);
}
}
}
return output;
} }
private static (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List<Segment> segments) private static (int index, Vector2 pos) RayCast(Vector2 rayStart, Vector2 rayEnd, List<Segment> segments)
@@ -954,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)
@@ -987,8 +1027,7 @@ namespace Barotrauma.Lights
indices = new short[indexCount]; indices = new short[indexCount];
} }
Vector2 drawPos = position; Vector2 drawPos = calculatedDrawPos;
if (ParentSub != null) { drawPos += ParentSub.DrawPosition; }
float cosAngle = (float)Math.Cos(Rotation); float cosAngle = (float)Math.Cos(Rotation);
float sinAngle = -(float)Math.Sin(Rotation); float sinAngle = -(float)Math.Sin(Rotation);
@@ -1042,7 +1081,7 @@ namespace Barotrauma.Lights
//calculate normal of first segment //calculate normal of first segment
Vector2 nDiff1 = vertex - nextVertex; Vector2 nDiff1 = vertex - nextVertex;
float tx = nDiff1.X; nDiff1.X = -nDiff1.Y; nDiff1.Y = tx; nDiff1 = new Vector2(-nDiff1.Y, nDiff1.X);
nDiff1 /= Math.Max(Math.Abs(nDiff1.X), Math.Abs(nDiff1.Y)); nDiff1 /= Math.Max(Math.Abs(nDiff1.X), Math.Abs(nDiff1.Y));
//if the normal is pointing towards the light origin //if the normal is pointing towards the light origin
//rather than away from it, invert it //rather than away from it, invert it
@@ -1050,21 +1089,23 @@ namespace Barotrauma.Lights
//calculate normal of second segment //calculate normal of second segment
Vector2 nDiff2 = prevVertex - vertex; Vector2 nDiff2 = prevVertex - vertex;
tx = nDiff2.X; nDiff2.X = -nDiff2.Y; nDiff2.Y = tx; nDiff2 = new Vector2(-nDiff2.Y, nDiff2.X);
nDiff2 /= Math.Max(Math.Abs(nDiff2.X),Math.Abs(nDiff2.Y)); nDiff2 /= Math.Max(Math.Abs(nDiff2.X), Math.Abs(nDiff2.Y));
//if the normal is pointing towards the light origin //if the normal is pointing towards the light origin
//rather than away from it, invert it //rather than away from it, invert it
if (Vector2.DistanceSquared(nDiff2, rawDiff) > Vector2.DistanceSquared(-nDiff2, rawDiff)) nDiff2 = -nDiff2; if (Vector2.DistanceSquared(nDiff2, rawDiff) > Vector2.DistanceSquared(-nDiff2, rawDiff)) nDiff2 = -nDiff2;
//add the normals together and use some magic numbers to create //add the normals together and use some magic numbers to create
//a somewhat useful/good-looking blur //a somewhat useful/good-looking blur
Vector2 nDiff = nDiff1 * 40.0f; float blurDistance = 40.0f;
if (MathUtils.GetLineIntersection(vertex + (nDiff1 * 40.0f), nextVertex + (nDiff1 * 40.0f), vertex + (nDiff2 * 40.0f), prevVertex + (nDiff2 * 40.0f), true, out Vector2 intersection)) Vector2 nDiff = nDiff1 * blurDistance;
if (MathUtils.GetLineIntersection(vertex + (nDiff1 * blurDistance), nextVertex + (nDiff1 * blurDistance), vertex + (nDiff2 * blurDistance), prevVertex + (nDiff2 * blurDistance), true, out Vector2 intersection))
{ {
nDiff = intersection - vertex; nDiff = intersection - vertex;
if (nDiff.LengthSquared() > 10000.0f) if (nDiff.LengthSquared() > 100.0f * 100.0f)
{ {
nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y)); nDiff *= 100.0f; nDiff /= Math.Max(Math.Abs(nDiff.X), Math.Abs(nDiff.Y));
nDiff *= 100.0f;
} }
} }
@@ -1162,7 +1203,6 @@ namespace Barotrauma.Lights
} }
translateVertices = Vector2.Zero; translateVertices = Vector2.Zero;
rotateVertices = 0.0f;
prevCalculatedPosition = position; prevCalculatedPosition = position;
prevCalculatedRotation = rotation; prevCalculatedRotation = rotation;
} }
@@ -1340,31 +1380,41 @@ namespace Barotrauma.Lights
if (NeedsRecalculation && allowRecalculation) if (NeedsRecalculation && allowRecalculation)
{ {
recalculationCount++; if (state == LightVertexState.UpToDate)
var verts = FindRaycastHits();
if (verts == null)
{ {
#if DEBUG recalculationCount++;
DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}"); FindRaycastHits();
#endif
Enabled = false;
return;
} }
else if (state == LightVertexState.PendingVertexRecalculation)
{
if (verts == null)
{
#if DEBUG
DebugConsole.ThrowError($"Failed to generate vertices for a light source. Range: {Range}, color: {Color}, brightness: {CurrentBrightness}, parent: {ParentBody?.UserData ?? "Unknown"}");
#endif
Enabled = false;
return;
}
CalculateLightVertices(verts); CalculateLightVertices(verts);
LastRecalculationTime = (float)Timing.TotalTime; LastRecalculationTime = (float)Timing.TotalTime;
NeedsRecalculation = false; NeedsRecalculation = needsRecalculationWhenUpToDate;
needsRecalculationWhenUpToDate = false;
state = LightVertexState.UpToDate;
}
} }
if (vertexCount == 0) { return; }
Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition; Vector2 offset = ParentSub == null ? Vector2.Zero : ParentSub.DrawPosition;
lightEffect.World = lightEffect.World =
Matrix.CreateTranslation(-new Vector3(position, 0.0f)) * Matrix.CreateTranslation(-new Vector3(position, 0.0f)) *
Matrix.CreateRotationZ(rotateVertices - MathHelper.ToRadians(LightSourceParams.Rotation)) * Matrix.CreateRotationZ(MathHelper.ToRadians(LightSourceParams.Rotation)) *
Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) * Matrix.CreateTranslation(new Vector3(position + offset + translateVertices, 0.0f)) *
transform; transform;
if (vertexCount == 0) { return; }
lightEffect.DiffuseColor = (new Vector3(Color.R, Color.G, Color.B) * (Color.A / 255.0f * CurrentBrightness)) / 255.0f; lightEffect.DiffuseColor = (new Vector3(Color.R, Color.G, Color.B) * (Color.A / 255.0f * CurrentBrightness)) / 255.0f;
if (OverrideLightTexture != null) if (OverrideLightTexture != null)
@@ -738,8 +738,8 @@ namespace Barotrauma
spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect); spriteBatch.GraphicsDevice.ScissorRectangle = Rectangle.Intersect(prevScissorRect, rect);
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
Vector2 topLeft = rectCenter + viewOffset; Vector2 topLeft = rectCenter + viewOffset - rect.Location.ToVector2();
Vector2 bottomRight = rectCenter + (viewOffset + new Vector2(Width, Height)); Vector2 bottomRight = topLeft + new Vector2(Width, Height);
Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale; Vector2 mapTileSize = mapTiles[0, 0].size * generationParams.MapTileScale;
int startX = (int)Math.Floor(-topLeft.X / mapTileSize.X) - 1; int startX = (int)Math.Floor(-topLeft.X / mapTileSize.X) - 1;
@@ -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();
} }
} }
} }
@@ -55,21 +55,11 @@ namespace Barotrauma
{ {
if (!CastShadow) { return; } if (!CastShadow) { return; }
if (convexHulls == null) convexHulls ??= new List<ConvexHull>();
{ var h = new ConvexHull(
convexHulls = new List<ConvexHull>(); new Rectangle((position - size / 2).ToPoint(), size.ToPoint()),
} IsHorizontal,
this);
Vector2 halfSize = size / 2;
Vector2[] verts = new Vector2[]
{
position + new Vector2(-halfSize.X, halfSize.Y),
position + new Vector2(halfSize.X, halfSize.Y),
position + new Vector2(halfSize.X, -halfSize.Y),
position + new Vector2(-halfSize.X, -halfSize.Y),
};
var h = new ConvexHull(verts, Color.Black, this);
if (Math.Abs(rotation) > 0.001f) if (Math.Abs(rotation) > 0.001f)
{ {
h.Rotate(position, rotation); h.Rotate(position, rotation);
@@ -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)
{ {
@@ -3077,8 +3077,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;
} }
@@ -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;
@@ -49,18 +50,22 @@ 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; }
@@ -15,7 +15,7 @@ namespace Barotrauma
private RenderTarget2D renderTargetWater; private RenderTarget2D renderTargetWater;
private RenderTarget2D renderTargetFinal; private RenderTarget2D renderTargetFinal;
private readonly Effect damageEffect; public readonly Effect DamageEffect;
private readonly Texture2D damageStencil; private readonly Texture2D damageStencil;
private readonly Texture2D distortTexture; private readonly Texture2D distortTexture;
@@ -39,7 +39,7 @@ namespace Barotrauma
}; };
//var blurEffect = LoadEffect("Effects/blurshader"); //var blurEffect = LoadEffect("Effects/blurshader");
damageEffect = EffectLoader.Load("Effects/damageshader"); DamageEffect = EffectLoader.Load("Effects/damageshader");
PostProcessEffect = EffectLoader.Load("Effects/postprocess"); PostProcessEffect = EffectLoader.Load("Effects/postprocess");
GradientEffect = EffectLoader.Load("Effects/gradientshader"); GradientEffect = EffectLoader.Load("Effects/gradientshader");
GrainEffect = EffectLoader.Load("Effects/grainshader"); GrainEffect = EffectLoader.Load("Effects/grainshader");
@@ -47,9 +47,9 @@ namespace Barotrauma
BlueprintEffect = EffectLoader.Load("Effects/blueprintshader"); BlueprintEffect = EffectLoader.Load("Effects/blueprintshader");
damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png"); damageStencil = TextureLoader.FromFile("Content/Map/walldamage.png");
damageEffect.Parameters["xStencil"].SetValue(damageStencil); DamageEffect.Parameters["xStencil"].SetValue(damageStencil);
damageEffect.Parameters["aMultiplier"].SetValue(50.0f); DamageEffect.Parameters["aMultiplier"].SetValue(50.0f);
damageEffect.Parameters["cMultiplier"].SetValue(200.0f); DamageEffect.Parameters["cMultiplier"].SetValue(200.0f);
distortTexture = TextureLoader.FromFile("Content/Effects/distortnormals.png"); distortTexture = TextureLoader.FromFile("Content/Effects/distortnormals.png");
PostProcessEffect.Parameters["xDistortTexture"].SetValue(distortTexture); PostProcessEffect.Parameters["xDistortTexture"].SetValue(distortTexture);
@@ -346,12 +346,13 @@ namespace Barotrauma
GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontParticles", sw.ElapsedTicks); GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontParticles", sw.ElapsedTicks);
sw.Restart(); sw.Restart();
DamageEffect.CurrentTechnique = DamageEffect.Techniques["StencilShader"];
spriteBatch.Begin(SpriteSortMode.Immediate, spriteBatch.Begin(SpriteSortMode.Immediate,
BlendState.NonPremultiplied, SamplerState.LinearWrap, BlendState.NonPremultiplied, SamplerState.LinearWrap,
null, null, null, null,
damageEffect, DamageEffect,
cam.Transform); cam.Transform);
Submarine.DrawDamageable(spriteBatch, damageEffect, false); Submarine.DrawDamageable(spriteBatch, DamageEffect, false);
spriteBatch.End(); spriteBatch.End();
sw.Stop(); sw.Stop();
@@ -378,7 +379,7 @@ namespace Barotrauma
{ {
graphics.DepthStencilState = DepthStencilState.None; graphics.DepthStencilState = DepthStencilState.None;
graphics.SamplerStates[0] = SamplerState.LinearWrap; graphics.SamplerStates[0] = SamplerState.LinearWrap;
graphics.BlendState = Lights.CustomBlendStates.Multiplicative; graphics.BlendState = CustomBlendStates.Multiplicative;
Quad.UseBasicEffect(GameMain.LightManager.LightMap); Quad.UseBasicEffect(GameMain.LightManager.LightMap);
Quad.Render(); Quad.Render();
} }
@@ -409,6 +410,7 @@ namespace Barotrauma
{ {
GameMain.LightManager.LosEffect.CurrentTechnique = GameMain.LightManager.LosEffect.Techniques["LosShader"]; GameMain.LightManager.LosEffect.CurrentTechnique = GameMain.LightManager.LosEffect.Techniques["LosShader"];
GameMain.LightManager.LosEffect.Parameters["blurDistance"].SetValue(0.005f);
GameMain.LightManager.LosEffect.Parameters["xTexture"].SetValue(renderTargetBackground); GameMain.LightManager.LosEffect.Parameters["xTexture"].SetValue(renderTargetBackground);
GameMain.LightManager.LosEffect.Parameters["xLosTexture"].SetValue(GameMain.LightManager.LosTexture); GameMain.LightManager.LosEffect.Parameters["xLosTexture"].SetValue(GameMain.LightManager.LosTexture);
GameMain.LightManager.LosEffect.Parameters["xLosAlpha"].SetValue(GameMain.LightManager.LosAlpha); GameMain.LightManager.LosEffect.Parameters["xLosAlpha"].SetValue(GameMain.LightManager.LosAlpha);
@@ -434,8 +436,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)
@@ -519,6 +524,11 @@ namespace Barotrauma
spriteBatch.End(); spriteBatch.End();
} }
if (GameMain.LightManager.DebugLos)
{
GameMain.LightManager.DebugDrawLos(spriteBatch, cam);
}
sw.Stop(); sw.Stop();
GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:PostProcess", sw.ElapsedTicks); GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:PostProcess", sw.ElapsedTicks);
sw.Restart(); sw.Restart();
@@ -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;
@@ -82,6 +81,7 @@ namespace Barotrauma
{ {
GameMain.Instance.ResolutionChanged += () => GameMain.Instance.ResolutionChanged += () =>
{ {
SetMenuTabPositioning();
CreateHostServerFields(); CreateHostServerFields();
CreateCampaignSetupUI(); CreateCampaignSetupUI();
SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform); SettingsMenu.Create(menuTabs[Tab.Settings].RectTransform);
@@ -444,31 +444,33 @@ namespace Barotrauma
var relativeSize = new Vector2(0.6f, 0.65f); var relativeSize = new Vector2(0.6f, 0.65f);
var minSize = new Point(600, 400); var minSize = new Point(600, 400);
var maxSize = new Point(2000, 1500); var maxSize = new Point(2000, 1500);
var anchor = Anchor.CenterRight; var anchor = Anchor.Center;
var pivot = Pivot.CenterRight; var pivot = Pivot.Center;
Vector2 relativeSpacing = new Vector2(0.05f, 0.0f); Vector2 relativeOffset = new Vector2(0.05f, 0.0f);
menuTabs = new Dictionary<Tab, GUIFrame>(); menuTabs = new Dictionary<Tab, GUIFrame>
{
menuTabs[Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }, [Tab.Settings] = new GUIFrame(new RectTransform(new Vector2(relativeSize.X, 0.8f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset },
style: null); style: null)
menuTabs[Tab.Settings].CanBeFocused = false; {
CanBeFocused = false
menuTabs[Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); },
menuTabs[Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); [Tab.NewGame] = new GUIFrame(new RectTransform(relativeSize * new Vector2(1.0f, 1.15f), GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset }),
[Tab.LoadGame] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset })
};
CreateCampaignSetupUI(); CreateCampaignSetupUI();
var hostServerScale = new Vector2(0.7f, 1.2f); var hostServerScale = new Vector2(0.7f, 1.2f);
menuTabs[Tab.HostServer] = new GUIFrame(new RectTransform( menuTabs[Tab.HostServer] = new GUIFrame(new RectTransform(
Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale)) Vector2.Multiply(relativeSize, hostServerScale), GUI.Canvas, anchor, pivot, minSize.Multiply(hostServerScale), maxSize.Multiply(hostServerScale))
{ RelativeOffset = relativeSpacing }); { RelativeOffset = relativeOffset });
CreateHostServerFields(); CreateHostServerFields();
//---------------------------------------------------------------------- //----------------------------------------------------------------------
menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing }); menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeOffset });
CreateTutorialTab(); CreateTutorialTab();
this.game = game; this.game = game;
@@ -489,6 +491,20 @@ namespace Barotrauma
SelectTab(Tab.Empty); SelectTab(Tab.Empty);
return true; return true;
}; };
SetMenuTabPositioning();
}
private void SetMenuTabPositioning()
{
foreach (GUIFrame menuTab in menuTabs.Values)
{
var anchor = GUI.IsUltrawide ? Anchor.Center : Anchor.CenterRight;
var pivot = GUI.IsUltrawide ? Pivot.Center : Pivot.CenterRight;
Vector2 relativeOffset = GUI.IsUltrawide ? Vector2.Zero : new Vector2(0.05f, 0.0f);
menuTab.RectTransform.SetPosition(anchor, pivot);
menuTab.RectTransform.RelativeOffset = relativeOffset;
}
} }
private void CreateTutorialTab() private void CreateTutorialTab()
@@ -1532,10 +1548,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();
} }
@@ -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;
}; };
@@ -2850,7 +2855,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();
@@ -5794,7 +5799,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>();
@@ -5907,6 +5915,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);
@@ -347,8 +347,7 @@ namespace Barotrauma
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())
{ {
@@ -189,6 +189,7 @@ namespace Barotrauma.Steam
ModProject modProject = new ModProject(tempPkg) ModProject modProject = new ModProject(tempPkg)
{ {
ModVersion = modVersion, ModVersion = modVersion,
Name = title,
ExpectedHash = tempPkg.CalculateHash(name: title, modVersion: modVersion) 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)
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.0.13.2</Version> <Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.0.13.2</Version> <Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -5,6 +5,8 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = <xTexture>; }
Texture2D xStencil; Texture2D xStencil;
sampler StencilSampler = sampler_state { Texture = <xStencil>; }; sampler StencilSampler = sampler_state { Texture = <xStencil>; };
float4 solidColor;
float4 inColor; float4 inColor;
float aCutoff; float aCutoff;
@@ -16,7 +18,6 @@ float cMultiplier;
float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{ {
float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor; float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor;
float4 stencilColor = xStencil.Sample(StencilSampler, texCoord); float4 stencilColor = xStencil.Sample(StencilSampler, texCoord);
float aDiff = stencilColor.a - aCutoff; float aDiff = stencilColor.a - aCutoff;
@@ -30,6 +31,18 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord
min(aDiff * aMultiplier, c.a)); min(aDiff * aMultiplier, c.a));
} }
float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{
float4 c = xTexture.Sample(TextureSampler, texCoord) * inColor;
float4 stencilColor = xStencil.Sample(StencilSampler, texCoord);
float aDiff = stencilColor.a - aCutoff;
clip(aDiff);
return float4(solidColor.rgb, solidColor.a * min(aDiff * aMultiplier, c.a));
}
technique StencilShader technique StencilShader
{ {
pass Pass1 pass Pass1
@@ -37,3 +50,11 @@ technique StencilShader
PixelShader = compile ps_4_0_level_9_1 main(); PixelShader = compile ps_4_0_level_9_1 main();
} }
} }
technique StencilShaderSolidColor
{
pass Pass1
{
PixelShader = compile ps_4_0_level_9_1 solidColorStencil();
}
}
@@ -5,6 +5,8 @@ sampler TextureSampler : register (s0) = sampler_state { Texture = <xTexture>; }
Texture xStencil; Texture xStencil;
sampler StencilSampler = sampler_state { Texture = <xStencil>; }; sampler StencilSampler = sampler_state { Texture = <xStencil>; };
float4 solidColor;
float4 inColor; float4 inColor;
float aCutoff; float aCutoff;
@@ -16,7 +18,6 @@ float cMultiplier;
float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0 float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{ {
float4 c = tex2D(TextureSampler, texCoord) * inColor; float4 c = tex2D(TextureSampler, texCoord) * inColor;
float4 stencilColor = tex2D(StencilSampler, texCoord); float4 stencilColor = tex2D(StencilSampler, texCoord);
float aDiff = stencilColor.a - aCutoff; float aDiff = stencilColor.a - aCutoff;
@@ -30,6 +31,18 @@ float4 main(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord
min(aDiff * aMultiplier, c.a)); min(aDiff * aMultiplier, c.a));
} }
float4 solidColorStencil(float4 position : POSITION0, float4 color : COLOR0, float2 texCoord : TEXCOORD0) : COLOR0
{
float4 c = tex2D(TextureSampler, texCoord) * inColor;
float4 stencilColor = tex2D(StencilSampler, texCoord);
float aDiff = stencilColor.a - aCutoff;
clip(aDiff);
return float4(solidColor.rgb, solidColor.a * min(aDiff * aMultiplier, c.a));
}
technique StencilShader technique StencilShader
{ {
pass Pass1 pass Pass1
@@ -37,3 +50,11 @@ technique StencilShader
PixelShader = compile ps_2_0 main(); PixelShader = compile ps_2_0 main();
} }
} }
technique StencilShaderSolidColor
{
pass Pass1
{
PixelShader = compile ps_2_0 solidColorStencil();
}
}
@@ -30,10 +30,17 @@ float xLosAlpha;
float4 xColor; float4 xColor;
float blurDistance;
float4 mainPS(VertexShaderOutput input) : COLOR0 float4 mainPS(VertexShaderOutput input) : COLOR0
{ {
float4 sampleColor = xTexture.Sample(TextureSampler, input.TexCoords); float4 sampleColor = xTexture.Sample(TextureSampler, input.TexCoords);
float4 losColor = xLosTexture.Sample(LosSampler, input.TexCoords);
float4 losColor = xLosTexture.Sample(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y + blurDistance));
losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x - blurDistance, input.TexCoords.y - blurDistance));
losColor += xLosTexture.Sample(LosSampler, float2(input.TexCoords.x + blurDistance, input.TexCoords.y - blurDistance));
losColor += xLosTexture.Sample(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;
@@ -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,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>1.0.13.2</Version> <Version>1.0.20.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright> <Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -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.0.13.2</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.0.13.2</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)
@@ -1,4 +1,5 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using System.Linq;
namespace Barotrauma namespace Barotrauma
{ {
@@ -16,11 +17,10 @@ namespace Barotrauma
foreach (var kvp in spawnedResources) foreach (var kvp in spawnedResources)
{ {
msg.WriteByte((byte)kvp.Value.Count); msg.WriteByte((byte)kvp.Value.Count);
var rotation = resourceClusters[kvp.Key].Rotation; msg.WriteSingle(kvp.Value.FirstOrDefault()?.Rotation ?? 0.0f);
msg.WriteSingle(rotation); foreach (var item in kvp.Value)
foreach (var r in kvp.Value)
{ {
r.WriteSpawnData(msg, r.ID, Entity.NullEntityID, 0, -1); item.WriteSpawnData(msg, item.ID, Entity.NullEntityID, 0, -1);
} }
} }
@@ -28,9 +28,9 @@ namespace Barotrauma
{ {
msg.WriteIdentifier(kvp.Key); msg.WriteIdentifier(kvp.Key);
msg.WriteByte((byte)kvp.Value.Length); msg.WriteByte((byte)kvp.Value.Length);
foreach (var i in kvp.Value) foreach (var item in kvp.Value)
{ {
msg.WriteUInt16(i.ID); msg.WriteUInt16(item.ID);
} }
} }
} }
@@ -6,7 +6,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.Steam;
namespace Barotrauma namespace Barotrauma
{ {
@@ -1310,6 +1309,10 @@ namespace Barotrauma
public override bool TryPurchase(Client client, int price) public override bool TryPurchase(Client client, int price)
{ {
//disconnected clients can never purchase anything
//(can happen e.g. if someone starts a vote to buy something and then disconnects)
if (client != null && !GameMain.Server.ConnectedClients.Contains(client)) { return false; }
Wallet wallet = GetWallet(client); Wallet wallet = GetWallet(client);
if (!AllowedToManageWallets(client)) if (!AllowedToManageWallets(client))
{ {
@@ -1359,6 +1362,12 @@ namespace Barotrauma
modeElement.Add(Settings.Save()); modeElement.Add(Settings.Save());
modeElement.Add(SaveStats()); modeElement.Add(SaveStats());
modeElement.Add(Bank.Save()); modeElement.Add(Bank.Save());
if (GameMain.GameSession?.EventManager != null)
{
modeElement.Add(GameMain.GameSession?.EventManager.Save());
}
CampaignMetadata?.Save(modeElement); CampaignMetadata?.Save(modeElement);
Map.Save(modeElement); Map.Save(modeElement);
CargoManager?.SavePurchasedItems(modeElement); CargoManager?.SavePurchasedItems(modeElement);
@@ -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;
} }
@@ -185,7 +185,7 @@ namespace Barotrauma.Items.Components
//already connected, no need to do anything //already connected, no need to do anything
if (Connections[i].Wires.Contains(newWire)) { continue; } if (Connections[i].Wires.Contains(newWire)) { continue; }
newWire.Connect(Connections[i], true, true); newWire.TryConnect(Connections[i], true, true);
Connections[i].TryAddLink(newWire); Connections[i].TryAddLink(newWire);
var otherConnection = newWire.OtherConnection(Connections[i]); var otherConnection = newWire.OtherConnection(Connections[i]);
@@ -23,15 +23,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;
} }
} }
@@ -136,8 +134,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, listenIp); ServerSettings = new ServerSettings(this, name, port, queryPort, maxPlayers, isPublic, attemptUPnP, listenIp);
@@ -1680,38 +1676,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)
@@ -3138,7 +3150,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)
@@ -3193,7 +3205,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
{ {
@@ -3227,7 +3239,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
{ {
@@ -3459,33 +3471,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)
@@ -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
{ {
@@ -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.0.13.2</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
@@ -425,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
@@ -473,7 +465,7 @@ 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 || IsCurrentPathUnreachable || 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(); Character.ReleaseSecondaryItem();
@@ -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";
} }
@@ -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;
@@ -3504,10 +3523,10 @@ namespace Barotrauma
private readonly float stateResetCooldown = 10; private readonly float stateResetCooldown = 10;
private float stateResetTimer; private float stateResetTimer;
private bool isStateChanged; private bool isStateChanged;
private readonly Dictionary<AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<AITrigger, CharacterParams.TargetParams>(); private readonly Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams> activeTriggers = new Dictionary<StatusEffect.AITrigger, CharacterParams.TargetParams>();
private readonly HashSet<AITrigger> inactiveTriggers = new HashSet<AITrigger>(); private readonly HashSet<StatusEffect.AITrigger> inactiveTriggers = new HashSet<StatusEffect.AITrigger>();
public void LaunchTrigger(AITrigger trigger) public void LaunchTrigger(StatusEffect.AITrigger trigger)
{ {
if (trigger.IsTriggered) { return; } if (trigger.IsTriggered) { return; }
if (activeTriggers.ContainsKey(trigger)) { return; } if (activeTriggers.ContainsKey(trigger)) { return; }
@@ -3527,7 +3546,7 @@ namespace Barotrauma
{ {
foreach (var triggerObject in activeTriggers) foreach (var triggerObject in activeTriggers)
{ {
AITrigger trigger = triggerObject.Key; StatusEffect.AITrigger trigger = triggerObject.Key;
if (trigger.IsPermanent) { continue; } if (trigger.IsPermanent) { continue; }
trigger.UpdateTimer(deltaTime); trigger.UpdateTimer(deltaTime);
if (!trigger.IsActive) if (!trigger.IsActive)
@@ -3537,7 +3556,7 @@ namespace Barotrauma
inactiveTriggers.Add(trigger); inactiveTriggers.Add(trigger);
} }
} }
foreach (AITrigger trigger in inactiveTriggers) foreach (StatusEffect.AITrigger trigger in inactiveTriggers)
{ {
activeTriggers.Remove(trigger); activeTriggers.Remove(trigger);
} }
@@ -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;
@@ -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;
@@ -436,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
@@ -443,9 +446,15 @@ 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)
@@ -615,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))
{ {
@@ -900,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))
{ {
@@ -1145,7 +1154,7 @@ namespace Barotrauma
string msgId = "DialogLowOxygen"; string msgId = "DialogLowOxygen";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
} }
if (Character.Bleeding > 2.0f && !Character.IsMedic) if (Character.Bleeding > AfflictionPrefab.Bleeding.TreatmentThreshold && !Character.IsMedic)
{ {
string msgId = "DialogBleeding"; string msgId = "DialogBleeding";
Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f); Character.Speak(TextManager.Get(msgId).Value, delay: Rand.Range(minDelay, maxDelay), identifier: msgId.ToIdentifier(), minDurationBetweenSimilar: 30.0f);
@@ -1658,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.
@@ -1891,7 +1900,7 @@ 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>
@@ -1900,9 +1909,13 @@ namespace Barotrauma
DoForEachBot(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);
} }
@@ -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,46 +126,17 @@ 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)
@@ -183,19 +144,10 @@ namespace Barotrauma
bool needsNewPath = currentPath == null || currentPath.Unreachable || currentPath.Finished || currentPath.CurrentNode == null; 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,14 +157,14 @@ namespace Barotrauma
if (needsNewPath || findPathTimer < -1.0f) if (needsNewPath || findPathTimer < -1.0f)
{ {
IsPathDirty = true; IsPathDirty = true;
if (!needsNewPath && findPathTimer < -1) if (!needsNewPath && currentPath?.CurrentNode is WayPoint wp)
{ {
if (character.Submarine != null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0) if (character.Submarine != null && wp.Ladders == null && wp.ConnectedDoor == null && Math.Abs(character.AnimController.TargetMovement.Combine()) <= 0)
{ {
// Not moving -> need a new path. // Not moving -> need a new path.
needsNewPath = true; needsNewPath = true;
} }
if (character.Submarine == null && currentPath?.CurrentNode is WayPoint wp && wp.CurrentHull != null) if (character.Submarine == null && wp.CurrentHull != null)
{ {
// Current node inside, while we are outside // 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) // -> Check that the current node is not too far (can happen e.g. if someone controls the character in the meanwhile)
@@ -226,7 +178,7 @@ namespace Barotrauma
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;
@@ -252,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()
{ {
@@ -330,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; }
@@ -346,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());
@@ -371,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))
@@ -398,40 +391,28 @@ namespace Barotrauma
} }
} }
} }
var collider = character.AnimController.Collider;
if (character.IsClimbing && !useLadders)
{
character.StopClimbing();
}
if (character.IsClimbing && useLadders) if (character.IsClimbing && useLadders)
{ {
if (currentLadder == null && nextLadder != null) if (currentLadder == null && nextLadder != null && character.SelectedSecondaryItem == nextLadder.Item)
{ {
// Climbing a ladder but the path is still on the node next to the ladder -> Skip the node. // Climbing a ladder but the path is still on the node next to the ladder -> Skip the node.
NextNode(!doorsChecked); NextNode(!doorsChecked);
} }
else else
{ {
bool nextLadderSameAsCurrent = IsNextLadderSameAsCurrent; bool nextLadderSameAsCurrent = currentLadder == nextLadder;
if (nextLadderSameAsCurrent || currentLadder != null && nextLadder != null && Math.Abs(currentLadder.Item.Position.X - nextLadder.Item.Position.X) < 50) if (currentLadder != null && nextLadder != null)
{ {
//climbing ladders -> don't move horizontally //climbing ladders -> don't move horizontally
diff.X = 0.0f; diff.X = 0.0f;
} }
//at the same height as the waypoint //at the same height as the waypoint
float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y); float heightDiff = Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y);
float colliderSize = (collider.Height / 2 + collider.Radius) * 1.25f; float colliderHeight = collider.Height / 2 + collider.Radius;
if (heightDiff < colliderSize) float distanceMargin = ConvertUnits.ToDisplayUnits(colliderSize.X);
if (heightDiff < colliderHeight * 1.25f)
{ {
float heightFromFloor = character.AnimController.GetHeightFromFloor(); if (nextLadder != null && !nextLadderSameAsCurrent)
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative.
bool isAboveFloor = heightFromFloor > -0.1f;
// If the next waypoint is horizontally far, we don't want to keep holding the ladders
if (isAboveFloor && !currentPath.IsAtEndNode && (nextLadder == null || Math.Abs(currentPath.CurrentNode.WorldPosition.X - currentPath.NextNode.WorldPosition.X) > 50))
{
character.StopClimbing();
}
else if (nextLadder != null && !nextLadderSameAsCurrent)
{ {
// Try to change the ladder (hatches between two submarines) // Try to change the ladder (hatches between two submarines)
if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item)) if (character.SelectedSecondaryItem != nextLadder.Item && character.CanInteractWith(nextLadder.Item))
@@ -442,12 +423,36 @@ namespace Barotrauma
} }
} }
} }
if (isAboveFloor || nextLadderSameAsCurrent || nextLadder == null && Math.Abs(diff.Y) < 10) bool isAboveFloor;
if (diff.Y < 0)
{ {
NextNode(!doorsChecked); // When climbing down, let's use the collider bottom to prevent getting stuck at the bottom of the ladders.
float colliderBottom = character.AnimController.Collider.SimPosition.Y;
float floorY = character.AnimController.FloorY;
isAboveFloor = colliderBottom > floorY;
}
else
{
// When climbing up, let's use the lowest collider (feet).
// We need some margin, because if a hatch has closed, it's possible that the height from floor is slightly negative,
// when a foot is still below the platform.
float heightFromFloor = character.AnimController.GetHeightFromFloor();
isAboveFloor = heightFromFloor > -0.1f;
}
if (isAboveFloor)
{
if (Math.Abs(diff.Y) < distanceMargin)
{
NextNode(!doorsChecked);
}
else if (!currentPath.IsAtEndNode && (nextLadder == null || (currentLadder != null && Math.Abs(currentLadder.Item.WorldPosition.X - nextLadder.Item.WorldPosition.X) > distanceMargin)))
{
// Can't skip the node -> Release the ladders, because the next node is not on a ladder or it's horizontally too far.
character.StopClimbing();
}
} }
} }
else if (nextLadder != null) else if (currentLadder != null && currentPath.NextNode != null)
{ {
if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y)) if (Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
{ {
@@ -466,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);
@@ -485,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.
@@ -512,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 && currentLadder == null && (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)
@@ -533,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; }
@@ -631,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,6 +506,8 @@ namespace Barotrauma
} }
} }
public virtual void SpeakAfterOrderReceived() { }
protected static bool CanEquip(Character character, Item item, bool allowWearing) protected static bool CanEquip(Character character, Item item, bool allowWearing)
{ {
if (item == null) { return false; } if (item == null) { return false; }
@@ -14,6 +14,10 @@ namespace Barotrauma
public readonly List<Item> prioritizedItems = new List<Item>(); public readonly List<Item> prioritizedItems = new List<Item>();
public static readonly Identifier AllowCleanupTag = "allowcleanup".ToIdentifier();
protected override int MaxTargets => 100;
public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1) public AIObjectiveCleanupItems(Character character, AIObjectiveManager objectiveManager, Item prioritizedItem = null, float priorityModifier = 1)
: base(character, objectiveManager, priorityModifier) : base(character, objectiveManager, priorityModifier)
{ {
@@ -81,8 +85,8 @@ namespace Barotrauma
public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) => public static bool IsValidContainer(Item container, Character character, bool allowUnloading = true) =>
allowUnloading && allowUnloading &&
container.HasTag(AllowCleanupTag) &&
container.HasAccess(character) && container.HasAccess(character) &&
container.HasTag("allowcleanup") &&
container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() && container.ParentInventory == null && container.OwnInventory != null && container.OwnInventory.AllItems.Any() &&
container.GetComponent<ItemContainer>() != null && container.GetComponent<ItemContainer>() != null &&
IsItemInsideValidSubmarine(container, character) && IsItemInsideValidSubmarine(container, character) &&
@@ -91,7 +95,6 @@ namespace Barotrauma
public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true) public static bool IsValidTarget(Item item, Character character, bool checkInventory, bool allowUnloading = true)
{ {
if (item == null) { return false; } if (item == null) { return false; }
if (!item.HasAccess(character)) { return false; }
if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; } if ((item.SpawnedInCurrentOutpost && !item.AllowStealing) == character.IsOnPlayerTeam) { return false; }
if (item.ParentInventory != null) if (item.ParentInventory != null)
{ {
@@ -102,6 +105,7 @@ namespace Barotrauma
} }
if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; } if (!IsValidContainer(item.Container, character, allowUnloading)) { return false; }
} }
if (!item.HasAccess(character)) { return false; }
if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; } if (character != null && !IsItemInsideValidSubmarine(item, character)) { return false; }
if (item.HasBallastFloraInHull) { return false; } if (item.HasBallastFloraInHull) { return false; }
var wire = item.GetComponent<Wire>(); var wire = item.GetComponent<Wire>();
@@ -995,10 +995,18 @@ namespace Barotrauma
} }
} }
} }
if (HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems) && !Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy))
//prefer using handcuffs already on the enemy's inventory
if (!HumanAIController.HasItem(Enemy, "handlocker".ToIdentifier(), out IEnumerable<Item> matchingItems))
{
HumanAIController.HasItem(character, "handlocker".ToIdentifier(), out matchingItems);
}
if (matchingItems.Any() &&
!Enemy.IsUnconscious && Enemy.IsKnockedDown && character.CanInteractWith(Enemy) && !Enemy.LockHands)
{ {
var handCuffs = matchingItems.First(); var handCuffs = matchingItems.First();
if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true)) if (!HumanAIController.TakeItem(handCuffs, Enemy.Inventory, equip: true, wear: true))
{ {
#if DEBUG #if DEBUG
DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red); DebugConsole.NewMessage($"{character.Name}: Failed to handcuff the target.", Color.Red);
@@ -198,7 +198,7 @@ namespace Barotrauma
TargetName = container.Item.Name, TargetName = container.Item.Name,
AbortCondition = obj => AbortCondition = obj =>
container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) || container?.Item == null || container.Item.Removed || !container.Item.HasAccess(character) ||
(container.Item.GetRootContainer()?.OwnInventory?.Locked ?? false) || (container.Item.RootContainer?.OwnInventory?.Locked ?? false) ||
ItemToContain == null || ItemToContain.Removed || ItemToContain == null || ItemToContain.Removed ||
!ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character, !ItemToContain.IsOwnedBy(character) || container.Item.GetRootInventoryOwner() is Character c && c != character,
SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(), SpeakIfFails = !objectiveManager.IsCurrentOrder<AIObjectiveCleanupItems>(),
@@ -30,7 +30,8 @@ namespace Barotrauma
public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier(); public static readonly Identifier DIVING_GEAR_WEARABLE_INDOORS = "divinggear_wearableindoors".ToIdentifier();
public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier(); public static readonly Identifier OXYGEN_SOURCE = "oxygensource".ToIdentifier();
protected override bool CheckObjectiveSpecific() => targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head); protected override bool CheckObjectiveSpecific() =>
targetItem != null && character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head);
public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier) public AIObjectiveFindDivingGear(Character character, bool needsDivingSuit, AIObjectiveManager objectiveManager, float priorityModifier = 1) : base(character, objectiveManager, priorityModifier)
{ {
@@ -51,7 +52,7 @@ namespace Barotrauma
TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true)); TrySetTargetItem(character.Inventory.FindItemByTag(HEAVY_DIVING_GEAR, true));
} }
if (targetItem == null || if (targetItem == null ||
!character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes) && !character.HasEquippedItem(targetItem, slotType: InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head) &&
targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it))) targetItem.ContainedItems.Any(it => IsSuitableContainedOxygenSource(it)))
{ {
TryAddSubObjective(ref getDivingGear, () => TryAddSubObjective(ref getDivingGear, () =>
@@ -65,7 +66,7 @@ namespace Barotrauma
AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _), AllowStealing = HumanAIController.NeedsDivingGear(character.CurrentHull, out _),
AllowToFindDivingGear = false, AllowToFindDivingGear = false,
AllowDangerousPressure = true, AllowDangerousPressure = true,
EquipSlotType = InvSlotType.OuterClothes | InvSlotType.Head | InvSlotType.InnerClothes, EquipSlotType = InvSlotType.OuterClothes | InvSlotType.InnerClothes | InvSlotType.Head,
Wear = true Wear = true
}; };
}, },

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