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

# Conflicts:
#	Barotrauma/BarotraumaClient/ClientSource/DebugConsole.cs
#	Barotrauma/BarotraumaClient/ClientSource/Events/EventManager.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/CrewManagement.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/MedicalClinicUI.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/Store.cs
#	Barotrauma/BarotraumaClient/ClientSource/GUI/SubmarineSelection.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/GameModes/MultiPlayerCampaign.cs
#	Barotrauma/BarotraumaClient/ClientSource/GameSession/RoundSummary.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Door.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Machines/Fabricator.cs
#	Barotrauma/BarotraumaClient/ClientSource/Items/Components/Projectile.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/ConvexHull.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightManager.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Lights/LightSource.cs
#	Barotrauma/BarotraumaClient/ClientSource/Map/Structure.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/Voip/VoipClient.cs
#	Barotrauma/BarotraumaClient/ClientSource/Networking/Voting.cs
#	Barotrauma/BarotraumaClient/ClientSource/Screens/ServerListScreen/ServerListScreen.cs
#	Barotrauma/BarotraumaClient/LinuxClient.csproj
#	Barotrauma/BarotraumaClient/MacClient.csproj
#	Barotrauma/BarotraumaClient/WindowsClient.csproj
#	Barotrauma/BarotraumaServer/LinuxServer.csproj
#	Barotrauma/BarotraumaServer/MacServer.csproj
#	Barotrauma/BarotraumaServer/ServerSource/DebugConsole.cs
#	Barotrauma/BarotraumaServer/ServerSource/GameSession/MedicalClinic.cs
#	Barotrauma/BarotraumaServer/ServerSource/Items/Inventory.cs
#	Barotrauma/BarotraumaServer/ServerSource/Networking/GameServer.cs
#	Barotrauma/BarotraumaServer/ServerSource/Utils/DoSProtection.cs
#	Barotrauma/BarotraumaServer/WindowsServer.csproj
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/HumanAIController.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/IndoorsSteeringManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveContainItem.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveFindSafety.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGetItem.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveGoTo.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/Objectives/AIObjectiveIdle.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/AI/ShipCommand/ShipIssueWorkerOperateWeapons.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Attack.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Character.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/Affliction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/Afflictions/AfflictionPrefab.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Health/CharacterHealth.cs
#	Barotrauma/BarotraumaShared/SharedSource/Characters/Params/CharacterParams.cs
#	Barotrauma/BarotraumaShared/SharedSource/ContentManagement/ContentPackageManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Enums.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/MissionAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCChangeTeamAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCFollowAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventActions/NPCWaitAction.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/EventManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/Mission.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/PirateMission.cs
#	Barotrauma/BarotraumaShared/SharedSource/Events/Missions/SalvageMission.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/CargoManager.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/Data/Reputation.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/GameModes/CampaignMode.cs
#	Barotrauma/BarotraumaShared/SharedSource/GameSession/GameSession.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/LevelResource.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Holdable/RangedWeapon.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/ItemContainer.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Projectile.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Signal/LightComponent.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Components/Turret.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs
#	Barotrauma/BarotraumaShared/SharedSource/Items/ItemPrefab.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Gap.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Levels/Level.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Map/Location.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Map/Map.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Outposts/OutpostGenerator.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/Submarine.cs
#	Barotrauma/BarotraumaShared/SharedSource/Map/SubmarineBody.cs
#	Barotrauma/BarotraumaShared/SharedSource/Networking/ServerSettings.cs
#	Barotrauma/BarotraumaShared/SharedSource/Settings/GameSettings.cs
#	Barotrauma/BarotraumaShared/SharedSource/StatusEffects/PropertyConditional.cs
#	Barotrauma/BarotraumaShared/SharedSource/StatusEffects/StatusEffect.cs
#	Barotrauma/BarotraumaShared/changelog.txt
This commit is contained in:
Regalis11
2023-06-15 16:55:39 +03:00
238 changed files with 5395 additions and 3045 deletions
@@ -19,7 +19,7 @@ namespace Barotrauma
var target = _selectedAiTarget ?? _lastAiTarget;
if (target != null && target.Entity != null)
{
var memory = GetTargetMemory(target, false);
var memory = GetTargetMemory(target);
if (memory != null)
{
Vector2 targetPos = memory.Location;
@@ -10,7 +10,7 @@ namespace Barotrauma
public override void DebugDraw(Microsoft.Xna.Framework.Graphics.SpriteBatch spriteBatch)
{
if (Character == Character.Controlled) { return; }
if (!debugai) { return; }
if (!DebugAI) { return; }
Vector2 pos = Character.WorldPosition;
pos.Y = -pos.Y;
Vector2 textOffset = new Vector2(-40, -160);
@@ -6,11 +6,17 @@ namespace Barotrauma
{
partial class Attack
{
[Serialize("StructureBlunt", IsPropertySaveable.Yes), Editable()]
[Serialize("StructureBlunt", IsPropertySaveable.Yes, description: "Name of the sound effect the attack makes when it hits a structure."), Editable()]
public string StructureSoundType { get; private set; }
/// <summary>
/// Sound to play when the attack deals damage.
/// </summary>
private RoundSound sound;
/// <summary>
/// Particle emitter to use when the attack deals damage.
/// </summary>
private ParticleEmitter particleEmitter;
partial void InitProjSpecific(ContentXElement element)
@@ -582,7 +582,7 @@ namespace Barotrauma
float closestItemDistance = Math.Max(aimAssistAmount, 2.0f);
foreach (MapEntity entity in entityList)
{
if (!(entity is Item item))
if (entity is not Item item)
{
continue;
}
@@ -214,7 +214,6 @@ namespace Barotrauma
double aimAngle = msg.ReadUInt16() / 65535.0 * 2.0 * Math.PI;
cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 500.0f;
TransformCursorPos();
bool ragdollInput = msg.ReadBoolean();
keys[(int)InputType.Ragdoll].Held = ragdollInput;
@@ -850,7 +850,7 @@ namespace Barotrauma
if (treatmentButton.Enabled && treatmentButton.State == GUIComponent.ComponentState.Hover)
{
//highlight the slot the treatment item is in
var rootContainer = matchingItem.GetRootContainer() ?? matchingItem;
var rootContainer = matchingItem.RootContainer ?? matchingItem;
var index = Character.Controlled.Inventory.FindIndex(rootContainer);
if (Character.Controlled.Inventory.visualSlots != null && index > -1 && index < Character.Controlled.Inventory.visualSlots.Length &&
Character.Controlled.Inventory.visualSlots[index].HighlightTimer <= 0.0f)
@@ -870,7 +870,7 @@ namespace Barotrauma
{
if (wearable.Type == WearableType.Hair)
{
if (HairWithHatSprite != null)
if (HairWithHatSprite != null && !hideLimb)
{
DrawWearable(HairWithHatSprite, depthStep, spriteBatch, blankColor, alpha: color.A / 255f, spriteEffect);
depthStep += step;
@@ -86,7 +86,7 @@ namespace Barotrauma
public string ModVersion = ContentPackage.DefaultModVersion;
public Md5Hash? ExpectedHash { get; private set; }
public Md5Hash? ExpectedHash { get; set; }
public bool IsCore = false;
@@ -125,9 +125,11 @@ namespace Barotrauma
public static string IncrementModVersion(string modVersion)
{
if (string.IsNullOrWhiteSpace(modVersion)) { return string.Empty; }
//look for an integer at the end of the string and increment it
int startIndex = modVersion.Length - 1;
while (char.IsDigit(modVersion[startIndex])) { startIndex--; }
while (startIndex > 0 && char.IsDigit(modVersion[startIndex])) { startIndex--; }
startIndex++;
if (startIndex >= modVersion.Length
@@ -9,6 +9,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
@@ -636,15 +637,29 @@ namespace Barotrauma
commands.Add(new Command("wikiimage_character", "Save an image of the currently controlled character with a transparent background.", (string[] args) =>
{
if (Character.Controlled == null) { return; }
WikiImage.Create(Character.Controlled);
try
{
WikiImage.Create(Character.Controlled);
}
catch (Exception e)
{
DebugConsole.ThrowError("The command 'wikiimage_character' failed.", e);
}
}));
commands.Add(new Command("wikiimage_sub", "Save an image of the main submarine with a transparent background.", (string[] args) =>
{
if (Submarine.MainSub == null) { return; }
MapEntity.SelectedList.Clear();
MapEntity.ClearHighlightedEntities();
WikiImage.Create(Submarine.MainSub);
try
{
MapEntity.SelectedList.Clear();
MapEntity.ClearHighlightedEntities();
WikiImage.Create(Submarine.MainSub);
}
catch (Exception e)
{
DebugConsole.ThrowError("The command 'wikiimage_sub' failed.", e);
}
}));
AssignRelayToServer("kick", false);
@@ -1141,6 +1156,15 @@ namespace Barotrauma
GameMain.LightManager.DebugLos = state;
NewMessage("Los debug draw mode " + (GameMain.LightManager.DebugLos ? "enabled" : "disabled"), Color.Yellow);
});
AssignOnExecute("debugwiring", (string[] args) =>
{
if (args.None() || !bool.TryParse(args[0], out bool state))
{
state = !ConnectionPanel.DebugWiringMode;
}
ConnectionPanel.DebugWiringMode = state;
NewMessage("Wiring debug mode " + (ConnectionPanel.DebugWiringMode ? "enabled" : "disabled"), Color.Yellow);
});
AssignRelayToServer("debugdraw", false);
AssignOnExecute("devmode", (string[] args) =>
@@ -1263,8 +1287,8 @@ namespace Barotrauma
AssignOnExecute("debugai", (string[] args) =>
{
HumanAIController.debugai = !HumanAIController.debugai;
if (HumanAIController.debugai)
HumanAIController.DebugAI = !HumanAIController.DebugAI;
if (HumanAIController.DebugAI)
{
GameMain.DevMode = true;
GameMain.DebugDraw = true;
@@ -1279,7 +1303,7 @@ namespace Barotrauma
GameMain.LightManager.LosEnabled = true;
GameMain.LightManager.LosAlpha = 1f;
}
NewMessage(HumanAIController.debugai ? "AI debug info visible" : "AI debug info hidden", Color.Yellow);
NewMessage(HumanAIController.DebugAI ? "AI debug info visible" : "AI debug info hidden", Color.Yellow);
});
AssignRelayToServer("debugai", false);
@@ -2338,7 +2362,7 @@ namespace Barotrauma
{
if (mapEntity is Item item)
{
item.Rect = new Rectangle(item.Rect.X, item.Rect.Y,
item.Rect = item.DefaultRect = new Rectangle(item.Rect.X, item.Rect.Y,
(int)(item.Prefab.Sprite.size.X * item.Prefab.Scale),
(int)(item.Prefab.Sprite.size.Y * item.Prefab.Scale));
}
@@ -2811,7 +2835,26 @@ namespace Barotrauma
ContentPackageManager.EnabledPackages.ReloadCore();
}));
#warning TODO: reimplement?
#if WINDOWS
commands.Add(new Command("startdedicatedserver", "", (string[] args) =>
{
Process.Start("DedicatedServer.exe");
}));
commands.Add(new Command("editserversettings", "", (string[] args) =>
{
if (Process.GetProcessesByName("DedicatedServer").Length > 0)
{
NewMessage("Can't be edited if DedicatedServer.exe is already running", Color.Red);
}
else
{
Process.Start("notepad.exe", "serversettings.xml");
}
}));
#endif
#warning TODO: reimplement?
/*commands.Add(new Command("ingamemodswap", "", (string[] args) =>
{
ContentPackage.IngameModSwap = !ContentPackage.IngameModSwap;
@@ -662,34 +662,36 @@ namespace Barotrauma
Identifier missionIdentifier = msg.ReadIdentifier();
int locationIndex = msg.ReadInt32();
int destinationIndex = msg.ReadInt32();
string missionName = msg.ReadString();
MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier);
if (prefab != null)
if (Screen.Selected != GameMain.NetLobbyScreen)
{
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName),
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
MissionPrefab? prefab = MissionPrefab.Prefabs.Find(mp => mp.Identifier == missionIdentifier);
if (prefab != null)
{
IconColor = prefab.IconColor
};
if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count)
{
Location location = map.Locations[locationIndex];
map.Discover(location, checkTalents: false);
new GUIMessageBox(string.Empty, TextManager.GetWithVariable("missionunlocked", "[missionname]", missionName),
Array.Empty<LocalizedString>(), type: GUIMessageBox.Type.InGame, icon: prefab.Icon, relativeSize: new Vector2(0.3f, 0.15f), minSize: new Point(512, 128))
{
IconColor = prefab.IconColor
};
if (GameMain.GameSession?.Map is { } map && locationIndex >= 0 && locationIndex < map.Locations.Count)
{
Location location = map.Locations[locationIndex];
map.Discover(location, checkTalents: false);
LocationConnection? connection = null;
if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count)
{
Location destination = map.Locations[destinationIndex];
connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination));
}
if (connection != null)
{
location.UnlockMission(prefab, connection);
}
else
{
location.UnlockMission(prefab);
LocationConnection? connection = null;
if (destinationIndex != locationIndex && destinationIndex >= 0 && destinationIndex < map.Locations.Count)
{
Location destination = map.Locations[destinationIndex];
connection = map.Connections.FirstOrDefault(c => c.Locations.Contains(location) && c.Locations.Contains(destination));
}
if (connection != null)
{
location.UnlockMission(prefab, connection);
}
else
{
location.UnlockMission(prefab);
}
}
}
}
@@ -88,13 +88,14 @@ namespace Barotrauma
public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element)
=> TextManager.SpeciallyHandledCharCategories
.Where(category => element.GetAttributeBool($"is{category}", category switch {
// CJK isn't supported by default
// CJK and Japanese aren't supported by default
TextManager.SpeciallyHandledCharCategory.CJK => false,
TextManager.SpeciallyHandledCharCategory.Japanese => false,
// For backwards compatibility, we assume that Cyrillic is supported by default
TextManager.SpeciallyHandledCharCategory.Cyrillic => true,
_ => throw new Exception("unreachable")
_ => throw new NotImplementedException($"nameof{category} not implemented.")
}))
.Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category);
@@ -517,6 +518,10 @@ namespace Barotrauma
GlyphData gd = GetGlyphData(charIndex);
if (gd.TexIndex >= 0)
{
if (gd.TexIndex < 0 || gd.TexIndex >= textures.Count)
{
throw new ArgumentOutOfRangeException($"Error while rendering text. Texture index was out of range. Text: {text}, char: {charIndex} index: {gd.TexIndex}, texture count: {textures.Count}");
}
Texture2D tex = textures[gd.TexIndex];
Vector2 drawOffset;
drawOffset.X = gd.DrawOffset.X * advanceUnit.X * scale.X - gd.DrawOffset.Y * advanceUnit.Y * scale.Y;
@@ -533,7 +533,7 @@ namespace Barotrauma
{
if (characterInfo.MinReputationToHire.factionId != Identifier.Empty)
{
if (campaign.GetReputation(characterInfo.MinReputationToHire.factionId) < characterInfo.MinReputationToHire.reputation)
if (MathF.Round(campaign.GetReputation(characterInfo.MinReputationToHire.factionId)) < characterInfo.MinReputationToHire.reputation)
{
return false;
}
@@ -532,6 +532,7 @@ namespace Barotrauma
void drawRect(Vector2 topLeft, Vector2 bottomRight)
{
int minWidth = GUI.IntScale(5);
if (OverflowClip) { topLeft.X = Math.Max(topLeft.X, 0.0f); }
if (bottomRight.X - topLeft.X < minWidth) { bottomRight.X = topLeft.X + minWidth; }
GUI.DrawRectangle(spriteBatch,
Rect.Location.ToVector2() + topLeft,
@@ -801,6 +802,7 @@ namespace Barotrauma
IEnumerable<GUITextBox> GetAndSortTextBoxes(GUIComponent parent) => parent.GetAllChildren<GUITextBox>().OrderBy(t => t.Rect.Y).ThenBy(t => t.Rect.X);
GUITextBox SelectNextTextBox(GUIListBox listBox)
{
if (listBox?.SelectedComponent == null) { return null; }
var textBoxes = GetAndSortTextBoxes(listBox.SelectedComponent);
if (textBoxes.Any())
{
@@ -240,7 +240,7 @@ namespace Barotrauma
private void UpdatePending()
{
if (!(pendingHealList is { } healList)) { return; }
if (pendingHealList is not { } healList) { return; }
ImmutableArray<MedicalClinic.NetCrewMember> pendingList = medicalClinic.PendingHeals.ToImmutableArray();
@@ -493,20 +493,26 @@ namespace Barotrauma
GUIButton treatAllButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), clinicContainer.RectTransform), TextManager.Get("medicalclinic.treateveryone"))
{
OnClicked = (_, _) =>
OnClicked = (button, _) =>
{
if (isWaitingForServer) { return true; }
button.Enabled = false;
isWaitingForServer = true;
medicalClinic.TreatAllButtonAction(OnReceived);
bool wasSuccessful = medicalClinic.TreatAllButtonAction(_ => ReEnableButton());
if (!wasSuccessful) { ReEnableButton(); }
void ReEnableButton()
{
isWaitingForServer = false;
button.Enabled = true;
}
return true;
}
};
crewHealList = new CrewHealList(crewList, parent, treatAllButton);
void OnReceived(MedicalClinic.CallbackOnlyRequest obj)
{
isWaitingForServer = false;
}
}
private void CreateCrewEntry(GUIComponent parent, CrewHealList healList, CharacterInfo info, GUIComponent panel)
@@ -585,8 +591,10 @@ namespace Barotrauma
OnClicked = (button, _) =>
{
button.Enabled = false;
medicalClinic.HealAllButtonAction(request =>
isWaitingForServer = true;
bool wasSuccessful = medicalClinic.HealAllButtonAction(request =>
{
isWaitingForServer = false;
switch (request.HealResult)
{
case MedicalClinic.HealRequestResult.InsufficientFunds:
@@ -600,6 +608,12 @@ namespace Barotrauma
button.Enabled = true;
ClosePopup();
});
if (!wasSuccessful)
{
isWaitingForServer = false;
button.Enabled = true;
}
ClosePopup();
return true;
}
@@ -610,11 +624,19 @@ namespace Barotrauma
ClickSound = GUISoundType.Cart,
OnClicked = (button, _) =>
{
if (isWaitingForServer) { return true; }
button.Enabled = false;
medicalClinic.ClearAllButtonAction(_ =>
isWaitingForServer = true;
bool wasSuccessful = medicalClinic.ClearAllButtonAction(_ => ReEnableButton());
if (!wasSuccessful) { ReEnableButton(); }
void ReEnableButton()
{
isWaitingForServer = false;
button.Enabled = true;
});
}
return true;
}
};
@@ -701,10 +723,15 @@ namespace Barotrauma
OnClicked = (button, _) =>
{
button.Enabled = false;
medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ =>
bool wasSuccessful = medicalClinic.RemovePendingButtonAction(crewMember, affliction, _ =>
{
button.Enabled = true;
});
if (!wasSuccessful)
{
button.Enabled = true;
}
return true;
}
};
@@ -792,7 +819,13 @@ namespace Barotrauma
selectedCrewAfflictionList = popupAfflictionList;
isWaitingForServer = true;
medicalClinic.RequestAfflictions(info, OnReceived);
bool wasSuccessful = medicalClinic.RequestAfflictions(info, OnReceived);
if (!wasSuccessful)
{
isWaitingForServer = false;
ClosePopup();
}
void OnReceived(MedicalClinic.AfflictionRequest request)
{
@@ -800,6 +833,16 @@ namespace Barotrauma
if (request.Result != MedicalClinic.RequestResult.Success)
{
switch (request.Result)
{
case MedicalClinic.RequestResult.CharacterInfoMissing:
DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName}\" in medical clini because the character health was missing.");
break;
case MedicalClinic.RequestResult.CharacterNotFound:
DebugConsole.ThrowError($"Unable to select character \"{info.Character?.DisplayName} in medical clinic because the server was unable to find a character with ID {info.ID}.");
break;
}
feedbackBlock.Text = GetErrorText(request.Result);
feedbackBlock.TextColor = GUIStyle.Red;
return;
@@ -953,14 +996,20 @@ namespace Barotrauma
}
existingMember.Afflictions = existingMember.Afflictions.Concat(afflictions).ToImmutableArray();
ToggleElements(ElementState.Disabled, elementsToDisable);
medicalClinic.AddPendingButtonAction(existingMember, request =>
bool wasSuccessful = medicalClinic.AddPendingButtonAction(existingMember, request =>
{
if (request.Result == MedicalClinic.RequestResult.Timeout)
{
ToggleElements(ElementState.Enabled, elementsToDisable);
}
});
if (!wasSuccessful)
{
ToggleElements(ElementState.Enabled, elementsToDisable);
}
}
#warning TODO: this doesn't seem like the right place for this, and it's not clear from the method signature how this differs from ToolBox.LimitString
@@ -1090,9 +1139,8 @@ namespace Barotrauma
{
return result switch
{
MedicalClinic.RequestResult.Error => TextManager.Get("error"),
MedicalClinic.RequestResult.Timeout => TextManager.Get("medicalclinic.requesttimeout"),
_ => "What the hell did you just do" // this should never happen
_ => TextManager.Get("error")
};
}
@@ -175,6 +175,7 @@ namespace Barotrauma
{
if (relativeOffset.NearlyEquals(value)) { return; }
relativeOffset = value;
recalculateRect = true;
RecalculateChildren(false, false);
}
}
@@ -870,7 +870,7 @@ namespace Barotrauma
{
foreach (var minRep in priceInfo.MinReputation)
{
if (campaign.GetReputation(minRep.Key) < minRep.Value)
if (MathF.Round(campaign.GetReputation(minRep.Key)) < minRep.Value)
{
return minRep;
}
@@ -1930,7 +1930,7 @@ namespace Barotrauma
"campaignstore.reputationrequired",
("[amount]", ((int)requiredReputation.Value.Value).ToString()),
("[faction]", TextManager.Get("faction." + requiredReputation.Value.Key).Value));
Color color = campaign.GetReputation(requiredReputation.Value.Key) < requiredReputation.Value.Value ?
Color color = MathF.Round(campaign.GetReputation(requiredReputation.Value.Key)) < requiredReputation.Value.Value ?
GUIStyle.Orange : GUIStyle.Green;
toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖";
}
@@ -807,8 +807,10 @@ namespace Barotrauma
{
if (GameMain.Client == null)
{
GameMain.GameSession.PurchaseSubmarine(selectedSubmarine);
GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch);
if (GameMain.GameSession.TryPurchaseSubmarine(selectedSubmarine))
{
GameMain.GameSession.SwitchSubmarine(selectedSubmarine, TransferItemsOnSwitch);
}
RefreshSubmarineDisplay(true);
}
else
@@ -829,7 +831,7 @@ namespace Barotrauma
{
if (GameMain.Client == null)
{
GameMain.GameSession.PurchaseSubmarine(selectedSubmarine);
GameMain.GameSession.TryPurchaseSubmarine(selectedSubmarine);
RefreshSubmarineDisplay(true);
}
else
@@ -23,9 +23,12 @@ namespace Barotrauma
private float votingTime = 100f;
private float timer;
private VoteType currentVoteType;
private Color SubmarineColor => GUIStyle.Orange;
private static Color SubmarineColor => GUIStyle.Orange;
private Point createdForResolution;
//timer ran out but server still hasn't notified of the result of the vote
public bool TimedOut => VoteRunning && timer - votingTime > 10.0f;
public static VotingInterface CreateSubmarineVotingInterface(Client starter, SubmarineInfo info, VoteType type, bool transferItems, float votingTime)
{
if (starter == null || info == null) { return null; }
@@ -664,7 +664,10 @@ namespace Barotrauma
while (Timing.Accumulator >= Timing.Step)
{
Timing.TotalTime += Timing.Step;
if (!Paused)
{
Timing.TotalTimeUnpaused += Timing.Step;
}
Stopwatch sw = new Stopwatch();
sw.Start();
@@ -944,7 +947,10 @@ namespace Barotrauma
PerformanceCounter.UpdateTimeGraph.Update(sw.ElapsedTicks * 1000.0f / (float)Stopwatch.Frequency);
}
if (!Paused) { Timing.Alpha = Timing.Accumulator / Timing.Step; }
if (!Paused)
{
Timing.Alpha = Timing.Accumulator / Timing.Step;
}
if (performanceCounterTimer.ElapsedMilliseconds > 1000)
{
@@ -31,7 +31,7 @@ namespace Barotrauma
// Item must be in a non-equipment slot if possible
if (!item.AllowedSlots.All(s => equipmentSlots.Contains(s)) && IsInEquipmentSlot(item)) { return false; }
// Item must not be contained inside an item in an equipment slot
if (item.GetRootContainer() is Item rootContainer && IsInEquipmentSlot(rootContainer)) { return false; }
if (item.RootContainer is Item rootContainer && IsInEquipmentSlot(rootContainer)) { return false; }
return true;
}, recursive: true).Distinct();
@@ -166,6 +166,9 @@ namespace Barotrauma
if (Submarine.MainSub == null || Level.Loaded == null) { return; }
bool allowEndingRound = false;
endRoundButton.Color = endRoundButton.Style.Color;
endRoundButton.HoverColor = endRoundButton.Style.HoverColor;
RichString overrideEndRoundButtonToolTip = string.Empty;
var availableTransition = GetAvailableTransition(out _, out Submarine leavingSub);
LocalizedString buttonText = "";
switch (availableTransition)
@@ -194,13 +197,23 @@ namespace Barotrauma
break;
case TransitionType.None:
default:
if (Level.Loaded.Type == LevelData.LevelType.Outpost &&
!Level.Loaded.IsEndBiome &&
(Character.Controlled?.Submarine?.Info.Type == SubmarineType.Player || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false)))
bool inFriendlySub = Character.Controlled is { IsInFriendlySub: true };
if (Level.Loaded.Type == LevelData.LevelType.Outpost && !Level.Loaded.IsEndBiome &&
(inFriendlySub || (Character.Controlled?.CurrentHull?.OutpostModuleTags.Contains("airlock".ToIdentifier()) ?? false)))
{
if (Missions.Any(m => m is SalvageMission salvageMission && salvageMission.AnyTargetNeedsToBeRetrievedToSub))
{
overrideEndRoundButtonToolTip = TextManager.Get("SalvageTargetNotInSub");
endRoundButton.Color = GUIStyle.Red * 0.7f;
endRoundButton.HoverColor = GUIStyle.Red;
}
buttonText = TextManager.GetWithVariable("LeaveLocation", "[locationname]", Level.Loaded.StartLocation?.Name ?? "[ERROR]");
allowEndingRound = !ForceMapUI && !ShowCampaignUI;
}
else
{
allowEndingRound = false;
}
break;
}
if (Level.IsLoadedOutpost && !ObjectiveManager.AllActiveObjectivesCompleted())
@@ -227,7 +240,11 @@ namespace Barotrauma
prevCampaignUIAutoOpenType = availableTransition;
}
endRoundButton.Text = ToolBox.LimitString(buttonText.Value, endRoundButton.Font, endRoundButton.Rect.Width - 5);
if (endRoundButton.Text != buttonText)
if (overrideEndRoundButtonToolTip != string.Empty)
{
endRoundButton.ToolTip = overrideEndRoundButtonToolTip;
}
else if (endRoundButton.Text != buttonText)
{
endRoundButton.ToolTip = buttonText;
}
@@ -193,7 +193,7 @@ namespace Barotrauma
if (GameMain.Client == null)
{
yield return CoroutineStatus.Failure;
yield return CoroutineStatus.Success;
}
if (GameMain.Client.LateCampaignJoin)
@@ -335,7 +335,7 @@ namespace Barotrauma
//--------------------------------------
//wait for the new level to be loaded
DateTime timeOut = DateTime.Now + new TimeSpan(0, 0, seconds: 60);
DateTime timeOut = DateTime.Now + GameClient.LevelTransitionTimeOut;
while (Level.Loaded == prevLevel || Level.Loaded == null)
{
if (DateTime.Now > timeOut || Screen.Selected != GameMain.GameScreen) { break; }
@@ -345,7 +345,11 @@ namespace Barotrauma
endTransition.Stop();
overlayColor = Color.Transparent;
if (DateTime.Now > timeOut) { GameMain.NetLobbyScreen.Select(); }
if (DateTime.Now > timeOut)
{
DebugConsole.ThrowError("Failed to start the round. Timed out while waiting for the level transition to finish.");
GameMain.NetLobbyScreen.Select();
}
if (Screen.Selected is not RoundSummaryScreen)
{
if (continueButton != null)
@@ -17,7 +17,8 @@ namespace Barotrauma
{
Undecided,
Success,
Error,
CharacterInfoMissing,
CharacterNotFound,
Timeout
}
@@ -34,7 +35,9 @@ namespace Barotrauma
private readonly List<RequestAction<CallbackOnlyRequest>> addRequests = new List<RequestAction<CallbackOnlyRequest>>();
private readonly List<RequestAction<CallbackOnlyRequest>> removeRequests = new List<RequestAction<CallbackOnlyRequest>>();
public void RequestAfflictions(CharacterInfo info, Action<AfflictionRequest> onReceived)
private static readonly LeakyBucket requestBucket = new(RateLimitExpiry / (float)RateLimitMaxRequests, 10);
public bool RequestAfflictions(CharacterInfo info, Action<AfflictionRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
@@ -42,23 +45,26 @@ namespace Barotrauma
if (Screen.Selected is TestScreen)
{
onReceived.Invoke(new AfflictionRequest(RequestResult.Success, TestAfflictions.ToImmutableArray()));
return;
return true;
}
#endif
if (info is not { Character.CharacterHealth: { } health })
{
onReceived.Invoke(new AfflictionRequest(RequestResult.Error, ImmutableArray<NetAffliction>.Empty));
return;
onReceived.Invoke(new AfflictionRequest(RequestResult.CharacterInfoMissing, ImmutableArray<NetAffliction>.Empty));
return true;
}
ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health).ToImmutableArray();
ImmutableArray<NetAffliction> pendingAfflictions = GetAllAfflictions(health);
onReceived.Invoke(new AfflictionRequest(RequestResult.Success, pendingAfflictions));
return;
return true;
}
afflictionRequests.Add(new RequestAction<AfflictionRequest>(onReceived, GetTimeout()));
SendAfflictionRequest(info);
return requestBucket.TryEnqueue(() =>
{
afflictionRequests.Add(new RequestAction<AfflictionRequest>(onReceived, GetTimeout()));
SendAfflictionRequest(info);
});
}
public void RequestLatestPending(Action<PendingRequest> onReceived)
@@ -66,8 +72,11 @@ namespace Barotrauma
// no need to worry about syncing when there's only one pair of eyes capable of looking at the UI
if (GameMain.IsSingleplayer) { return; }
pendingHealRequests.Add(new RequestAction<PendingRequest>(onReceived, GetTimeout()));
SendPendingRequest();
requestBucket.TryEnqueue(() =>
{
pendingHealRequests.Add(new RequestAction<PendingRequest>(onReceived, GetTimeout()));
SendPendingRequest();
});
}
public void Update(float deltaTime)
@@ -79,6 +88,7 @@ namespace Barotrauma
UpdateQueue(clearAllRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(addRequests, now, onTimeout: CallbackOnlyTimeout);
UpdateQueue(removeRequests, now, onTimeout: CallbackOnlyTimeout);
requestBucket.Update(deltaTime);
static void CallbackOnlyTimeout(Action<CallbackOnlyRequest> callback) { callback(new CallbackOnlyRequest(RequestResult.Timeout)); }
}
@@ -146,21 +156,25 @@ namespace Barotrauma
return (from client in clients where client.Name == ownName select client.Ping).FirstOrDefault();
}
public void TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived)
public bool TreatAllButtonAction(Action<CallbackOnlyRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
AddEverythingToPending();
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
return true;
}
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.ADD_EVERYTHING_TO_PENDING, DeliveryMethod.Reliable);
});
}
public void HealAllButtonAction(Action<HealRequest> onReceived)
public bool HealAllButtonAction(Action<HealRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
@@ -171,33 +185,39 @@ namespace Barotrauma
OnUpdate?.Invoke();
}
return;
return true;
}
if (campaign?.CampaignUI?.MedicalClinic is { } ui)
if (campaign?.CampaignUI?.MedicalClinic is { } openedUi)
{
ui.ClosePopup();
openedUi.ClosePopup();
}
healAllRequests.Add(new RequestAction<HealRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
healAllRequests.Add(new RequestAction<HealRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.HEAL_PENDING, DeliveryMethod.Reliable);
});
}
public void ClearAllButtonAction(Action<CallbackOnlyRequest> onReceived)
public bool ClearAllButtonAction(Action<CallbackOnlyRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
ClearPendingHeals();
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
return true;
}
clearAllRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
clearAllRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(null, NetworkHeader.CLEAR_PENDING, DeliveryMethod.Reliable);
});
}
private void ClearRequstReceived()
private void ClearRequestReceived()
{
ClearPendingHeals();
if (TryDequeue(clearAllRequests, out var callback))
@@ -224,28 +244,31 @@ namespace Barotrauma
OnUpdate?.Invoke();
}
public void AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived)
public bool AddPendingButtonAction(NetCrewMember crewMember, Action<CallbackOnlyRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
InsertPendingCrewMember(crewMember);
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
return true;
}
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
addRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(crewMember, NetworkHeader.ADD_PENDING, DeliveryMethod.Reliable);
});
}
public void RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action<CallbackOnlyRequest> onReceived)
public bool RemovePendingButtonAction(NetCrewMember crewMember, NetAffliction affliction, Action<CallbackOnlyRequest> onReceived)
{
if (GameMain.IsSingleplayer)
{
RemovePendingAffliction(crewMember, affliction);
onReceived(new CallbackOnlyRequest(RequestResult.Success));
OnUpdate?.Invoke();
return;
return true;
}
INetSerializableStruct removedAffliction = new NetRemovedAffliction
@@ -254,11 +277,14 @@ namespace Barotrauma
Affliction = affliction
};
removeRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable);
return requestBucket.TryEnqueue(() =>
{
removeRequests.Add(new RequestAction<CallbackOnlyRequest>(onReceived, GetTimeout()));
ClientSend(removedAffliction, NetworkHeader.REMOVE_PENDING, DeliveryMethod.Reliable);
});
}
private void NewAdditonReceived(IReadMessage inc, MessageFlag flag)
private void NewAdditionReceived(IReadMessage inc, MessageFlag flag)
{
var crewMembers = INetSerializableStruct.Read<NetCollection<NetCrewMember>>(inc);
foreach (var crewMember in crewMembers)
@@ -300,7 +326,7 @@ namespace Barotrauma
NetCrewMember crewMember = INetSerializableStruct.Read<NetCrewMember>(inc);
if (TryDequeue(afflictionRequests, out var callback))
{
RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.Error : RequestResult.Success;
RequestResult result = crewMember.CharacterInfoID is 0 ? RequestResult.CharacterNotFound : RequestResult.Success;
callback(new AfflictionRequest(result, crewMember.Afflictions.ToImmutableArray()));
}
}
@@ -336,7 +362,7 @@ namespace Barotrauma
IWriteMessage msg = StartSending();
msg.WriteByte((byte)header);
netStruct?.Write(msg);
GameMain.Client.ClientPeer?.Send(msg, deliveryMethod);
GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod);
}
public void ClientRead(IReadMessage inc)
@@ -356,7 +382,7 @@ namespace Barotrauma
PendingRequestReceived(inc);
break;
case NetworkHeader.ADD_PENDING:
NewAdditonReceived(inc, flag);
NewAdditionReceived(inc, flag);
break;
case NetworkHeader.REMOVE_PENDING:
NewRemovalReceived(inc, flag);
@@ -365,7 +391,7 @@ namespace Barotrauma
HealRequestReceived(inc);
break;
case NetworkHeader.CLEAR_PENDING:
ClearRequstReceived();
ClearRequestReceived();
break;
}
}
@@ -310,9 +310,10 @@ namespace Barotrauma
};
}
}
var missionDescription = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform),
RichString.Rich(missionMessage), wrap: true);
if (selectedMissions.Contains(displayedMission) && displayedMission.Completed)
if (selectedMissions.Contains(displayedMission))
{
RichString reputationText = displayedMission.GetReputationRewardText();
if (!reputationText.IsNullOrEmpty())
@@ -324,7 +325,7 @@ namespace Barotrauma
if (totalReward > 0)
{
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), missionTextContent.RectTransform), RichString.Rich(displayedMission.GetMissionRewardText(Submarine.MainSub)));
if (GameMain.IsMultiplayer && Character.Controlled is { } controlled)
if (GameMain.IsMultiplayer && Character.Controlled is { } controlled && displayedMission.Completed)
{
var (share, percentage, _) = Mission.GetRewardShare(controlled.Wallet.RewardDistribution, GameSession.GetSessionCrewCharacters(CharacterType.Player).Where(c => c != controlled), Option<int>.Some(totalReward));
if (share > 0)
@@ -575,7 +575,7 @@ namespace Barotrauma
//cancel dragging if too far away from the container of the dragged item
if (DraggingItems.Any())
{
var rootContainer = DraggingItems.First().GetRootContainer();
var rootContainer = DraggingItems.First().RootContainer;
var rootInventory = DraggingItems.First().ParentInventory;
if (rootContainer != null)
@@ -155,8 +155,6 @@ namespace Barotrauma.Items.Components
convexHull.Enabled = true;
SetVertices(convexHull, rect);
}
convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom;
if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; }
}
@@ -169,12 +167,11 @@ namespace Barotrauma.Items.Components
IsHorizontal ?
new Vector2[] { new Vector2(verts[0].X, center.Y), new Vector2(verts[2].X, center.Y) } :
new Vector2[] { new Vector2(center.X, verts[0].Y), new Vector2(center.X, verts[2].Y) });
convexHull.MaxMergeLosVerticesDist = 35.0f;
}
partial void UpdateProjSpecific(float deltaTime)
{
convexHull.IsExteriorWall = !linkedGap.IsRoomToRoom;
if (convexHull2 != null) { convexHull2.IsExteriorWall = convexHull.IsExteriorWall; }
if (shakeTimer > 0.0f)
{
shakeTimer -= deltaTime;
@@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
if (character == null || !character.IsKeyDown(InputType.Aim)) { return; }
if (character == null || !character.IsKeyDown(InputType.Aim) || !character.CanAim) { return; }
//camera focused on some other item/device, don't draw the crosshair
if (character.ViewTarget != null && (character.ViewTarget is Item viewTargetItem) && viewTargetItem.Prefab.FocusOnSelected) { return; }
@@ -169,7 +169,7 @@ namespace Barotrauma.Items.Components
{
//whole text can fit in the textblock, no need to scroll
needsScrolling = false;
scrollingText = DisplayText.Value;
TextBlock.Text = scrollingText = DisplayText.Value;
scrollPadding = 0;
scrollAmount = 0.0f;
scrollIndex = 0;
@@ -73,6 +73,7 @@ namespace Barotrauma.Items.Components
Step = 0.05f,
OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
{
lastReceivedTargetForce = null;
float newTargetForce = barScroll * 200.0f - 100.0f;
if (Math.Abs(newTargetForce - targetForce) < 0.01) { return false; }
@@ -15,7 +15,7 @@ namespace Barotrauma.Items.Components
private GUIFrame selectedItemFrame;
private GUIFrame selectedItemReqsFrame;
private GUITextBlock amountTextMin, amountTextMax;
private GUITextBlock amountTextMax;
private GUIScrollBar amountInput;
public GUIButton ActivateButton
@@ -29,6 +29,9 @@ namespace Barotrauma.Items.Components
private GUIComponent outputSlot;
private GUIComponent inputInventoryHolder, outputInventoryHolder;
private readonly List<GUIButton> itemCategoryButtons = new List<GUIButton>();
private MapEntityCategory? selectedItemCategory;
public FabricationRecipe SelectedItem
{
get { return selectedItem; }
@@ -77,7 +80,67 @@ namespace Barotrauma.Items.Components
AutoScaleVertical = true
};
var mainFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), paddedFrame.RectTransform, Anchor.Center), childAnchor: Anchor.TopCenter)
var innerArea = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.95f), paddedFrame.RectTransform, Anchor.Center), isHorizontal: true)
{
RelativeSpacing = 0.01f,
Stretch = true,
CanBeFocused = true
};
List<MapEntityCategory> itemCategories = Enum.GetValues<MapEntityCategory>().ToList();
itemCategories.Remove(MapEntityCategory.None);
itemCategories.RemoveAll(c => fabricationRecipes.None(f => f.Value?.TargetItem is ItemPrefab ti && ti.Category.HasFlag(c)));
itemCategoryButtons.Clear();
//only create category buttons if there's more than one category in addition to "All"
if (itemCategories.Count > 2)
{
// === Item category buttons ===
var categoryButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(0.05f, 1.0f), innerArea.RectTransform))
{
RelativeSpacing = 0.01f
};
int buttonSize = Math.Min(categoryButtonContainer.Rect.Width, categoryButtonContainer.Rect.Height / itemCategories.Count);
var categoryButton = new GUIButton(new RectTransform(new Point(buttonSize), categoryButtonContainer.RectTransform), style: "CategoryButton.All")
{
ToolTip = TextManager.Get("MapEntityCategory.All"),
OnClicked = OnClickedCategoryButton
};
itemCategoryButtons.Add(categoryButton);
foreach (MapEntityCategory category in itemCategories)
{
categoryButton = new GUIButton(new RectTransform(new Point(buttonSize), categoryButtonContainer.RectTransform),
style: "CategoryButton." + category)
{
ToolTip = TextManager.Get("MapEntityCategory." + category),
UserData = category,
OnClicked = OnClickedCategoryButton
};
itemCategoryButtons.Add(categoryButton);
}
bool OnClickedCategoryButton(GUIButton button, object userData)
{
MapEntityCategory? newCategory = !button.Selected ? (MapEntityCategory?)userData : null;
if (newCategory.HasValue) { itemFilterBox.Text = ""; }
selectedItemCategory = newCategory;
FilterEntities(newCategory, itemFilterBox.Text);
return true;
}
foreach (var btn in itemCategoryButtons)
{
btn.RectTransform.SizeChanged += () =>
{
if (btn.Frame.sprites == null || !btn.Frame.sprites.TryGetValue(GUIComponent.ComponentState.None, out var spriteList)) { return; }
var sprite = spriteList?.First();
if (sprite == null) { return; }
btn.RectTransform.NonScaledSize = new Point(btn.Rect.Width, (int)(btn.Rect.Width * ((float)sprite.Sprite.SourceRect.Height / sprite.Sprite.SourceRect.Width)));
};
}
}
var mainFrame = new GUILayoutGroup(new RectTransform(Vector2.One, innerArea.RectTransform), childAnchor: Anchor.TopCenter)
{
RelativeSpacing = 0.02f,
Stretch = true,
@@ -105,10 +168,13 @@ namespace Barotrauma.Items.Components
Padding = Vector4.Zero,
AutoScaleVertical = true
};
itemFilterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), createClearButton: true);
itemFilterBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1.0f), filterArea.RectTransform), createClearButton: true)
{
OverflowClip = true
};
itemFilterBox.OnTextChanged += (textBox, text) =>
{
FilterEntities(text);
FilterEntities(selectedItemCategory, text);
return true;
};
filterArea.RectTransform.MaxSize = new Point(int.MaxValue, itemFilterBox.Rect.Height);
@@ -174,7 +240,7 @@ namespace Barotrauma.Items.Components
Stretch = true
};
amountTextMin = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);
new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);
amountInput = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1.0f), amountInputHolder.RectTransform), barSize: 0.1f, style: "GUISlider")
{
@@ -489,15 +555,37 @@ namespace Barotrauma.Items.Components
inputContainer.Inventory.visualSlots[slotIndex].ShowBorderHighlight(GUIStyle.Green, 0.5f, 0.5f, 0.2f);
}
var requiredItemPrefab = requiredItem.FirstMatchingPrefab;
var itemIcon = requiredItemPrefab.InventoryIcon ?? requiredItemPrefab.Sprite;
Rectangle slotRect = inputContainer.Inventory.visualSlots[slotIndex].Rect;
itemIcon.Draw(
spriteBatch,
slotRect.Center.ToVector2(),
color: requiredItemPrefab.InventoryIconColor * 0.3f,
scale: Math.Min(slotRect.Width / itemIcon.size.X, slotRect.Height / itemIcon.size.Y));
var requiredItemPrefab = requiredItem.FirstMatchingPrefab;
float iconAlpha = 0.0f;
ItemPrefab requiredItemToDisplay;
int count = requiredItem.ItemPrefabs.Count();
if (count > 1)
{
float iconCycleSpeed = 0.5f / count;
float iconCycleT = (float)Timing.TotalTime * iconCycleSpeed;
int iconIndex = (int)(iconCycleT % requiredItem.ItemPrefabs.Count());
requiredItemToDisplay = requiredItem.ItemPrefabs.Skip(iconIndex).FirstOrDefault();
iconAlpha = Math.Min(Math.Abs(MathF.Sin(iconCycleT * MathHelper.Pi)) * 2.0f, 1.0f);
}
else
{
requiredItemToDisplay = requiredItem.ItemPrefabs.FirstOrDefault();
iconAlpha = 1.0f;
}
if (iconAlpha > 0.0f)
{
var itemIcon = requiredItemToDisplay.InventoryIcon ?? requiredItemToDisplay.Sprite;
itemIcon.Draw(
spriteBatch,
slotRect.Center.ToVector2(),
color: requiredItemToDisplay.InventoryIconColor * 0.3f * iconAlpha,
scale: Math.Min(slotRect.Width * 0.9f / itemIcon.size.X, slotRect.Height * 0.9f / itemIcon.size.Y));
}
if (missingCount > 1)
{
Vector2 stackCountPos = new Vector2(slotRect.Right, slotRect.Bottom);
@@ -552,9 +640,13 @@ namespace Barotrauma.Items.Components
}
toolTipText = $"‖color:{Color.White.ToStringHex()}‖{toolTipText}‖color:end‖";
if (!requiredItemPrefab.Description.IsNullOrEmpty())
if (!requiredItem.OverrideDescription.IsNullOrEmpty())
{
toolTipText = '\n' + requiredItemPrefab.Description;
toolTipText += '\n' + requiredItem.OverrideDescription;
}
else if (!requiredItemPrefab.Description.IsNullOrEmpty())
{
toolTipText += '\n' + requiredItemPrefab.Description;
}
tooltip = new ToolTip { TargetElement = slotRect, Tooltip = toolTipText };
}
@@ -601,22 +693,21 @@ namespace Barotrauma.Items.Components
}
}
private bool FilterEntities(string filter)
private bool FilterEntities(MapEntityCategory? category, string filter)
{
if (string.IsNullOrWhiteSpace(filter))
foreach (GUIComponent child in itemList.Content.Children)
{
itemList.Content.Children.ForEach(c => c.Visible = true);
}
else
{
foreach (GUIComponent child in itemList.Content.Children)
{
FabricationRecipe recipe = child.UserData as FabricationRecipe;
if (recipe?.DisplayName == null) { continue; }
child.Visible = recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase);
}
}
FabricationRecipe recipe = child.UserData as FabricationRecipe;
if (recipe?.DisplayName == null) { continue; }
child.Visible =
(string.IsNullOrWhiteSpace(filter) || recipe.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) &&
(!category.HasValue || recipe.TargetItem.Category.HasFlag(category.Value));
}
foreach (GUIButton btn in itemCategoryButtons)
{
btn.Selected = (MapEntityCategory?)btn.UserData == selectedItemCategory;
}
HideEmptyItemListCategories();
return true;
@@ -648,7 +739,7 @@ namespace Barotrauma.Items.Components
public bool ClearFilter()
{
FilterEntities("");
FilterEntities(selectedItemCategory, "");
itemList.UpdateScrollBarSize();
itemList.BarScroll = 0.0f;
itemFilterBox.Text = "";
@@ -737,6 +828,7 @@ namespace Barotrauma.Items.Components
TextManager.Get("FabricatorRequiredSkills"), textColor: inadequateSkills.Any() ? GUIStyle.Red : GUIStyle.Green, font: GUIStyle.SubHeadingFont)
{
AutoScaleHorizontal = true,
ToolTip = TextManager.Get("fabricatorrequiredskills.tooltip")
};
foreach (Skill skill in selectedItem.RequiredSkills)
{
@@ -125,18 +125,15 @@ namespace Barotrauma.Items.Components
{
public static MiniMapSettings Default = new MiniMapSettings
(
ignoreOutposts: false,
createHullElements: true,
elementColor: MiniMap.MiniMapBaseColor
);
public readonly bool IgnoreOutposts;
public readonly bool CreateHullElements;
public readonly Color ElementColor;
public MiniMapSettings(bool ignoreOutposts = false, bool createHullElements = false, Color? elementColor = null)
public MiniMapSettings(bool createHullElements = false, Color? elementColor = null)
{
IgnoreOutposts = ignoreOutposts;
CreateHullElements = createHullElements;
ElementColor = elementColor ?? MiniMap.MiniMapBaseColor;
}
@@ -403,7 +400,8 @@ namespace Barotrauma.Items.Components
private bool VisibleOnItemFinder(Item it)
{
if (!item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; }
if (it?.Submarine == null) { return false; }
if (item.Submarine == null || !item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true)) { return false; }
if (it.NonInteractable || it.HiddenInGame) { return false; }
if (it.GetComponent<Pickable>() == null) { return false; }
@@ -436,7 +434,11 @@ namespace Barotrauma.Items.Components
prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
submarineContainer.ClearChildren();
if (item.Submarine is null) { return; }
if (item.Submarine is null)
{
displayedSubs.Clear();
return;
}
scissorComponent = new GUIScissorComponent(new RectTransform(Vector2.One, submarineContainer.RectTransform, Anchor.Center));
miniMapContainer = new GUIFrame(new RectTransform(Vector2.One, scissorComponent.Content.RectTransform, Anchor.Center), style: null) { CanBeFocused = false };
@@ -444,8 +446,8 @@ namespace Barotrauma.Items.Components
ImmutableHashSet<Item> hullPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.Prefab.ShowInStatusMonitor && (it.GetComponent<Door>() != null || it.GetComponent<Turret>() != null)).ToImmutableHashSet();
miniMapFrame = CreateMiniMap(item.Submarine, submarineContainer, MiniMapSettings.Default, hullPointsOfInterest, out hullStatusComponents);
IEnumerable<Item> electrialPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.GetComponent<Repairable>() != null);
electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electrialPointsOfInterest, out electricalMapComponents);
IEnumerable<Item> electricalPointsOfInterest = Item.ItemList.Where(it => item.Submarine.IsEntityFoundOnThisSub(it, includingConnectedSubs: true) && !it.HiddenInGame && !it.NonInteractable && it.GetComponent<Repairable>() != null);
electricalFrame = CreateMiniMap(item.Submarine, miniMapContainer, new MiniMapSettings(createHullElements: false), electricalPointsOfInterest, out electricalMapComponents);
Dictionary<MiniMapGUIComponent, GUIComponent> electricChildren = new Dictionary<MiniMapGUIComponent, GUIComponent>();
@@ -535,7 +537,7 @@ namespace Barotrauma.Items.Components
displayedSubs.Clear();
displayedSubs.Add(item.Submarine);
displayedSubs.AddRange(item.Submarine.DockedTo);
displayedSubs.AddRange(item.Submarine.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID));
subEntities = MapEntity.mapEntityList.Where(me => (item.Submarine is { } sub && sub.IsEntityFoundOnThisSub(me, includingConnectedSubs: true, allowDifferentType: false)) && !me.HiddenInGame).OrderByDescending(w => w.SpriteDepth).ToList();
@@ -550,7 +552,7 @@ namespace Barotrauma.Items.Components
item.Submarine is { } itemSub &&
(
!displayedSubs.Contains(itemSub) || // current sub not displayed
itemSub.DockedTo.Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed
itemSub.DockedTo.Where(s => s.TeamID == item.Submarine.TeamID).Any(s => !displayedSubs.Contains(s) && itemSub.ConnectedDockingPorts[s].IsLocked) || // some of the docked subs not displayed
displayedSubs.Any(s => s != itemSub && !itemSub.DockedTo.Contains(s)) // displaying a sub that shouldn't be displayed
) ||
prevResolution.X != GameMain.GraphicsWidth || prevResolution.Y != GameMain.GraphicsHeight || // resolution changed
@@ -730,7 +732,7 @@ namespace Barotrauma.Items.Components
if (sprite != null && ShowHullIntegrity)
{
Vector2 spriteSize = sprite.size;
Rectangle worldBorders = item.Submarine.GetDockedBorders();
Rectangle worldBorders = item.Submarine.GetDockedBorders(allowDifferentTeam: false);
worldBorders.Location += item.Submarine.WorldPosition.ToPoint();
foreach (Gap gap in Gap.GapList)
{
@@ -914,7 +916,7 @@ namespace Barotrauma.Items.Components
}
RectangleF dockedBorders = item.Submarine.GetDockedBorders();
RectangleF dockedBorders = item.Submarine.GetDockedBorders(allowDifferentTeam: false);
dockedBorders.Location += item.Submarine.WorldPosition;
RectangleF parentRect = miniMapFrame.Rect;
@@ -1063,7 +1065,9 @@ namespace Barotrauma.Items.Components
waterVolume += linkedHull.WaterVolume;
totalVolume += linkedHull.Volume;
}
hullData.HullWaterAmount = MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100);
hullData.HullWaterAmount =
waterVolume > 1.0f ?
MathHelper.Clamp((int)Math.Ceiling(waterVolume / totalVolume * 100), 0, 100) : 0.0f;
}
else
{
@@ -1302,7 +1306,7 @@ namespace Barotrauma.Items.Components
GameMain.Instance.GraphicsDevice.SetRenderTarget(rt);
GameMain.Instance.GraphicsDevice.Clear(Color.Transparent);
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable);
Rectangle worldBorders = sub.GetDockedBorders();
Rectangle worldBorders = sub.GetDockedBorders(allowDifferentTeam: false);
worldBorders.Location += sub.WorldPosition.ToPoint();
parentRect.Inflate(-inflate, -inflate);
@@ -1523,7 +1527,7 @@ namespace Barotrauma.Items.Components
Dictionary<MapEntity, MiniMapGUIComponent> pointsOfInterestCollection = new Dictionary<MapEntity, MiniMapGUIComponent>();
RectangleF worldBorders = sub.GetDockedBorders();
RectangleF worldBorders = sub.GetDockedBorders(allowDifferentTeam: false);
worldBorders.Location += sub.WorldPosition;
// create a container that has the same "aspect ratio" as the sub
@@ -1536,7 +1540,7 @@ namespace Barotrauma.Items.Components
GUIFrame hullContainer = new GUIFrame(new RectTransform(containerScale * elementPadding, parent.RectTransform, Anchor.Center), style: null);
ImmutableHashSet<Submarine> connectedSubs = sub.GetConnectedSubs().ToImmutableHashSet();
ImmutableHashSet<Submarine> connectedSubs = sub.GetConnectedSubs().Where(s => s.TeamID == sub.TeamID).ToImmutableHashSet();
ImmutableArray<Hull> hullList = ImmutableArray<Hull>.Empty;
ImmutableDictionary<Hull, ImmutableArray<Hull>> combinedHulls = ImmutableDictionary<Hull, ImmutableArray<Hull>>.Empty;
@@ -1683,7 +1687,7 @@ namespace Barotrauma.Items.Components
bool IsPartofSub(MapEntity entity)
{
if (entity.Submarine != sub && !connectedSubs.Contains(entity.Submarine) || entity.HiddenInGame) { return false; }
return !settings.IgnoreOutposts || sub.IsEntityFoundOnThisSub(entity, true);
return sub.IsEntityFoundOnThisSub(entity, true);
}
bool IsStandaloneHull(Hull hull)
@@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components
private const float NearbyObjectUpdateInterval = 1.0f;
float nearbyObjectUpdateTimer;
private List<Submarine> connectedSubs = new List<Submarine>();
private readonly List<Submarine> connectedSubs = new List<Submarine>();
private const float ConnectedSubUpdateInterval = 1.0f;
float connectedSubUpdateTimer;
@@ -335,9 +335,11 @@ namespace Barotrauma.Items.Components
// Setup layout for nav terminal
if (isConnectedToSteering || RightLayout)
{
controlContainer.RectTransform.AbsoluteOffset = Point.Zero;
controlContainer.RectTransform.RelativeOffset = controlBoxOffset;
controlContainer.RectTransform.SetPosition(Anchor.TopRight);
sonarView.RectTransform.ScaleBasis = ScaleBasis.Smallest;
if (HasMineralScanner) { PreventMineralScannerOverlap(); }
sonarView.RectTransform.SetPosition(Anchor.CenterLeft);
sonarView.RectTransform.Resize(GUISizeCalculation);
GUITextBlock.AutoScaleAndNormalize(textBlocksToScaleAndNormalize);
@@ -431,10 +433,11 @@ namespace Barotrauma.Items.Components
var mineralScannerFrame = new GUIFrame(new RectTransform(new Vector2(1.0f, zoomSlider.Parent.RectTransform.RelativeSize.Y), lowerAreaFrame.RectTransform, Anchor.BottomCenter), style: null);
mineralScannerSwitch = new GUIButton(new RectTransform(new Vector2(0.3f, 0.8f), mineralScannerFrame.RectTransform, Anchor.CenterLeft), string.Empty, style: "SwitchHorizontal")
{
Selected = UseMineralScanner,
OnClicked = (button, data) =>
{
useMineralScanner = !useMineralScanner;
button.Selected = useMineralScanner;
UseMineralScanner = !UseMineralScanner;
button.Selected = UseMineralScanner;
if (GameMain.Client != null)
{
unsentChanges = true;
@@ -496,12 +499,12 @@ namespace Barotrauma.Items.Components
{
if (transducer.Transducer.Item.Submarine == null) { continue; }
if (connectedSubs.Contains(transducer.Transducer.Item.Submarine)) { continue; }
connectedSubs = transducer.Transducer.Item.Submarine?.GetConnectedSubs();
connectedSubs.AddRange(transducer.Transducer.Item.Submarine.GetConnectedSubs());
}
}
else if (item.Submarine != null)
{
connectedSubs = item.Submarine?.GetConnectedSubs();
connectedSubs.AddRange(item.Submarine?.GetConnectedSubs());
}
connectedSubUpdateTimer = ConnectedSubUpdateInterval;
}
@@ -1032,7 +1035,7 @@ namespace Barotrauma.Items.Components
missionIndex++;
}
if (HasMineralScanner && useMineralScanner && CurrentMode == Mode.Active && MineralClusters != null &&
if (HasMineralScanner && UseMineralScanner && CurrentMode == Mode.Active && MineralClusters != null &&
(item.CurrentHull == null || !DetectSubmarineWalls))
{
foreach (var c in MineralClusters)
@@ -1311,7 +1314,6 @@ namespace Barotrauma.Items.Components
float worldPingRadiusSqr = worldPingRadius * worldPingRadius;
disruptedDirections.Clear();
if (Level.Loaded == null) { return; }
for (var pingIndex = 0; pingIndex < activePingsCount; ++pingIndex)
{
@@ -1513,9 +1515,10 @@ namespace Barotrauma.Items.Components
}
}
foreach (Item item in Item.ItemList)
foreach (Item item in Item.SonarVisibleItems)
{
if (item.CurrentHull == null && item.Prefab.SonarSize > 0.0f)
System.Diagnostics.Debug.Assert(item.Prefab.SonarSize > 0.0f);
if (item.CurrentHull == null)
{
float pointDist = ((item.WorldPosition - pingSource) * displayScale).LengthSquared();
if (pointDist > prevPingRadiusSqr && pointDist < pingRadiusSqr)
@@ -1923,7 +1926,7 @@ namespace Barotrauma.Items.Components
float pingAngle = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(pingDirection));
msg.WriteRangedSingle(MathUtils.InverseLerp(0.0f, MathHelper.TwoPi, pingAngle), 0.0f, 1.0f, 8);
}
msg.WriteBoolean(useMineralScanner);
msg.WriteBoolean(UseMineralScanner);
}
}
@@ -1935,7 +1938,7 @@ namespace Barotrauma.Items.Components
float zoomT = 1.0f;
bool directionalPing = useDirectionalPing;
float directionT = 0.0f;
bool mineralScanner = useMineralScanner;
bool mineralScanner = UseMineralScanner;
if (isActive)
{
zoomT = msg.ReadRangedSingle(0.0f, 1.0f, 8);
@@ -1966,7 +1969,7 @@ namespace Barotrauma.Items.Components
pingDirection = new Vector2((float)Math.Cos(pingAngle), (float)Math.Sin(pingAngle));
}
useDirectionalPing = directionalModeSwitch.Selected = directionalPing;
useMineralScanner = mineralScanner;
UseMineralScanner = mineralScanner;
if (mineralScannerSwitch != null)
{
mineralScannerSwitch.Selected = mineralScanner;
@@ -1983,7 +1986,7 @@ namespace Barotrauma.Items.Components
directionalModeSwitch.Selected = useDirectionalPing;
if (mineralScannerSwitch != null)
{
mineralScannerSwitch.Selected = useMineralScanner;
mineralScannerSwitch.Selected = UseMineralScanner;
}
}
}
@@ -20,7 +20,7 @@ namespace Barotrauma.Items.Components
User = Entity.FindEntityByID(userId) as Character;
Vector2 simPosition = new Vector2(msg.ReadSingle(), msg.ReadSingle());
float rotation = msg.ReadSingle();
SpreadCounter = msg.ReadByte();
spreadIndex = msg.ReadByte();
if (User != null)
{
Shoot(User, simPosition, simPosition, rotation, ignoredBodies: User.AnimController.Limbs.Where(l => !l.IsSevered).Select(l => l.body.FarseerBody).ToList(), createNetworkEvent: false);
@@ -21,7 +21,8 @@ namespace Barotrauma.Items.Components
public float FlashTimer { get; private set; }
public static Wire DraggingConnected { get; private set; }
public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Rectangle dragArea, Character character)
public static void DrawConnections(SpriteBatch spriteBatch, ConnectionPanel panel, Rectangle dragArea, Character character,
out (Vector2 tooltipPos, LocalizedString text) tooltip)
{
if (DraggingConnected?.Item?.Removed ?? true)
{
@@ -64,6 +65,8 @@ namespace Barotrauma.Items.Components
}
}
tooltip = (Vector2.Zero, string.Empty);
//two passes: first the connector, then the wires to get the wires to render in front
for (int i = 0; i < 2; i++)
{
@@ -97,6 +100,42 @@ namespace Barotrauma.Items.Components
}
}
Vector2 position = c.IsOutput ? rightPos : leftPos;
Color highlightColor = Color.Transparent;
if (ConnectionPanel.ShouldDebugDrawWiring)
{
if (c.IsPower)
{
highlightColor = VisualizeSignal(0.0f, highlightColor, Color.Red);
}
else
{
highlightColor = VisualizeSignal(c.LastReceivedSignal.TimeSinceCreated, highlightColor, Color.LightGreen);
highlightColor = VisualizeSignal(c.LastSentSignal.TimeSinceCreated, highlightColor, Color.Orange);
}
bool mouseOn = Vector2.DistanceSquared(position, PlayerInput.MousePosition) < MathUtils.Pow2(35 * GUI.Scale);
LocalizedString toolTipText = c.GetToolTip();
if (mouseOn) { tooltip = (position, toolTipText); }
if (!toolTipText.IsNullOrEmpty())
{
var glowSprite = GUIStyle.UIGlowCircular.Value.Sprite;
glowSprite.Draw(spriteBatch, position, highlightColor, glowSprite.size / 2,
scale: 45.0f / glowSprite.size.X * panel.Scale);
}
}
static Color VisualizeSignal(double timeSinceCreated, Color defaultColor, Color color)
{
if (timeSinceCreated < 1.0f)
{
float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * 10.0f) + 3.0f) / 4.0f;
Color targetColor = Color.Lerp(defaultColor, color, pulseAmount);
return Color.Lerp(targetColor, defaultColor, (float)timeSinceCreated);
}
return defaultColor;
}
//outputs are drawn at the right side of the panel, inputs at the left
if (c.IsOutput)
{
@@ -127,7 +166,6 @@ namespace Barotrauma.Items.Components
}
}
if (DraggingConnected != null)
{
if (mouseInRect)
@@ -225,7 +263,9 @@ namespace Barotrauma.Items.Components
GUI.DrawString(spriteBatch, labelPos, text, GUIStyle.TextColorBright, font: GUIStyle.SmallFont);
float connectorSpriteScale = (35.0f / connectionSprite.SourceRect.Width) * panel.Scale;
connectionSprite.Draw(spriteBatch, position, scale: connectorSpriteScale);
}
private void DrawWires(SpriteBatch spriteBatch, ConnectionPanel panel, Vector2 position, Vector2 wirePosition, bool mouseIn, Wire equippedWire, float wireInterval)
@@ -307,6 +347,63 @@ namespace Barotrauma.Items.Components
FlashTimer -= deltaTime;
}
private (string signal, LocalizedString tooltip) lastSignalToolTip;
private (int powerValue, LocalizedString tooltip) lastPowerToolTip;
private LocalizedString GetToolTip()
{
if (LastReceivedSignal.TimeSinceCreated < 1.0f)
{
return getSignalTooltip(LastReceivedSignal, "receivedsignal");
}
else if (LastSentSignal.TimeSinceCreated < 1.0f)
{
return getSignalTooltip(LastSentSignal, "sentsignal");
}
LocalizedString getSignalTooltip(Signal signal, string textTag)
{
if (lastSignalToolTip.signal == signal.value && !lastSignalToolTip.tooltip.IsNullOrEmpty()) { return lastSignalToolTip.tooltip; }
lastSignalToolTip = (signal.value, TextManager.GetWithVariable(textTag, "[signal]", signal.value));
return lastSignalToolTip.tooltip;
}
if (IsPower)
{
if (item.GetComponent<Powered>() is Powered powered)
{
if (IsOutput)
{
if (powered.CurrPowerConsumption < 0)
{
return getPowerTooltip(-(int)powered.CurrPowerConsumption, "reactoroutput");
}
else if (powered is PowerTransfer || powered is PowerContainer)
{
return getPowerTooltip((int)(Grid?.Power ?? 0), "reactoroutput");
}
}
else if (!IsOutput)
{
float powerConsumption = powered.GetCurrentPowerConsumption(this);
if (!MathUtils.NearlyEqual((int)powerConsumption, 0.0f))
{
return getPowerTooltip((int)powerConsumption, "reactorload");
}
}
}
}
LocalizedString getPowerTooltip(int powerValue, string textTag)
{
if (lastPowerToolTip.powerValue == powerValue && !lastPowerToolTip.tooltip.IsNullOrEmpty()) { return lastPowerToolTip.tooltip; }
lastPowerToolTip = (powerValue, TextManager.GetWithVariable(textTag, "[kw]", powerValue.ToString()));
return lastPowerToolTip.tooltip;
}
return null;
}
private static void DrawWire(SpriteBatch spriteBatch, Wire wire, Vector2 end, Vector2 start, Wire equippedWire, ConnectionPanel panel, LocalizedString label)
{
int textX = (int)start.X;
@@ -10,6 +10,10 @@ namespace Barotrauma.Items.Components
{
partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable
{
public static bool DebugWiringMode;
public static double DebugWiringEnabledUntil;
public static bool ShouldDebugDrawWiring => DebugWiringMode || Timing.TotalTimeUnpaused < DebugWiringEnabledUntil;
//how long the rewiring sound plays after doing changes to the wiring
const float RewireSoundDuration = 5.0f;
@@ -120,12 +124,15 @@ namespace Barotrauma.Items.Components
if (user != Character.Controlled || user == null) { return; }
HighlightedWire = null;
Connection.DrawConnections(spriteBatch, this, dragArea.Rect, user);
Connection.DrawConnections(spriteBatch, this, dragArea.Rect, user, out (Vector2 tooltipPos, LocalizedString text) tooltip);
foreach (UISprite sprite in GUIStyle.GetComponentStyle("ConnectionPanelFront").Sprites[GUIComponent.ComponentState.None])
{
sprite.Draw(spriteBatch, GuiFrame.Rect, Color.White, SpriteEffects.None);
}
if (!tooltip.text.IsNullOrEmpty())
{
GUIComponent.DrawToolTip(spriteBatch, tooltip.text, tooltip.tooltipPos);
}
}
private void CheckForLabelOverlap()
@@ -250,7 +250,7 @@ namespace Barotrauma.Items.Components
int visibleElementCount = 0;
foreach (var uiElement in uiElements)
{
if (!(uiElement.UserData is CustomInterfaceElement element)) { continue; }
if (uiElement.UserData is not CustomInterfaceElement element) { continue; }
bool visible = Screen.Selected == GameMain.SubEditorScreen || element.StatusEffects.Any() || element.HasPropertyName || (element.Connection != null && element.Connection.Wires.Count > 0);
if (visible) { visibleElementCount++; }
if (uiElement.Visible != visible)
@@ -337,7 +337,9 @@ namespace Barotrauma.Items.Components
{
if (uiElements[i] is GUITextBox tb)
{
tb.Text = customInterfaceElementList[i].Signal;
tb.Text = Screen.Selected is { IsEditor: true } ?
customInterfaceElementList[i].Signal :
TextManager.Get(customInterfaceElementList[i].Signal).Value;
}
else if (uiElements[i] is GUINumberInput ni)
{
@@ -5,7 +5,6 @@ using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -119,6 +118,13 @@ namespace Barotrauma.Items.Components
get { return sectionExtents; }
}
public readonly record struct VisualSignal(
float TimeSent,
Color Color,
int Direction);
private VisualSignal lastReceivedSignal;
public static Wire DraggingWire
{
get => draggingWire;
@@ -126,13 +132,11 @@ namespace Barotrauma.Items.Components
public static Sprite ExtractWireSprite(ContentXElement element)
{
if (defaultWireSprite == null)
{
defaultWireSprite = new Sprite("Content/Items/Electricity/signalcomp.png", new Rectangle(970, 47, 14, 16), new Vector2(0.5f, 0.5f))
defaultWireSprite ??=
new Sprite("Content/Items/Electricity/signalcomp.png", new Rectangle(970, 47, 14, 16), new Vector2(0.5f, 0.5f))
{
Depth = 0.855f
};
}
Sprite overrideSprite = null;
foreach (var subElement in element.Elements())
@@ -153,6 +157,35 @@ namespace Barotrauma.Items.Components
if (wireSprite != defaultWireSprite) { overrideSprite = wireSprite; }
}
public void RegisterSignal(Signal signal, Connection source)
{
lastReceivedSignal = new VisualSignal(
(float)Timing.TotalTimeUnpaused,
GetSignalColor(signal),
Direction: source == connections[0] ? 1 : -1);
}
private static readonly Color[] dataSignalColors = new Color[] { Color.White, Color.LightBlue, Color.CornflowerBlue, Color.Blue, Color.BlueViolet, Color.Violet };
private static Color GetSignalColor(Signal signal)
{
if (signal.value == "0")
{
return Color.Red;
}
else if (signal.value == "1")
{
return Color.LightGreen;
}
else if (float.TryParse(signal.value, out float floatValue))
{
//convert numeric values to a color (guessing the value might be somewhere in the range of 0-200)
//so a player with a keen eye can get some info out of the color of the signal
return ToolBox.GradientLerp(Math.Abs(floatValue / 200.0f), dataSignalColors);
}
return Color.LightBlue;
}
public void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1)
{
Draw(spriteBatch, editing, Vector2.Zero, itemDepth);
@@ -166,20 +199,7 @@ namespace Barotrauma.Items.Components
return;
}
Vector2 drawOffset = Vector2.Zero;
Submarine sub = item.Submarine;
if (IsActive && sub == null) // currently being rewired, we need to get the sub from the connections in case the wire has been taken outside
{
if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; }
if (connections[1] != null && connections[1].Item.Submarine != null) { sub = connections[1].Item.Submarine; }
}
if (sub != null)
{
drawOffset = sub.DrawPosition + sub.HiddenSubPosition;
}
drawOffset += offset;
Vector2 drawOffset = GetDrawOffset() + offset;
float baseDepth = UseSpriteDepth ? item.SpriteDepth : wireSprite.Depth;
float depth = item.IsSelected ? 0.0f : SubEditorScreen.IsWiringMode() ? 0.02f : baseDepth + (item.ID % 100) * 0.000001f;// item.GetDrawDepth(wireSprite.Depth, wireSprite);
@@ -188,7 +208,9 @@ namespace Barotrauma.Items.Components
{
foreach (WireSection section in sections)
{
section.Draw(spriteBatch, wireSprite, Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor, drawOffset, depth + 0.00001f, Width * 2.0f);
section.Draw(spriteBatch, wireSprite,
Screen.Selected == GameMain.GameScreen ? higlightColor : editorHighlightColor,
drawOffset, depth + 0.00001f, Width * 2.0f);
}
}
else if (item.IsSelected)
@@ -270,6 +292,12 @@ namespace Barotrauma.Items.Components
}
}
if (ConnectionPanel.ShouldDebugDrawWiring)
{
DebugDraw(spriteBatch, alpha: 0.2f);
}
if (!editing || !GameMain.SubEditorScreen.WiringMode) { return; }
for (int i = 0; i < nodes.Count; i++)
@@ -295,6 +323,102 @@ namespace Barotrauma.Items.Components
}
}
public void DebugDraw(SpriteBatch spriteBatch, float alpha = 1.0f)
{
if (sections.Count == 0 || Hidden)
{
return;
}
const float PowerPulseSpeedLow = 5.0f;
const float PowerPulseSpeedHigh = 10.0f;
const float PowerHighlightScaleLow = 1.5f;
const float PowerHighlightScaleHigh = 2.5f;
const float SignalIndicatorInterval = 15.0f;
const float SignalIndicatorSpeed = 100.0f;
Vector2 drawOffset = GetDrawOffset();
Color currentHighlightColor = Color.Transparent;
float highlightScale = 0.0f;
if (connections[0] != null && connections[1] != null)
{
float voltage = Math.Max(GetVoltage(0), GetVoltage(1));
float GetVoltage(int connectionIndex)
{
var connection1 = connections[connectionIndex];
var connection2 = connections[1 - connectionIndex];
if (connection1.IsOutput && connection1.Grid is { Power: > 0.01f } grid1)
{
if (connection2.Item.GetComponent<Powered>() is Powered powered &&
(powered.GetCurrentPowerConsumption(connection2) > 0 || powered is PowerTransfer))
{
return grid1.Voltage;
}
}
return 0.0f;
}
if (voltage > 0.0f)
{
//pulse faster when there's overvoltage
float pulseSpeed = voltage > 1.2f ? PowerPulseSpeedHigh : PowerPulseSpeedLow;
float pulseAmount = (MathF.Sin((float)Timing.TotalTimeUnpaused * pulseSpeed) + 1.5f) / 2.5f;
voltage = Math.Min(voltage, 1.0f);
highlightScale = MathHelper.Lerp(PowerHighlightScaleLow, PowerHighlightScaleHigh, voltage);
currentHighlightColor = Color.Red * voltage * pulseAmount;
}
}
if (highlightScale > 0.0f)
{
foreach (WireSection section in sections)
{
section.Draw(spriteBatch, wireSprite, currentHighlightColor * alpha, drawOffset, 0.0f, Width * highlightScale);
}
}
float signalDuration = (float)Timing.TotalTimeUnpaused - lastReceivedSignal.TimeSent;
if (ConnectionPanel.ShouldDebugDrawWiring && signalDuration < 1.0f)
{
//make some wires "off sync" so it's easier to differentiate signals on overlapping wires
float offset = item.ID % 2 == 1 ? SignalIndicatorInterval / 2 : 0.0f;
float signalProgress = ((float)(Timing.TotalTimeUnpaused * SignalIndicatorSpeed + offset) % SignalIndicatorInterval) * lastReceivedSignal.Direction;
foreach (WireSection section in sections)
{
for (float x = 0; x < section.Length; x += SignalIndicatorInterval)
{
Vector2 dir = (section.End - section.Start) / section.Length;
float posOnSection = x + signalProgress;
if (posOnSection < 0 || posOnSection > section.Length) { continue; }
Vector2 signalPos = section.Start + drawOffset + dir * posOnSection;
float a = 1.0f - Vector2.Distance(Screen.Selected.Cam.WorldViewCenter, signalPos) / 500.0f;
if (a < 0) { continue; }
signalPos.Y = -signalPos.Y;
GUI.DrawRectangle(spriteBatch, signalPos - Vector2.One * 2.5f, Vector2.One * 5, lastReceivedSignal.Color * a * (1.0f - signalDuration) * alpha, isFilled: true);
}
}
}
}
private Vector2 GetDrawOffset()
{
Submarine sub = item.Submarine;
if (IsActive && sub == null) // currently being rewired, we need to get the sub from the connections in case the wire has been taken outside
{
if (connections[0] != null && connections[0].Item.Submarine != null) { sub = connections[0].Item.Submarine; }
if (connections[1] != null && connections[1].Item.Submarine != null) { sub = connections[1].Item.Submarine; }
}
if (sub == null)
{
return Vector2.Zero;
}
else
{
return sub.DrawPosition + sub.HiddenSubPosition;
}
}
private void DrawHangingWire(SpriteBatch spriteBatch, Vector2 start, float depth)
{
float angle = (float)Math.Sin(GameMain.GameScreen.GameTime * 2.0f + item.ID) * 0.2f;
@@ -46,6 +46,13 @@ namespace Barotrauma.Items.Components
private set;
}
[Serialize(false, IsPropertySaveable.No)]
public bool DebugWiring
{
get;
private set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool ShowDeadCharacters
{
@@ -111,6 +118,11 @@ namespace Barotrauma.Items.Components
refEntity = item;
}
if (equipper != null && equipper == Character.Controlled && DebugWiring)
{
ConnectionPanel.DebugWiringEnabledUntil = Timing.TotalTimeUnpaused + 0.5;
}
thermalEffectState += deltaTime;
thermalEffectState %= 10000.0f;
@@ -153,6 +165,11 @@ namespace Barotrauma.Items.Components
IsActive = false;
}
public override void Drop(Character dropper, bool setTransform = true)
{
Unequip(dropper);
}
public override void DrawHUD(SpriteBatch spriteBatch, Character character)
{
if (character == null) { return; }
@@ -1,4 +1,5 @@
using Barotrauma.Networking;
using Barotrauma.Lights;
using Barotrauma.Networking;
using FarseerPhysics;
using FarseerPhysics.Collision;
using Microsoft.Xna.Framework;
@@ -10,6 +11,8 @@ namespace Barotrauma.Items.Components
{
private GUIMessageBox autodockingVerification;
private readonly ConvexHull[] convexHulls = new ConvexHull[2];
public Vector2 DrawSize
{
//use the extents of the item as the draw size
@@ -109,6 +112,15 @@ namespace Barotrauma.Items.Components
}
}
partial void RemoveConvexHulls()
{
for (int i = 0; i < convexHulls.Length; i++)
{
convexHulls[i]?.Remove();
convexHulls[i] = null;
}
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
bool isDocked = msg.ReadBoolean();
@@ -1398,7 +1398,7 @@ namespace Barotrauma
}
else
{
throw new Exception("Failed to read component state - " + components[componentIndex].GetType() + " is not IServerSerializable.");
throw new Exception($"Failed to read component state - {components[componentIndex].GetType()} in item \"{Prefab.Identifier}\" is not IServerSerializable.");
}
}
break;
@@ -1411,7 +1411,7 @@ namespace Barotrauma
}
else
{
throw new Exception("Failed to read inventory state - " + components[containerIndex].GetType() + " is not an ItemContainer.");
throw new Exception($"Failed to read inventory state - {components[containerIndex].GetType()} in item \"{Prefab.Identifier}\" is not an ItemContainer.");
}
}
break;
@@ -1460,9 +1460,9 @@ namespace Barotrauma
byte length = msg.ReadByte();
for (int i = 0; i < length; i++)
{
var statIdentifier = INetSerializableStruct.Read<ItemStatManager.TalentStatIdentifier>(msg);
var statIdentifier = INetSerializableStruct.Read<TalentStatIdentifier>(msg);
var statValue = msg.ReadSingle();
StatManager.ApplyStat(statIdentifier, statValue);
StatManager.ApplyStatDirect(statIdentifier, statValue);
}
break;
case EventType.Upgrade:
@@ -9,15 +9,17 @@ namespace Barotrauma
{
private LightSource lightSource;
partial void UpdateProjSpecific(float growModifier)
private float particleTimer;
partial void UpdateProjSpecific(float growModifier, float deltaTime)
{
if (this is DummyFireSource)
{
EmitParticles(size, WorldPosition, hull, growModifier, null);
EmitParticles(size, WorldPosition, deltaTime, hull, growModifier, null);
}
else
{
EmitParticles(size, WorldPosition, hull, growModifier, OnChangeHull);
EmitParticles(size, WorldPosition, deltaTime, hull, growModifier, OnChangeHull);
}
lightSource.Color = new Color(1.0f, 0.45f, 0.3f) * Rand.Range(0.8f, 1.0f);
@@ -25,23 +27,29 @@ namespace Barotrauma
if (Vector2.DistanceSquared(lightSource.Position, position) > 5.0f) { lightSource.Position = position + Vector2.UnitY * 30.0f; }
}
public static void EmitParticles(Vector2 size, Vector2 worldPosition, Hull hull, float growModifier, Particle.OnChangeHullHandler onChangeHull = null)
public void EmitParticles(Vector2 size, Vector2 worldPosition, float deltaTime, Hull hull, float growModifier, Particle.OnChangeHullHandler onChangeHull = null)
{
float particleCount = Rand.Range(0.0f, size.X / 50.0f);
var particlePrefab = ParticleManager.FindPrefab("flame");
if (particlePrefab == null) { return; }
for (int i = 0; i < particleCount; i++)
float particlesPerSecond = MathHelper.Clamp(size.X / 2.0f, 10.0f, 200.0f);
float particleInterval = 1.0f / particlesPerSecond;
particleTimer += deltaTime;
while (particleTimer > particleInterval)
{
particleTimer -= particleInterval;
Vector2 particlePos = new Vector2(
worldPosition.X + Rand.Range(0.0f, size.X),
Rand.Range(worldPosition.Y - size.Y, worldPosition.Y + 20.0f));
worldPosition.Y - size.Y + particlePrefab.CollisionRadius);
Vector2 particleVel = new Vector2(
particlePos.X - (worldPosition.X + size.X / 2.0f),
Math.Max((float)Math.Sqrt(size.X) * Rand.Range(0.0f, 15.0f) * growModifier, 0.0f));
particleVel.X = MathHelper.Clamp(particleVel.X, -200.0f, 200.0f);
var particle = GameMain.ParticleManager.CreateParticle("flame",
var particle = GameMain.ParticleManager.CreateParticle(particlePrefab,
particlePos, particleVel, 0.0f, hull);
if (particle == null) { continue; }
@@ -54,7 +62,7 @@ namespace Barotrauma
if (Rand.Int(5) == 1)
{
var smokeParticle = GameMain.ParticleManager.CreateParticle("smoke",
particlePos, new Vector2(particleVel.X, particleVel.Y * 0.1f), 0.0f, hull);
particlePos, new Vector2(particleVel.X, particleVel.Y * 0.1f), 0.0f, hull);
if (smokeParticle != null)
{
@@ -27,7 +27,7 @@ namespace Barotrauma
{
foreach (var edge in cell.Edges)
{
if (MathUtils.GetLineIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection))
if (MathUtils.GetLineSegmentIntersection(worldPosition, cell.Center, edge.Point1 + cell.Translation, edge.Point2 + cell.Translation, out Vector2 intersection))
{
intersectionFound = true;
particlePos = intersection;
@@ -93,13 +93,16 @@ namespace Barotrauma.Lights
private readonly int thickness;
public bool IsExteriorWall;
public VertexPositionColor[] ShadowVertices { get; private set; }
public VertexPositionTexture[] PenumbraVertices { get; private set; }
public int ShadowVertexCount { get; private set; }
public int PenumbraVertexCount { get; private set; }
/// <summary>
/// Overrides the maximum distance a LOS vertex can be moved to make it align with a nearby LOS segment
/// </summary>
public float? MaxMergeLosVerticesDist;
private readonly HashSet<ConvexHull> overlappingHulls = new HashSet<ConvexHull>();
public MapEntity ParentEntity { get; private set; }
@@ -130,7 +133,7 @@ namespace Barotrauma.Lights
public Rectangle BoundingBox { get; private set; }
public ConvexHull(Rectangle rect, bool? isHorizontal, MapEntity parent)
public ConvexHull(Rectangle rect, bool isHorizontal, MapEntity parent)
{
shadowEffect ??= new BasicEffect(GameMain.Instance.GraphicsDevice)
{
@@ -150,15 +153,15 @@ namespace Barotrauma.Lights
BoundingBox = rect;
this.isHorizontal = isHorizontal ?? BoundingBox.Width > BoundingBox.Height;
this.isHorizontal = isHorizontal;
if (ParentEntity is Structure structure)
{
System.Diagnostics.Debug.Assert(!structure.Removed);
Debug.Assert(!structure.Removed);
isHorizontal = structure.IsHorizontal;
}
else if (ParentEntity is Item item)
{
System.Diagnostics.Debug.Assert(!item.Removed);
Debug.Assert(!item.Removed);
var door = item.GetComponent<Door>();
if (door != null) { isHorizontal = door.IsHorizontal; }
}
@@ -205,44 +208,97 @@ namespace Barotrauma.Lights
{
if (ch == this) { return; }
//hide segments that are roughly at the some position as some other segment (e.g. the ends of two adjacent wall pieces)
float mergeDist = MathHelper.Clamp(ch.thickness * 0.55f, 16, 512);
mergeDist = Math.Min(mergeDist, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) / 2);
//merge dist in the direction parallel to the segment
//(e.g. how far up/down we can stretch a vertical segment)
float mergeDistParallel = MathHelper.Clamp(ch.thickness * 0.65f, 16, 512);
if (MaxMergeLosVerticesDist.HasValue)
{
mergeDistParallel = Math.Max(mergeDistParallel, MaxMergeLosVerticesDist.Value);
}
else
{
Rectangle inflatedAABB = ch.BoundingBox;
inflatedAABB.Inflate(2, 2);
//if this los segment isn't touching the other's bounding box,
//don't extend the segment by more than 50% of it's length
if (!inflatedAABB.Contains(losVertices[0].Pos) &&
!inflatedAABB.Contains(losVertices[1].Pos))
{
mergeDistParallel = Math.Min(mergeDistParallel, Vector2.Distance(losVertices[0].Pos, losVertices[1].Pos) * 0.5f);
}
}
//merge dist in the direction perpendicular to the segment
//(e.g. how far right/left we can stretch a vertical segment)
//do not allow more than ~half of the thickness, because that'd make the segment go outside the convex hull
float mergeDistPerpendicular = Math.Min(mergeDistParallel, thickness * 0.35f);
float mergeDistSqr = mergeDist * mergeDist;
Vector2 center = (losVertices[0].Pos + losVertices[1].Pos) / 2;
bool changed = false;
for (int i = 0; i < losVertices.Length; i++)
{
//find the closest point on the other convex hull segment
Vector2 segmentDir = Vector2.Normalize(losVertices[i].Pos - center);
//check if the closest point on the other convex hull segment is close enough, disregarding any offsets
//otherwise we might end up moving the vertex too much if we stretch it to an already-offset segment
if (!isCloseEnough(
MathUtils.GetClosestPointOnLineSegment(ch.losVertices[0].Pos, ch.losVertices[1].Pos, losVertices[i].Pos),
losVertices[i].Pos))
{
continue;
}
//check the offset position of the segment next
Vector2 closest = MathUtils.GetClosestPointOnLineSegment(
ch.losVertices[0].Pos + ch.losOffsets[0],
ch.losVertices[1].Pos + ch.losOffsets[1],
losVertices[i].Pos);
if (Vector2.DistanceSquared(closest, losVertices[i].Pos) > mergeDistSqr) { continue; }
if (!isCloseEnough(closest, losVertices[i].Pos)) { continue; }
//find where the segments would intersect if they had infinite length
// if it's close to the closest point, let's use that instead to keep
// the direction of the segment unchanged (i.e. vertical segment stays vertical)
if (MathUtils.GetLineIntersection(
ch.losVertices[0].Pos + ch.losOffsets[0],
ch.losVertices[1].Pos + ch.losOffsets[1],
losVertices[0].Pos,
losVertices[1].Pos,
out Vector2 intersection))
ch.losVertices[0].Pos + ch.losOffsets[0], ch.losVertices[1].Pos + ch.losOffsets[1],
losVertices[0].Pos, losVertices[1].Pos,
areLinesInfinite: true, out Vector2 intersection) &&
//the intersection needs to be outwards from the vertex we're checking
Vector2.Dot(segmentDir, intersection - losVertices[i].Pos) > 0 &&
//the intersection needs to be close enough to the default position of the vertex and the closest point
//(we don't want to merge the segments somewhere close to infinity!)
(Vector2.DistanceSquared(intersection, losVertices[i].Pos) < mergeDistParallel * mergeDistParallel ||
Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f))
{
if (Vector2.DistanceSquared(intersection, losVertices[i].Pos) < mergeDistSqr ||
Vector2.DistanceSquared(intersection, closest) < 16.0f * 16.0f)
{
closest = intersection;
}
closest = intersection;
}
//don't move the vertices of the segment too close to each other
if (Vector2.DistanceSquared(losVertices[1 - i].Pos + losOffsets[1 - i], closest) < mergeDistPerpendicular * mergeDistPerpendicular)
{
continue;
}
losOffsets[i] = closest - losVertices[i].Pos;
overlappingHulls.Add(ch);
ch.overlappingHulls.Add(this);
changed = true;
bool isCloseEnough(Vector2 closest, Vector2 vertex)
{
float dist = Vector2.Distance(closest, vertex);
if (dist < 0.001f) { return true; }
if (dist > mergeDistParallel) { return false; }
Vector2 closestDir = (closest - vertex) / dist;
float dot = Math.Abs(Vector2.Dot(segmentDir, closestDir));
float distAlongAxis = dist * dot;
if (distAlongAxis > mergeDistParallel) { return false; }
float distPerpendicular = dist * (1.0f - dot);
if (distPerpendicular > mergeDistPerpendicular) { return false; }
return true;
}
}
if (changed && refreshOtherOverlappingHulls)
{
@@ -253,6 +309,13 @@ namespace Barotrauma.Lights
}
}
public bool LosIntersects(Vector2 pos1, Vector2 pos2)
{
return MathUtils.LineSegmentsIntersect(
losVertices[0].Pos + losOffsets[0], losVertices[1].Pos + losOffsets[1],
pos1, pos2);
}
public void Rotate(Vector2 origin, float amount)
{
Matrix rotationMatrix =
@@ -347,6 +410,7 @@ namespace Barotrauma.Lights
for (int i = 0; i < 2; i++)
{
losVertices[i] = new SegmentPoint(losPoints[i], this);
losOffsets[i] = Vector2.Zero;
}
overlappingHulls.Clear();
@@ -612,8 +676,11 @@ namespace Barotrauma.Lights
vertexPos0 += ParentEntity.Submarine.DrawPosition;
vertexPos1 += ParentEntity.Submarine.DrawPosition;
}
Vector2 viewTargetPos = LightManager.ViewTarget.WorldPosition;
float alpha = IsSegmentFacing(vertexPos0, vertexPos1, viewTargetPos) ? 1.0f : 0.5f;
float alpha = 1.0f;
if (LightManager.ViewTarget != null)
{
alpha = IsSegmentFacing(vertexPos0, vertexPos1, LightManager.ViewTarget.WorldPosition) ? 1.0f : 0.5f;
}
vertexPos0.Y = -vertexPos0.Y;
vertexPos1.Y = -vertexPos1.Y;
GUI.DrawLine(spriteBatch, vertexPos0, vertexPos1, color * alpha, width: width);
@@ -189,7 +189,7 @@ namespace Barotrauma.Lights
}
}
private class RayCastTask
private sealed class RayCastTask
{
public LightSource LightSource;
public Vector2 DrawPos;
@@ -298,7 +298,8 @@ namespace Barotrauma.Lights
range *
((Character.Controlled?.Submarine != null && light.ParentSub == Character.Controlled?.Submarine) ? 2.0f : 1.0f) *
(light.CastShadows ? 10.0f : 1.0f) *
(light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f));
(light.LightSourceParams.OverrideLightSpriteAlpha ?? (light.Color.A / 255.0f)) *
light.PriorityMultiplier;
}
//find the lights with an active light volume
@@ -477,6 +478,17 @@ namespace Barotrauma.Lights
light.DrawLightVolume(spriteBatch, lightEffect, transform, recalculationCount < MaxLightVolumeRecalculationsPerFrame, ref recalculationCount);
}
if (ConnectionPanel.ShouldDebugDrawWiring)
{
foreach (MapEntity e in (Submarine.VisibleEntities ?? MapEntity.mapEntityList))
{
if (e is Item item && item.GetComponent<Wire>() is Wire wire)
{
wire.DebugDraw(spriteBatch, alpha: 0.4f);
}
}
}
lightEffect.World = transform;
GameMain.ParticleManager.Draw(spriteBatch, false, null, Particles.ParticleBlendState.Additive);
@@ -686,19 +698,42 @@ namespace Barotrauma.Lights
if (LosEnabled && LosMode != LosMode.None && ViewTarget != null)
{
Vector2 pos = ViewTarget.DrawPosition;
if (ViewTarget is Character character &&
bool centeredOnHead = false;
if (ViewTarget is Character character &&
character.AnimController?.GetLimb(LimbType.Head) is Limb head &&
!head.IsSevered && !head.Removed)
{
pos = head.body.DrawPosition;
centeredOnHead = true;
}
Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height);
Matrix shadowTransform = cam.ShaderTransform
* Matrix.CreateOrthographic(GameMain.GraphicsWidth, GameMain.GraphicsHeight, -1, 1) * 0.5f;
var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget.Submarine);
//make sure the head isn't peeking through any LOS segments, and if it is,
//center the LOS on the character's collider instead
if (centeredOnHead)
{
foreach (var ch in convexHulls)
{
Vector2 currentViewPos = pos;
Vector2 defaultViewPos = ViewTarget.DrawPosition;
if (ch.ParentEntity?.Submarine != null)
{
defaultViewPos -= ch.ParentEntity.Submarine.DrawPosition;
currentViewPos -= ch.ParentEntity.Submarine.DrawPosition;
}
//check if a line from the character's collider to the head intersects with the los segment (= head poking through it)
if (ch.LosIntersects(defaultViewPos, currentViewPos))
{
pos = ViewTarget.DrawPosition;
}
}
}
if (convexHulls != null)
{
List<VertexPositionColor> shadowVerts = new List<VertexPositionColor>();
@@ -706,7 +741,6 @@ namespace Barotrauma.Lights
foreach (ConvexHull convexHull in convexHulls)
{
if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; }
if (LosMode == LosMode.BlockOutsideView && !convexHull.IsExteriorWall) { continue; };
Vector2 relativeLightPos = pos;
if (convexHull.ParentEntity?.Submarine != null) { relativeLightPos -= convexHull.ParentEntity.Submarine.Position; }
@@ -744,13 +778,14 @@ namespace Barotrauma.Lights
public void DebugDrawLos(SpriteBatch spriteBatch, Camera cam)
{
if (ViewTarget == null) { return; }
Vector2 pos = ViewTarget?.Position ?? cam.Position;
spriteBatch.Begin(SpriteSortMode.Deferred, transformMatrix: cam.Transform);
var convexHulls = ConvexHull.GetHullsInRange(ViewTarget.Position, cam.WorldView.Width * 0.75f, ViewTarget?.Submarine);
var convexHulls = ConvexHull.GetHullsInRange(pos, cam.WorldView.Width * 0.75f, ViewTarget?.Submarine);
Rectangle camView = new Rectangle(cam.WorldView.X, cam.WorldView.Y - cam.WorldView.Height, cam.WorldView.Width, cam.WorldView.Height);
foreach (ConvexHull convexHull in convexHulls)
{
if (!convexHull.Enabled || !convexHull.Intersects(camView)) { continue; }
if (convexHull.ParentEntity is Structure { CastShadow: false }) { continue; }
convexHull.DebugDraw(spriteBatch);
}
spriteBatch.End();
@@ -53,10 +53,6 @@ namespace Barotrauma.Lights
[Serialize(0f, IsPropertySaveable.Yes), Editable(MinValueFloat = -360, MaxValueFloat = 360, ValueStep = 1, DecimalCount = 0)]
public float Rotation { get; set; }
[Serialize(false, IsPropertySaveable.Yes, "Directional lights only shine in \"one direction\", meaning no shadows are cast behind them."+
" Note that this does not affect how the light texture is drawn: if you want something like a conical spotlight, you should use an appropriate texture for that.")]
public bool Directional { get; set; }
public Vector2 GetOffset() => Vector2.Transform(Offset, Matrix.CreateRotationZ(MathHelper.ToRadians(Rotation)));
private float flicker;
@@ -233,6 +229,7 @@ namespace Barotrauma.Lights
//do we need to recalculate the vertices of the light volume
private bool needsRecalculation;
private bool needsRecalculationWhenUpToDate;
public bool NeedsRecalculation
{
get { return needsRecalculation; }
@@ -246,9 +243,15 @@ namespace Barotrauma.Lights
}
}
needsRecalculation = value;
if (needsRecalculation && state != LightVertexState.UpToDate)
{
//if we're currently recalculating light vertices, mark that we need to recalculate them again after it's done
needsRecalculationWhenUpToDate = true;
}
}
}
//when were the vertices of the light volume last calculated
public float LastRecalculationTime { get; private set; }
@@ -309,8 +312,6 @@ namespace Barotrauma.Lights
if (Math.Abs(value - rotation) < 0.001f) { return; }
rotation = value;
dir = new Vector2(MathF.Cos(rotation), -MathF.Sin(rotation));
if (Math.Abs(rotation - prevCalculatedRotation) < RotationRecalculationThreshold && vertices != null)
{
return;
@@ -321,8 +322,6 @@ namespace Barotrauma.Lights
}
}
private Vector2 dir = Vector2.UnitX;
private Vector2 _spriteScale = Vector2.One;
public Vector2 SpriteScale
@@ -396,6 +395,8 @@ namespace Barotrauma.Lights
public float Priority;
public float PriorityMultiplier = 1.0f;
private Vector2 lightTextureTargetSize;
public Vector2 LightTextureTargetSize
@@ -444,7 +445,7 @@ namespace Barotrauma.Lights
public bool Enabled = true;
private readonly ISerializableEntity conditionalTarget;
private readonly PropertyConditional.LogicalOperatorType logicalOperator;
private readonly PropertyConditional.Comparison comparison;
private readonly List<PropertyConditional> conditionals = new List<PropertyConditional>();
public LightSource(ContentXElement element, ISerializableEntity conditionalTarget = null)
@@ -452,8 +453,11 @@ namespace Barotrauma.Lights
{
lightSourceParams = new LightSourceParams(element);
CastShadows = element.GetAttributeBool("castshadows", true);
logicalOperator = element.GetAttributeEnum(nameof(logicalOperator),
element.GetAttributeEnum("comparison", logicalOperator));
string comparison = element.GetAttributeString("comparison", null);
if (comparison != null)
{
Enum.TryParse(comparison, ignoreCase: true, out this.comparison);
}
if (lightSourceParams.DeformableLightSpriteElement != null)
{
@@ -466,7 +470,13 @@ namespace Barotrauma.Lights
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "conditional":
conditionals.AddRange(PropertyConditional.FromXElement(subElement));
foreach (XAttribute attribute in subElement.Attributes())
{
if (PropertyConditional.IsValid(attribute))
{
conditionals.Add(new PropertyConditional(attribute));
}
}
break;
}
}
@@ -529,32 +539,11 @@ namespace Barotrauma.Lights
var fullChList = ConvexHull.HullLists.FirstOrDefault(chList => chList.Submarine == sub);
if (fullChList == null) { return; }
//used to check whether the lightsource hits the target hull if the light is directional
Vector2 ray = new Vector2(dir.X, -dir.Y) * TextureRange;
Vector2 normal = new Vector2(-ray.Y, ray.X);
chList.List.Clear();
foreach (var convexHull in fullChList.List)
{
if (!convexHull.Enabled) { continue; }
if (!MathUtils.CircleIntersectsRectangle(lightPos, TextureRange, convexHull.BoundingBox)) { continue; }
if (lightSourceParams.Directional && false)
{
Rectangle bounds = convexHull.BoundingBox;
//invert because GetLineRectangleIntersection uses the messed up rects that start from top-left
bounds.Y -= bounds.Height;
//the ray can't hit if
// center is in the opposite direction from the ray (cheapest check first)
if (Vector2.Dot(ray, convexHull.BoundingBox.Center.ToVector2() - lightPos) <= 0 &&
/*ray doesn't hit the convex hull*/
!MathUtils.GetLineRectangleIntersection(lightPos, lightPos + ray, bounds, out _) &&
/*normal vectors of the ray don't hit the convex hull */
!MathUtils.GetLineRectangleIntersection(lightPos + normal, lightPos - normal, bounds, out _))
{
continue;
}
}
chList.List.Add(convexHull);
}
chList.IsHidden.RemoveWhere(ch => !chList.List.Contains(ch));
@@ -704,7 +693,7 @@ namespace Barotrauma.Lights
lock (mutex)
{
hull.RefreshWorldPositions();
hull.GetVisibleSegments(drawPos, visibleSegments);
hull.GetVisibleSegments(drawPos, visibleSegments);
foreach (var visibleSegment in visibleSegments)
{
if (visibleSegment.ConvexHull?.ParentEntity?.Submarine != null)
@@ -713,7 +702,6 @@ namespace Barotrauma.Lights
}
}
}
}
}
foreach (ConvexHull hull in chList.List)
@@ -812,7 +800,7 @@ namespace Barotrauma.Lights
}
else
{
intersects = MathUtils.GetLineIntersection(p1a, p1b, p2a, p2b, out intersection);
intersects = MathUtils.GetLineSegmentIntersection(p1a, p1b, p2a, p2b, out intersection);
}
if (intersects)
@@ -1006,7 +994,7 @@ namespace Barotrauma.Lights
}
else
{
intersects = MathUtils.GetLineIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection);
intersects = MathUtils.GetLineSegmentIntersection(rayStart, rayEnd, s.Start.WorldPos, s.End.WorldPos, out intersection);
}
if (intersects)
@@ -1041,9 +1029,11 @@ namespace Barotrauma.Lights
Vector2 drawPos = calculatedDrawPos;
float cosAngle = (float)Math.Cos(Rotation);
float sinAngle = -(float)Math.Sin(Rotation);
Vector2 uvOffset = Vector2.Zero;
Vector2 overrideTextureDims = Vector2.One;
Vector2 dir = this.dir;
if (OverrideLightTexture != null)
{
overrideTextureDims = new Vector2(OverrideLightTexture.SourceRect.Width, OverrideLightTexture.SourceRect.Height);
@@ -1052,7 +1042,8 @@ namespace Barotrauma.Lights
if (LightSpriteEffect == SpriteEffects.FlipHorizontally)
{
origin.X = OverrideLightTexture.SourceRect.Width - origin.X;
dir = -dir;
cosAngle = -cosAngle;
sinAngle = -sinAngle;
}
if (LightSpriteEffect == SpriteEffects.FlipVertically) { origin.Y = OverrideLightTexture.SourceRect.Height - origin.Y; }
uvOffset = (origin / overrideTextureDims) - new Vector2(0.5f, 0.5f);
@@ -1125,8 +1116,8 @@ namespace Barotrauma.Lights
//calculate texture coordinates based on the light's rotation
Vector2 originDiff = diff;
diff.X = originDiff.X * dir.X - originDiff.Y * dir.Y;
diff.Y = originDiff.X * dir.Y + originDiff.Y * dir.X;
diff.X = originDiff.X * cosAngle - originDiff.Y * sinAngle;
diff.Y = originDiff.X * sinAngle + originDiff.Y * cosAngle;
diff *= (overrideTextureDims / OverrideLightTexture.size);// / (1.0f - Math.Max(Math.Abs(uvOffset.X), Math.Abs(uvOffset.Y)));
diff += uvOffset;
}
@@ -1231,6 +1222,9 @@ namespace Barotrauma.Lights
}
drawPos.Y = -drawPos.Y;
float cosAngle = (float)Math.Cos(Rotation);
float sinAngle = -(float)Math.Sin(Rotation);
float bounds = TextureRange;
if (OverrideLightTexture != null)
@@ -1242,8 +1236,8 @@ namespace Barotrauma.Lights
origin /= Math.Max(overrideTextureDims.X, overrideTextureDims.Y);
origin *= TextureRange;
drawPos.X += origin.X * dir.Y + origin.Y * dir.X;
drawPos.Y += origin.X * dir.X + origin.Y * dir.Y;
drawPos.X += origin.X * sinAngle + origin.Y * cosAngle;
drawPos.Y += origin.X * cosAngle + origin.Y * sinAngle;
}
//add a square-shaped boundary to make sure we've got something to construct the triangles from
@@ -1349,7 +1343,7 @@ namespace Barotrauma.Lights
{
if (conditionals.None()) { return; }
if (conditionalTarget == null) { return; }
if (logicalOperator == PropertyConditional.LogicalOperatorType.And)
if (comparison == PropertyConditional.Comparison.And)
{
Enabled = conditionals.All(c => c.Matches(conditionalTarget));
}
@@ -1405,7 +1399,9 @@ namespace Barotrauma.Lights
CalculateLightVertices(verts);
LastRecalculationTime = (float)Timing.TotalTime;
NeedsRecalculation = false;
NeedsRecalculation = needsRecalculationWhenUpToDate;
needsRecalculationWhenUpToDate = false;
state = LightVertexState.UpToDate;
}
}
@@ -56,12 +56,12 @@ namespace Barotrauma
}
private static readonly List<RoundSound> roundSounds = new List<RoundSound>();
private static readonly Dictionary<string, RoundSound> roundSoundByPath = new Dictionary<string, RoundSound>();
public static RoundSound? Load(ContentXElement element, bool stream = false)
{
if (GameMain.SoundManager?.Disabled ?? true) { return null; }
var filename = element.GetAttributeContentPath("file") ?? element.GetAttributeContentPath("sound");
if (filename is null)
{
string errorMsg = "Error when loading round sound (" + element + ") - file path not set";
@@ -70,7 +70,11 @@ namespace Barotrauma
return null;
}
Sound? existingSound = roundSounds.Find(s => s.Filename == filename?.FullPath && s.Stream == stream && s.Sound is { Disposed: false })?.Sound;
Sound? existingSound = null;
if (roundSoundByPath.TryGetValue(filename.FullPath, out RoundSound? rs) && rs.Sound is { Disposed: false })
{
existingSound = rs.Sound;
}
if (existingSound is null)
{
@@ -99,7 +103,10 @@ namespace Barotrauma
}
RoundSound newSound = new RoundSound(element, existingSound);
if (filename is not null && !newSound.Stream)
{
roundSoundByPath.TryAdd(filename.FullPath, newSound);
}
roundSounds.Add(newSound);
return newSound;
}
@@ -124,24 +131,14 @@ namespace Barotrauma
roundSound.Sound = existingSound;
}
private static void Remove(RoundSound roundSound)
{
#warning TODO: what is going on here????
roundSound.Sound?.Dispose();
if (roundSounds.Contains(roundSound)) { roundSounds.Remove(roundSound); }
foreach (RoundSound otherSound in roundSounds)
{
if (otherSound.Sound == roundSound.Sound) { otherSound.Sound = null; }
}
}
public static void RemoveAllRoundSounds()
{
for (int i = roundSounds.Count - 1; i >= 0; i--)
foreach (var roundSound in roundSounds)
{
Remove(roundSounds[i]);
roundSound.Sound?.Dispose();
}
roundSounds.Clear();
roundSoundByPath.Clear();
}
}
}
@@ -58,11 +58,8 @@ namespace Barotrauma
convexHulls ??= new List<ConvexHull>();
var h = new ConvexHull(
new Rectangle((position - size / 2).ToPoint(), size.ToPoint()),
IsHorizontal,
this)
{
IsExteriorWall = IsExteriorWall
};
IsHorizontal,
this);
if (Math.Abs(rotation) > 0.001f)
{
h.Rotate(position, rotation);
@@ -501,7 +498,7 @@ namespace Barotrauma
private bool ConditionalMatches(PropertyConditional conditional)
{
if (!string.IsNullOrEmpty(conditional.TargetItemComponent))
if (!string.IsNullOrEmpty(conditional.TargetItemComponentName))
{
return false;
}
@@ -116,11 +116,11 @@ namespace Barotrauma
foreach (MapEntity e in entitiesToRender)
{
if (!e.DrawOverWater) continue;
if (!e.DrawOverWater) { continue; }
if (predicate != null)
{
if (!predicate(e)) continue;
if (!predicate(e)) { continue; }
}
e.Draw(spriteBatch, editing, false);
@@ -1,6 +1,7 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma
{
@@ -18,7 +19,7 @@ namespace Barotrauma
return;
}
List<Submarine> subsToMove = submarine.GetConnectedSubs();
var subsToMove = submarine.GetConnectedSubs();
foreach (Submarine dockedSub in subsToMove)
{
if (dockedSub == submarine) { continue; }
@@ -51,7 +52,6 @@ namespace Barotrauma
sub.PhysicsBody.SetTransformIgnoreContacts(sub.PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(moveAmount), 0.0f);
}
}
if (closestSub != null && subsToMove.Contains(closestSub))
{
GameMain.GameScreen.Cam.Position += moveAmount;
@@ -67,6 +67,10 @@ namespace Barotrauma
else if (ConnectedDoor != null)
{
sprite = iconSprites["Door"];
if (ConnectedDoor.IsHorizontal && Ladders == null)
{
clr = Color.Yellow;
}
}
else if (Ladders != null)
{
@@ -8,7 +8,6 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
@@ -16,6 +15,10 @@ namespace Barotrauma.Networking
{
sealed class GameClient : NetworkMember
{
public static readonly TimeSpan CampaignSaveTransferTimeOut = new TimeSpan(0, 0, seconds: 100);
//this should be longer than CampaignSaveTransferTimeOut - we shouldn't give up starting the round if we're still waiting for the save file
public static readonly TimeSpan LevelTransitionTimeOut = new TimeSpan(0, 0, seconds: 150);
public override bool IsClient => true;
public override bool IsServer => false;
@@ -514,6 +517,7 @@ namespace Barotrauma.Networking
DisplayInLoadingScreens = true
};
Quit();
GUI.DisableHUD = false;
GameMain.ServerListScreen.Select();
return;
}
@@ -931,7 +935,7 @@ namespace Barotrauma.Networking
", level value count: " + levelEqualityCheckValues.Count +
", seed: " + Level.Loaded.Seed +
", sub: " + Submarine.MainSub.Info.Name + " (" + Submarine.MainSub.Info.MD5Hash.ShortRepresentation + ")" +
", mirrored: " + Level.Loaded.Mirrored + "). Round init status: {roundInitStatus}." + campaignErrorInfo;
", mirrored: " + Level.Loaded.Mirrored + "). Round init status: " + roundInitStatus + "." + campaignErrorInfo;
GameAnalyticsManager.AddErrorEventOnce("GameClient.StartGame:LevelsDontMatch" + Level.Loaded.Seed, GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
throw new Exception(errorMsg);
}
@@ -1487,16 +1491,17 @@ namespace Barotrauma.Networking
NetIdUtils.IdMoreRecent(campaignSaveID, campaign.PendingSaveID))
{
campaign.PendingSaveID = campaignSaveID;
DateTime saveFileTimeOut = DateTime.Now + new TimeSpan(0, 0, 60);
DateTime saveFileTimeOut = DateTime.Now + CampaignSaveTransferTimeOut;
while (NetIdUtils.IdMoreRecent(campaignSaveID, campaign.LastSaveID))
{
if (DateTime.Now > saveFileTimeOut)
{
GameStarted = true;
DebugConsole.ThrowError("Failed to start campaign round (timed out while waiting for the up-to-date save file).");
new GUIMessageBox(TextManager.Get("error"), TextManager.Get("campaignsavetransfer.timeout"));
GameMain.NetLobbyScreen.Select();
roundInitStatus = RoundInitStatus.Interrupted;
yield return CoroutineStatus.Failure;
//use success status, even though this is a failure (no need to show a console error because we show it in the message box)
yield return CoroutineStatus.Success;
}
yield return new WaitForSeconds(0.1f);
}
@@ -1712,7 +1717,7 @@ namespace Barotrauma.Networking
yield return CoroutineStatus.Success;
}
if (GameMain.GameSession != null) { GameMain.GameSession.EndRound(endMessage, traitorResults, transitionType); }
GameMain.GameSession?.EndRound(endMessage, traitorResults, transitionType);
ServerSettings.ServerDetailsChanged = true;
@@ -2872,12 +2877,14 @@ namespace Barotrauma.Networking
ClientPeer.Send(msg, DeliveryMethod.Reliable);
}
public bool SpectateClicked(GUIButton button, object userData)
public bool SpectateClicked(GUIButton button, object _)
{
MultiPlayerCampaign campaign =
MultiPlayerCampaign campaign =
GameMain.NetLobbyScreen.SelectedMode == GameMain.GameSession?.GameMode.Preset ?
GameMain.GameSession?.GameMode as MultiPlayerCampaign : null;
if (campaign != null && campaign.LastSaveID < campaign.PendingSaveID)
if (FileReceiver.ActiveTransfers.Any(t => t.FileType == FileTransferType.CampaignSave) ||
(campaign != null && NetIdUtils.IdMoreRecent(campaign.PendingSaveID, campaign.LastSaveID)))
{
new GUIMessageBox("", TextManager.Get("campaignfiletransferinprogress"));
return false;
@@ -3063,8 +3070,12 @@ namespace Barotrauma.Networking
if (votingInterface != null)
{
votingInterface.Update(deltaTime);
if (!votingInterface.VoteRunning)
if (!votingInterface.VoteRunning || votingInterface.TimedOut)
{
if (votingInterface.TimedOut)
{
DebugConsole.AddWarning($"Voting interface timed out.");
}
votingInterface.Remove();
votingInterface = null;
}
@@ -111,8 +111,25 @@ namespace Barotrauma.Networking
? NetworkConnection.TimeoutThresholdInGame
: NetworkConnection.TimeoutThreshold;
IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection);
try
{
IReadMessage inc = new ReadOnlyMessage(data, false, 0, dataLength, ServerConnection);
ProcessP2PData(inc);
}
catch (Exception e)
{
string errorMsg = $"Client failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}";
GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnP2PData:ClientReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#else
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
}
}
private void ProcessP2PData(IReadMessage inc)
{
var (deliveryMethod, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (!packetHeader.IsServerMessage()) { return; }
@@ -141,15 +141,31 @@ namespace Barotrauma.Networking
if (remotePeer.DisconnectTime != null) { return; }
var peerPacketHeaders = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
PacketHeader packetHeader = peerPacketHeaders.PacketHeader;
try
{
ProcessP2PData(steamId, remotePeer, inc);
}
catch (Exception e)
{
string errorMsg = $"Server failed to read an incoming P2P message. {{{e}}}\n{e.StackTrace.CleanupStackTrace()}";
GameAnalyticsManager.AddErrorEventOnce($"SteamP2POwnerPeer.OnP2PData:OwnerReadException{e.TargetSite}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
#if DEBUG
DebugConsole.ThrowError(errorMsg);
#else
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError(errorMsg); }
#endif
}
}
if (!remotePeer.Authenticated && !remotePeer.Authenticating && packetHeader.IsConnectionInitializationStep())
private void ProcessP2PData(ulong steamId, RemotePeer remotePeer, IReadMessage inc)
{
var (deliveryMethod, packetHeader, connectionInitialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (remotePeer is { Authenticated: false, Authenticating: false } && packetHeader.IsConnectionInitializationStep())
{
remotePeer.DisconnectTime = null;
ConnectionInitialization initialization = peerPacketHeaders.Initialization ?? throw new Exception("Initialization step missing");
ConnectionInitialization initialization = connectionInitialization ?? throw new Exception("Initialization step missing");
if (initialization == ConnectionInitialization.SteamTicketAndVersion)
{
remotePeer.Authenticating = true;
@@ -181,6 +197,7 @@ namespace Barotrauma.Networking
ForwardToServerProcess(outMsg);
}
}
public override void Update(float deltaTime)
@@ -53,7 +53,7 @@ namespace Barotrauma.Networking
}
}
private readonly ref struct LobbyDataChangedEventHandler
private readonly struct LobbyDataChangedEventHandler : IDisposable
{
private readonly Action<Lobby> action;
@@ -187,7 +187,7 @@ namespace Barotrauma.Networking
int botSpawnMode = 0,
bool? useRespawnShuttle = null)
{
if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) return;
if (!GameMain.Client.HasPermission(Networking.ClientPermissions.ManageSettings)) { return; }
IWriteMessage outMsg = new WriteOnlyMessage();
@@ -253,12 +253,9 @@ namespace Barotrauma.Networking
//in push-to-talk mode, InputType.Voice uses the active chat mode
bool usingActiveMode = PlayerInput.KeyDown(InputType.Voice);
bool pttDown = (usingActiveMode || usingLocalMode || usingRadioMode) && GUI.KeyboardDispatcher.Subscriber == null;
if (pttDown || captureTimer <= 0)
{
ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode;
}
if (pttDown)
{
ForceLocal = (usingActiveMode && GameMain.ActiveChatMode == ChatMode.Local) || usingLocalMode;
allowEnqueue = true;
}
}
@@ -115,17 +115,20 @@ namespace Barotrauma.Networking
float speechImpedimentMultiplier = 1.0f - client.Character.SpeechImpediment / 100.0f;
bool spectating = Character.Controlled == null;
float rangeMultiplier = spectating ? 2.0f : 1.0f;
WifiComponent radio = null;
WifiComponent senderRadio = null;
var messageType =
!client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) && ChatMessage.CanUseRadio(Character.Controlled) ?
ChatMessageType.Radio : ChatMessageType.Default;
!client.VoipQueue.ForceLocal &&
ChatMessage.CanUseRadio(client.Character, out senderRadio) &&
ChatMessage.CanUseRadio(Character.Controlled, out var recipientRadio) &&
senderRadio.CanReceive(recipientRadio) ?
ChatMessageType.Radio : ChatMessageType.Default;
client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
client.RadioNoise = 0.0f;
if (messageType == ChatMessageType.Radio)
{
client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier);
client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier);
if (distanceFactor > RangeNear && !spectating)
{
//noise starts increasing exponentially after 40% range
@@ -348,11 +348,13 @@ namespace Barotrauma
switch (voteType)
{
case VoteType.PurchaseAndSwitchSub:
GameMain.GameSession.PurchaseSubmarine(subInfo);
GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems);
if (GameMain.GameSession.TryPurchaseSubmarine(subInfo))
{
GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems);
}
break;
case VoteType.PurchaseSub:
GameMain.GameSession.PurchaseSubmarine(subInfo);
GameMain.GameSession.TryPurchaseSubmarine(subInfo);
break;
case VoteType.SwitchSub:
GameMain.GameSession.SwitchSubmarine(subInfo, submarineVoteInfo.TransferItems);
@@ -1,4 +1,5 @@
using FarseerPhysics;
using Barotrauma.Extensions;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
@@ -48,19 +49,23 @@ namespace Barotrauma.Particles
private Vector2 drawPosition;
private float drawRotation;
private Vector2 colliderRadius;
private Hull currentHull;
private List<Gap> hullGaps;
private bool hasSubEmitters;
private List<ParticleEmitter> subEmitters = new List<ParticleEmitter>();
private readonly List<ParticleEmitter> subEmitters = new List<ParticleEmitter>();
private float animState;
private int animFrame;
private float collisionUpdateTimer;
private bool changesSize;
public bool HighQualityCollisionDetection;
public Vector4 ColorMultiplier;
@@ -127,7 +132,10 @@ namespace Barotrauma.Particles
position = (tracerPoints.Item1 + tracerPoints.Item2) / 2;
}
RefreshColliderSize();
sizeChange = prefab.SizeChangeMin + (prefab.SizeChangeMax - prefab.SizeChangeMin) * Rand.Range(0.0f, 1.0f);
changesSize = !sizeChange.NearlyEquals(Vector2.Zero);
this.position = position;
prevPosition = position;
@@ -256,8 +264,12 @@ namespace Barotrauma.Particles
}
}
size.X += sizeChange.X * deltaTime;
size.Y += sizeChange.Y * deltaTime;
if (changesSize)
{
size.X += sizeChange.X * deltaTime;
size.Y += sizeChange.Y * deltaTime;
RefreshColliderSize();
}
if (UseMiddleColor)
{
@@ -344,11 +356,11 @@ namespace Barotrauma.Particles
{
Rectangle hullRect = currentHull.WorldRect;
Vector2 collisionNormal = Vector2.Zero;
if (velocity.Y < 0.0f && position.Y - prefab.CollisionRadius * size.Y < hullRect.Y - hullRect.Height)
if (velocity.Y < 0.0f && position.Y - colliderRadius.Y < hullRect.Y - hullRect.Height)
{
collisionNormal = new Vector2(0.0f, 1.0f);
}
else if (velocity.Y > 0.0f && position.Y + prefab.CollisionRadius * size.Y > hullRect.Y)
else if (velocity.Y > 0.0f && position.Y + colliderRadius.Y > hullRect.Y)
{
collisionNormal = new Vector2(0.0f, -1.0f);
}
@@ -378,11 +390,11 @@ namespace Barotrauma.Particles
}
collisionNormal = Vector2.Zero;
if (velocity.X < 0.0f && position.X - prefab.CollisionRadius * size.X < hullRect.X)
if (velocity.X < 0.0f && position.X - colliderRadius.X < hullRect.X)
{
collisionNormal = new Vector2(1.0f, 0.0f);
}
else if (velocity.X > 0.0f && position.X + prefab.CollisionRadius * size.X > hullRect.Right)
else if (velocity.X > 0.0f && position.X + colliderRadius.X > hullRect.Right)
{
collisionNormal = new Vector2(-1.0f, 0.0f);
}
@@ -431,6 +443,13 @@ namespace Barotrauma.Particles
return UpdateResult.Normal;
}
private void RefreshColliderSize()
{
if (!prefab.UseCollision) { return; }
colliderRadius = new Vector2(prefab.CollisionRadius);
if (!prefab.InvariantCollisionSize) { colliderRadius *= size; }
}
private void ApplyDrag(float dragCoefficient, float deltaTime)
{
Vector2 relativeVel = velocity;
@@ -475,11 +494,11 @@ namespace Barotrauma.Particles
{
if (collisionNormal.X > 0.0f)
{
position.X = Math.Max(position.X, prevHullRect.X + prefab.CollisionRadius * size.X);
position.X = Math.Max(position.X, prevHullRect.X + colliderRadius.X);
}
else
{
position.X = Math.Min(position.X, prevHullRect.Right - prefab.CollisionRadius * size.X);
position.X = Math.Min(position.X, prevHullRect.Right - colliderRadius.X);
}
velocity.X = Math.Sign(collisionNormal.X) * Math.Abs(velocity.X) * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction);
@@ -488,11 +507,11 @@ namespace Barotrauma.Particles
{
if (collisionNormal.Y > 0.0f)
{
position.Y = Math.Max(position.Y, prevHullRect.Y - prevHullRect.Height + prefab.CollisionRadius * size.Y);
position.Y = Math.Max(position.Y, prevHullRect.Y - prevHullRect.Height + colliderRadius.Y);
}
else
{
position.Y = Math.Min(position.Y, prevHullRect.Y - prefab.CollisionRadius * size.Y);
position.Y = Math.Min(position.Y, prevHullRect.Y - colliderRadius.Y);
}
velocity.X *= (1.0f - prefab.Friction);
@@ -513,26 +532,26 @@ namespace Barotrauma.Particles
if (position.Y < center.Y)
{
position.Y = hullRect.Y - hullRect.Height - prefab.CollisionRadius;
position.Y = hullRect.Y - hullRect.Height - colliderRadius.Y;
velocity.X *= (1.0f - prefab.Friction);
velocity.Y = -velocity.Y * prefab.Restitution;
}
else if (position.Y > center.Y)
{
position.Y = hullRect.Y + prefab.CollisionRadius;
position.Y = hullRect.Y + colliderRadius.Y;
velocity.X *= (1.0f - prefab.Friction);
velocity.Y = -velocity.Y * prefab.Restitution;
}
if (position.X < center.X)
{
position.X = hullRect.X - prefab.CollisionRadius;
position.X = hullRect.X - colliderRadius.X;
velocity.X = -velocity.X * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction);
}
else if (position.X > center.X)
{
position.X = hullRect.X + hullRect.Width + prefab.CollisionRadius;
position.X = hullRect.X + hullRect.Width + colliderRadius.X;
velocity.X = -velocity.X * prefab.Restitution;
velocity.Y *= (1.0f - prefab.Friction);
}
@@ -559,11 +578,12 @@ namespace Barotrauma.Particles
Color currColor = new Color(color.ToVector4() * ColorMultiplier);
if (prefab.Sprites[spriteIndex] is SpriteSheet)
Vector2 drawPos = new Vector2(drawPosition.X, -drawPosition.Y);
if (prefab.Sprites[spriteIndex] is SpriteSheet sheet)
{
((SpriteSheet)prefab.Sprites[spriteIndex]).Draw(
sheet.Draw(
spriteBatch, animFrame,
new Vector2(drawPosition.X, -drawPosition.Y),
drawPos,
currColor * (currColor.A / 255.0f),
prefab.Sprites[spriteIndex].Origin, drawRotation,
drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth);
@@ -571,11 +591,23 @@ namespace Barotrauma.Particles
else
{
prefab.Sprites[spriteIndex].Draw(spriteBatch,
new Vector2(drawPosition.X, -drawPosition.Y),
drawPos,
currColor * (currColor.A / 255.0f),
prefab.Sprites[spriteIndex].Origin, drawRotation,
drawSize, SpriteEffects.None, prefab.Sprites[spriteIndex].Depth);
}
/*if (GameMain.DebugDraw && prefab.UseCollision)
{
GUI.DrawLine(spriteBatch,
drawPos - Vector2.UnitX * colliderRadius.X,
drawPos + Vector2.UnitX * colliderRadius.X,
Color.Gray);
GUI.DrawLine(spriteBatch,
drawPos - Vector2.UnitY * colliderRadius.Y,
drawPos + Vector2.UnitY * colliderRadius.Y,
Color.Gray);
}*/
}
}
}
@@ -135,6 +135,9 @@ namespace Barotrauma.Particles
[Editable(0.0f, 10000.0f), Serialize(0.0f, IsPropertySaveable.No, description: "Radius of the particle's collider. Only has an effect if UseCollision is set to true.")]
public float CollisionRadius { get; private set; }
[Editable, Serialize(false, IsPropertySaveable.No, description: "If enabled, the size (or changes in size) of the particle doesn't affect the size of the collider.")]
public bool InvariantCollisionSize { get; private set; }
[Editable, Serialize(false, IsPropertySaveable.No, description: "Does the particle collide with the walls of the submarine and the level.")]
public bool UseCollision { get; private set; }
@@ -18,7 +18,7 @@ namespace Barotrauma
public CharacterInfo.AppearanceCustomizationMenu[] CharacterMenus { get; private set; }
private GUIButton nextButton;
private GUILayoutGroup characterInfoColumns;
private GUIListBox characterInfoColumns;
public SinglePlayerCampaignSetupUI(GUIComponent newGameContainer, GUIComponent loadGameContainer, IEnumerable<SubmarineInfo> submarines, IEnumerable<CampaignMode.SaveInfo> saveFiles = null)
: base(newGameContainer, loadGameContainer)
@@ -249,11 +249,7 @@ namespace Barotrauma
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.04f), secondPageLayout.RectTransform),
TextManager.Get("Crew"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft);
characterInfoColumns = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true)
{
Stretch = true,
RelativeSpacing = 0.01f
};
characterInfoColumns = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.86f), secondPageLayout.RectTransform), isHorizontal: true);
var secondPageButtonContainer = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.08f),
secondPageLayout.RectTransform), childAnchor: Anchor.BottomLeft, isHorizontal: true)
@@ -306,8 +302,8 @@ namespace Barotrauma
for (int i = 0; i < characterInfos.Count; i++)
{
var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(1.0f / characterInfos.Count, 1.0f),
characterInfoColumns.RectTransform));
var subLayout = new GUILayoutGroup(new RectTransform(new Vector2(Math.Max(1.0f / characterInfos.Count, 0.33f), 1.0f),
characterInfoColumns.Content.RectTransform));
var (characterInfo, job) = characterInfos[i];
@@ -202,18 +202,21 @@ namespace Barotrauma
case CampaignMode.InteractionType.PurchaseSub:
submarineSelection?.Update();
break;
case CampaignMode.InteractionType.Crew:
CrewManagement?.Update();
break;
case CampaignMode.InteractionType.Store:
Store?.Update(deltaTime);
break;
break;
case CampaignMode.InteractionType.MedicalClinic:
MedicalClinic?.Update(deltaTime);
break;
case CampaignMode.InteractionType.Map:
if (StartButton != null)
{
StartButton.Enabled = CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap) && Character.Controlled is { IsIncapacitated: false };
}
break;
}
}
@@ -568,7 +571,6 @@ namespace Barotrauma
StartButton.Visible = false;
missionList.Enabled = false;
}
//locationInfoPanel?.UpdateAuto(1.0f);
}
public void SelectTab(CampaignMode.InteractionType tab, Character npc = null)
@@ -435,8 +435,11 @@ namespace Barotrauma
graphics.BlendState = BlendState.NonPremultiplied;
graphics.SamplerStates[0] = SamplerState.PointClamp;
graphics.SamplerStates[1] = SamplerState.PointClamp;
GameMain.LightManager.LosEffect.CurrentTechnique.Passes[0].Apply();
Quad.Render();
graphics.SamplerStates[0] = SamplerState.LinearWrap;
graphics.SamplerStates[1] = SamplerState.LinearWrap;
}
if (Character.Controlled is { } character)
@@ -59,7 +59,6 @@ namespace Barotrauma
private GUIImage playstyleBanner;
private GUITextBlock playstyleDescription;
private const string RemoteContentUrl = "http://www.barotraumagame.com/gamedata/";
private readonly GUIComponent remoteContentContainer;
private XDocument remoteContentDoc;
@@ -1525,10 +1524,11 @@ namespace Barotrauma
private void FetchRemoteContent()
{
if (string.IsNullOrEmpty(RemoteContentUrl)) { return; }
string remoteContentUrl = GameSettings.CurrentConfig.RemoteMainMenuContentUrl;
if (string.IsNullOrEmpty(remoteContentUrl)) { return; }
try
{
var client = new RestClient(RemoteContentUrl);
var client = new RestClient(remoteContentUrl);
var request = new RestRequest("MenuContent.xml", Method.GET);
TaskPool.Add("RequestMainMenuRemoteContent", client.ExecuteAsync(request),
RemoteContentReceived);
@@ -751,12 +751,20 @@ namespace Barotrauma
};
ServerMessage.OnTextChanged += (textBox, text) =>
{
Vector2 textSize = textBox.Font.MeasureString(textBox.WrappedText);
textBox.RectTransform.NonScaledSize = new Point(textBox.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10));
serverMessageContainer.UpdateScrollBarSize();
serverMessageHint.Visible = !textBox.Selected && !textBox.Readonly && string.IsNullOrWhiteSpace(textBox.Text);
RefreshServerInfoSize();
return true;
};
ServerMessage.RectTransform.SizeChanged += RefreshServerInfoSize;
void RefreshServerInfoSize()
{
serverMessageHint.Visible = !ServerMessage.Selected && !ServerMessage.Readonly && string.IsNullOrWhiteSpace(ServerMessage.Text);
Vector2 textSize = ServerMessage.Font.MeasureString(ServerMessage.WrappedText);
ServerMessage.RectTransform.NonScaledSize = new Point(ServerMessage.RectTransform.NonScaledSize.X, Math.Max(serverMessageContainer.Content.Rect.Height, (int)textSize.Y + 10));
serverMessageContainer.UpdateScrollBarSize();
}
ServerMessage.OnEnterPressed += (textBox, text) =>
{
string str = textBox.Text;
@@ -812,7 +812,7 @@ namespace Barotrauma
private bool SortList(GUIButton button, object obj)
{
if (!(obj is ColumnLabel sortBy)) { return false; }
if (obj is not ColumnLabel sortBy) { return false; }
SortList(sortBy, toggle: true);
return true;
}
@@ -848,8 +848,7 @@ namespace Barotrauma
{
if (c1.GUIComponent.UserData is not ServerInfo s1) { return 0; }
if (c2.GUIComponent.UserData is not ServerInfo s2) { return 0; }
int comparison = sortedAscending ? 1 : -1;
return CompareServer(sortBy, s1, s2) * comparison;
return CompareServer(sortBy, s1, s2, sortedAscending);
});
}
@@ -857,22 +856,31 @@ namespace Barotrauma
{
var children = serverList.Content.RectTransform.Children.Reverse().ToList();
int comparison = sortedAscending ? 1 : -1;
foreach (var child in children)
{
if (child.GUIComponent.UserData is not ServerInfo serverInfo2 || serverInfo.Equals(serverInfo2)) { continue; }
if (CompareServer(sortedBy, serverInfo, serverInfo2) * comparison >= 0)
if (CompareServer(sortedBy, serverInfo, serverInfo2, sortedAscending) >= 0)
{
var index = serverList.Content.RectTransform.GetChildIndex(child);
component.RectTransform.RepositionChildInHierarchy(index + 1);
component.RectTransform.RepositionChildInHierarchy(Math.Min(index + 1, serverList.Content.CountChildren - 1));
return;
}
}
component.RectTransform.SetAsFirstChild();
}
private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2)
private static int CompareServer(ColumnLabel sortBy, ServerInfo s1, ServerInfo s2, bool ascending)
{
//always put servers with unknown ping at the bottom (unless we're specifically sorting by ping)
//servers without a ping are often unreachable/spam
bool s1HasPing = s1.Ping.IsSome();
bool s2HasPing = s2.Ping.IsSome();
if (s1HasPing != s2HasPing)
{
return s1HasPing ? -1 : 1;
}
int comparison = ascending ? 1 : -1;
switch (sortBy)
{
case ColumnLabel.ServerListCompatible:
@@ -880,18 +888,18 @@ namespace Barotrauma
bool s2Compatible = NetworkMember.IsCompatible(GameMain.Version, s2.GameVersion);
if (s1Compatible == s2Compatible) { return 0; }
return s1Compatible ? -1 : 1;
return (s1Compatible ? -1 : 1) * comparison;
case ColumnLabel.ServerListHasPassword:
if (s1.HasPassword == s2.HasPassword) { return 0; }
return s1.HasPassword ? 1 : -1;
return (s1.HasPassword ? 1 : -1) * comparison;
case ColumnLabel.ServerListName:
// I think we actually want culture-specific sorting here?
return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture);
return string.Compare(s1.ServerName, s2.ServerName, StringComparison.CurrentCulture) * comparison;
case ColumnLabel.ServerListRoundStarted:
if (s1.GameStarted == s2.GameStarted) { return 0; }
return s1.GameStarted ? 1 : -1;
return (s1.GameStarted ? 1 : -1) * comparison;
case ColumnLabel.ServerListPlayers:
return s2.PlayerCount.CompareTo(s1.PlayerCount);
return s2.PlayerCount.CompareTo(s1.PlayerCount) * comparison;
case ColumnLabel.ServerListPing:
return (s1.Ping.TryUnwrap(out var s1Ping), s2.Ping.TryUnwrap(out var s2Ping)) switch
{
@@ -899,7 +907,7 @@ namespace Barotrauma
(true, true) => s2Ping.CompareTo(s1Ping),
(false, true) => 1,
(true, false) => -1
};
} * comparison;
default:
return 0;
}
@@ -1504,9 +1512,41 @@ namespace Barotrauma
private void AddToServerList(ServerInfo serverInfo, bool skipPing = false)
{
const int MaxAllowedPlayers = 1000;
const int MaxAllowedSimilarServers = 10;
const float MinSimilarityPercentage = 0.8f;
if (string.IsNullOrWhiteSpace(serverInfo.ServerName)) { return; }
if (serverInfo.PlayerCount > serverInfo.MaxPlayers) { return; }
if (serverInfo.PlayerCount < 0) { return; }
if (serverInfo.MaxPlayers <= 0) { return; }
//no way a legit server can have this many players
if (serverInfo.MaxPlayers > MaxAllowedPlayers) { return; }
int similarServerCount = 0;
string serverInfoStr = getServerInfoStr(serverInfo);
foreach (var serverElement in serverList.Content.Children)
{
if (!serverElement.Visible) { continue; }
if (serverElement.UserData is not ServerInfo otherServer || otherServer == serverInfo) { continue; }
if (ToolBox.LevenshteinDistance(serverInfoStr, getServerInfoStr(otherServer)) < serverInfoStr.Length * (1.0f - MinSimilarityPercentage))
{
similarServerCount++;
if (similarServerCount > MaxAllowedSimilarServers)
{
DebugConsole.Log($"Server {serverInfo.ServerName} seems to be almost identical to {otherServer.ServerName}. Hiding as a potential spam server.");
break;
}
}
}
if (similarServerCount > MaxAllowedSimilarServers) { return; }
static string getServerInfoStr(ServerInfo serverInfo)
{
string str = serverInfo.ServerName + serverInfo.ServerMessage + serverInfo.MaxPlayers;
if (str.Length > 200) { return str.Substring(0, 200); }
return str;
}
RemoveMsgFromServerList(MsgUserData.RefreshingServerList);
RemoveMsgFromServerList(MsgUserData.NoServers);
@@ -1522,7 +1562,6 @@ namespace Barotrauma
UpdateServerInfoUI(serverInfo);
if (!skipPing) { PingUtils.GetServerPing(serverInfo, UpdateServerInfoUI); }
InsertServer(serverInfo, serverFrame);
}
private void UpdateServerInfoUI(ServerInfo serverInfo)
@@ -1736,7 +1775,7 @@ namespace Barotrauma
AddToFavoriteServers(serverInfo);
}
SortList(sortedBy, toggle: false);
InsertServer(serverInfo, serverFrame);
FilterServers();
}
@@ -1,16 +1,16 @@
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Xml.Linq;
using Microsoft.Xna.Framework.Input;
using Barotrauma.IO;
using Barotrauma.Steam;
namespace Barotrauma
{
@@ -525,6 +525,11 @@ namespace Barotrauma
GUI.AddMessage(TextManager.Get("waypointsgeneratedsuccesfully"), GUIStyle.Green);
}
WayPoint.ShowWayPoints = true;
var matchingTickBox = showEntitiesTickBoxes?.Find(tb => tb.UserData as string == "waypoint");
if (matchingTickBox != null)
{
matchingTickBox.Selected = true;
}
generateWaypointsVerification.Close();
return true;
};
@@ -2847,7 +2852,7 @@ namespace Barotrauma
{
OnClicked = (button, o) =>
{
var requiredPackages = MapEntity.mapEntityList.Select(e => e.Prefab.ContentPackage)
var requiredPackages = MapEntity.mapEntityList.Select(e => e?.Prefab?.ContentPackage)
.Where(cp => cp != null)
.Distinct().OfType<ContentPackage>().Select(p => p.Name).ToHashSet();
var tickboxes = requiredContentPackList.Content.Children.OfType<GUITickBox>().ToArray();
@@ -3546,10 +3551,46 @@ namespace Barotrauma
TextManager.Get("LoadingVanillaSubmarineHeader"),
TextManager.Get("LoadingVanillaSubmarineDesc"));
public void LoadSub(SubmarineInfo info)
public void LoadSub(SubmarineInfo info, bool checkIdConflicts = true)
{
Submarine.Unload();
Submarine selectedSub = null;
if (checkIdConflicts)
{
Dictionary<int, Identifier> entities = new Dictionary<int, Identifier>();
foreach (var subElement in info.SubmarineElement.Elements())
{
int id = subElement.GetAttributeInt("ID", -1);
if (id == -1) { continue; }
Identifier identifier = subElement.GetAttributeIdentifier("identifier", string.Empty);
if (entities.TryGetValue(id, out Identifier duplicateEntity))
{
var errorMsg = new GUIMessageBox(
TextManager.Get("error"),
TextManager.GetWithVariables("subeditor.duplicateiderror",
("[entity1]", $"{duplicateEntity} ({id})"),
("[entity2]", $"{identifier} ({id})")),
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("No") });
errorMsg.Buttons[0].OnClicked = (bnt, userdata) =>
{
subElement.Remove();
LoadSub(info, checkIdConflicts: false);
errorMsg.Close();
return true;
};
errorMsg.Buttons[1].OnClicked = (bnt, userdata) =>
{
LoadSub(info, checkIdConflicts: false);
errorMsg.Close();
return true;
};
return;
}
entities.Add(id, identifier);
}
}
try
{
selectedSub = new Submarine(info);
@@ -5755,7 +5796,10 @@ namespace Barotrauma
{
item.SetTransform(dummyCharacter.SimPosition, 0.0f);
item.UpdateTransform();
item.SetTransform(item.body.SimPosition, 0.0f);
if (item.body != null)
{
item.SetTransform(item.body.SimPosition, 0.0f);
}
//wires need to be updated for the last node to follow the player during rewiring
Wire wire = item.GetComponent<Wire>();
@@ -5868,6 +5912,11 @@ namespace Barotrauma
spriteBatch.End();
}
if (GameMain.LightManager.DebugLos)
{
GameMain.LightManager.DebugDrawLos(spriteBatch, cam);
}
//-------------------- HUD -----------------------------
spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState);
@@ -343,12 +343,11 @@ namespace Barotrauma
if (property.PropertyType == typeof(string) && value == null)
{
value = "";
}
}
Identifier propertyTag = $"{property.PropertyInfo.DeclaringType.Name}.{property.PropertyInfo.Name}".ToIdentifier();
Identifier fallbackTag = property.PropertyInfo.Name.ToIdentifier();
LocalizedString displayName =
TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier());
LocalizedString displayName = TextManager.Get(propertyTag, $"sp.{propertyTag}.name".ToIdentifier());
if (displayName.IsNullOrEmpty())
{
Editable editable = property.GetAttribute<Editable>();
@@ -380,10 +379,14 @@ namespace Barotrauma
}
LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description");
if (toolTip.IsNullOrEmpty() && entity.GetType() != property.PropertyInfo.DeclaringType)
if (entity.GetType() != property.PropertyInfo.DeclaringType)
{
Identifier propertyTagForDerivedClass = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier();
toolTip = TextManager.Get($"{propertyTagForDerivedClass}.description", $"sp.{propertyTagForDerivedClass}.description");
var toolTipForDerivedClass = TextManager.Get($"{propertyTagForDerivedClass}.description", $"sp.{propertyTagForDerivedClass}.description");
if (!toolTipForDerivedClass.IsNullOrEmpty())
{
toolTip = toolTipForDerivedClass;
}
}
if (toolTip.IsNullOrEmpty())
{
@@ -163,7 +163,7 @@ namespace Barotrauma.Steam
CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.LocalPackages.Refresh());
}
public static async Task CreatePublishStagingCopy(string modVersion, ContentPackage contentPackage)
public static async Task CreatePublishStagingCopy(string title, string modVersion, ContentPackage contentPackage)
{
await Task.Yield();
@@ -184,11 +184,13 @@ namespace Barotrauma.Steam
throw new Exception("Staging copy could not be loaded",
result.TryUnwrapFailure(out var exception) ? exception : null);
}
//Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be
//Load filelist.xml and write the hash into it so anyone downloading this mod knows what it should be
ModProject modProject = new ModProject(tempPkg)
{
ModVersion = modVersion
ModVersion = modVersion,
Name = title,
ExpectedHash = tempPkg.CalculateHash(name: title, modVersion: modVersion)
};
modProject.Save(stagingFileListPath);
}
@@ -9,6 +9,7 @@ using Barotrauma.Extensions;
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Steamworks;
using Directory = Barotrauma.IO.Directory;
using ItemOrPackage = Barotrauma.Either<Steamworks.Ugc.Item, Barotrauma.ContentPackage>;
using Path = Barotrauma.IO.Path;
@@ -157,7 +158,7 @@ namespace Barotrauma.Steam
}
var selectedTitle =
new GUITextBlock(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), workshopItem.Title ?? localPackage.Name,
new GUITextBlock(new RectTransform((1.0f, 0.05f), mainLayout.RectTransform), localPackage.Name,
font: GUIStyle.LargeFont);
if (workshopItem.Id != 0)
{
@@ -212,7 +213,7 @@ namespace Barotrauma.Steam
};
Label(rightTop, TextManager.Get("WorkshopItemTitle"), GUIStyle.SubHeadingFont);
var titleTextBox = new GUITextBox(NewItemRectT(rightTop), workshopItem.Title ?? localPackage.Name);
var titleTextBox = new GUITextBox(NewItemRectT(rightTop), localPackage.Name);
Label(rightTop, TextManager.Get("WorkshopItemDescription"), GUIStyle.SubHeadingFont);
var descriptionTextBox
@@ -320,7 +321,9 @@ namespace Barotrauma.Steam
workshopItem.Id == 0
? Steamworks.Ugc.Editor.NewCommunityFile
: new Steamworks.Ugc.Editor(workshopItem.Id);
ugcEditor = ugcEditor.WithTitle(titleTextBox.Text)
ugcEditor = ugcEditor
.InLanguage(SteamUtils.SteamUILanguage ?? string.Empty)
.WithTitle(titleTextBox.Text)
.WithDescription(descriptionTextBox.Text)
.WithTags(tagButtons.Where(kvp => kvp.Value.Selected).Select(kvp => kvp.Key.Value))
.WithChangeLog(changeNoteTextBox.Text)
@@ -478,7 +481,7 @@ namespace Barotrauma.Steam
bool stagingReady = false;
Exception? stagingException = null;
TaskPool.Add("CreatePublishStagingCopy",
SteamManager.Workshop.CreatePublishStagingCopy(modVersion, localPackage),
SteamManager.Workshop.CreatePublishStagingCopy(editor.Title ?? localPackage.Name, modVersion, localPackage),
(t) =>
{
stagingReady = true;
@@ -0,0 +1,51 @@
#nullable enable
using System;
using System.Collections.Generic;
namespace Barotrauma
{
internal class LeakyBucket
{
private readonly Queue<Action> queue;
private readonly int capacity;
private readonly float cooldownInSeconds;
private float timer;
public LeakyBucket(float cooldownInSeconds, int capacity)
{
this.cooldownInSeconds = cooldownInSeconds;
this.capacity = capacity;
queue = new Queue<Action>(capacity);
}
public void Update(float deltaTime)
{
if (timer > 0f)
{
timer -= deltaTime;
return;
}
if (queue.Count is 0) { return; }
TryDequeue();
}
private void TryDequeue()
{
timer = cooldownInSeconds;
if (queue.TryDequeue(out var action))
{
action.Invoke();
}
}
public bool TryEnqueue(Action item)
{
if (queue.Count >= capacity) { return false; }
queue.Enqueue(item);
return true;
}
}
}