Release 1.10.5.0 - Autumn Update 2025

This commit is contained in:
Regalis11
2025-09-17 13:44:21 +03:00
parent d13836ce87
commit caa0326cf8
120 changed files with 2584 additions and 635 deletions
@@ -419,7 +419,12 @@ namespace Barotrauma
float scale = Math.Min(targetAreaSize.X / headSprite.size.X, targetAreaSize.Y / headSprite.size.Y);
headSprite.SourceRect = new Rectangle(CalculateOffset(headSprite, Head.SheetIndex.ToPoint()), headSprite.SourceRect.Size);
SetHeadEffect(spriteBatch);
headSprite.Draw(spriteBatch, screenPos, scale: scale, color: Head.SkinColor, spriteEffect: spriteEffects);
Vector2 origin = headSprite.Origin;
if (flip)
{
origin.X = headSprite.size.X - origin.X;
}
headSprite.Draw(spriteBatch, screenPos, origin: origin, scale: scale, color: Head.SkinColor, spriteEffect: spriteEffects);
if (AttachmentSprites != null)
{
float depthStep = 0.000001f;
@@ -467,6 +472,14 @@ namespace Barotrauma
{
origin = head.Origin;
attachment.Sprite.Origin = origin;
if (spriteEffects.HasFlag(SpriteEffects.FlipHorizontally))
{
origin.X = head.size.X - origin.X;
}
if (spriteEffects.HasFlag(SpriteEffects.FlipVertically))
{
origin.Y = head.size.Y - origin.Y;
}
}
else
{
@@ -0,0 +1,174 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
internal static partial class DebugConsole
{
private static void InitShowSoldItems()
{
commands.Add(new Command("showsolditems",
"showsolditems [filter (no-defined/only-min/only-max/name:pattern)] [Include stores (true/false)] [Sold only (true/false)] [limit (number)] [Hide store overrides from output. (true/false)]: " +
"Lists items and their shop availability settings. Filter can be availability filter or name pattern (e.g. 'name:*rifle*'). Include stores controls whether to check store-specific overrides (default true). Sold only controls whether to show only sold items (default true). Limit parameter controls how many items to show (default 50). Hide store overrides from output, defaults to false.",
(string[] args) =>
{
string filter = args.Length > 0 ? args[0].ToLowerInvariant() : null;
bool includeStores = args.Length <= 1 || !args[1].Equals("false", StringComparison.InvariantCultureIgnoreCase);
bool soldOnly = args.Length <= 2 || !args[2].Equals("false", StringComparison.InvariantCultureIgnoreCase);
int limit = 50;
if (args.Length > 3 && int.TryParse(args[3], out int parsedLimit))
{
limit = Math.Max(1, parsedLimit);
}
bool hideStoreOverrides = args.Length > 4 && args[4].Equals("true", StringComparison.InvariantCultureIgnoreCase);
var itemsWithPrice = ItemPrefab.Prefabs
.Where(item => item.ConfigElement.Element.Element("Price") != null);
// apply filtering
var matchingItems = itemsWithPrice
.OrderBy(i => i.Name.Value)
.Where(item => MatchesFilter(item, filter, includeStores, soldOnly))
.ToList();
// output results
NewMessage("=== Shop Item Availability ===", Color.Cyan);
NewMessage($"Filter: {filter ?? "all"}, IncludeStores: {includeStores}, SoldOnly: {soldOnly}, Limit: {limit}, HideStoreOverrides: {hideStoreOverrides}", Color.Yellow);
NewMessage($"Items: {matchingItems.Count} matching out of {itemsWithPrice.Count()} being sold (showing first {Math.Min(limit, matchingItems.Count)})", Color.LightGreen);
NewMessage("", Color.White);
foreach (var item in matchingItems.Take(limit))
{
PrintItemInfo(item, hideStoreOverrides);
}
},
getValidArgs: () =>
[
["all", "no-defined", "only-min", "only-max", "name:*"], // filter
["true", "false"], // includeStores
["true", "false"], // soldOnly
["10", "25", "50", "100"], // limit suggestions
["false", "true"] // hidestoreoverrides
]));
}
private static bool MatchesFilter(ItemPrefab item, string filter, bool includeStores, bool soldOnly)
{
var priceElement = item.ConfigElement.Element.Element("Price");
if (priceElement == null) { return false; } // No price = not sold = don't include
if (!includeStores) { return MatchesPriceElement(priceElement, item, filter, soldOnly); }
// Check if Base element matches first...
if (MatchesPriceElement(priceElement, item, filter, soldOnly)) { return true; }
// ...then check store-specific price element matches
foreach (var storeElement in priceElement.Elements().Where(e => e.Name == "Price"))
{
if (MatchesPriceElement(storeElement, item, filter, soldOnly)) { return true; }
}
return false;
}
private static bool MatchesPriceElement(XElement priceEl, ItemPrefab itemPrefab, string filter, bool soldOnly)
{
bool isSold = PriceInfo.GetSold(priceEl, true);
if (soldOnly && !isSold) { return false; }
if (filter == null) { return true; }
// Handle name pattern matching
if (filter.StartsWith("name:"))
{
string pattern = filter[5..];
string name = itemPrefab.Name.Value.ToLowerInvariant();
string identifier = itemPrefab.Identifier.Value.ToLowerInvariant();
// If pattern contains '*', treat as wildcard (convert to regex)
if (pattern.Contains('*'))
{
// Escape regex special chars except *
string regexPattern = System.Text.RegularExpressions.Regex.Escape(pattern).Replace("\\*", ".*");
return System.Text.RegularExpressions.Regex.IsMatch(name, $"^{regexPattern}$")
|| System.Text.RegularExpressions.Regex.IsMatch(identifier, $"^{regexPattern}$");
}
else
{
// No wildcards: match exactly
return name == pattern || identifier == pattern;
}
}
bool hasMin = PriceInfo.HasMinAmountDefined(priceEl);
bool hasMax = PriceInfo.HasMaxAmountDefined(priceEl);
// Apply the filter logic
return filter switch
{
"no-defined" => !hasMin && !hasMax, // Neither min nor max defined
"only-min" => hasMin && !hasMax, // Only min defined
"only-max" => !hasMin && hasMax, // Only max defined
_ => true // No filter or show all
};
}
private static void PrintItemInfo(ItemPrefab item, bool hideStoreOverrides = false)
{
var priceElement = item.ConfigElement.Element.Element("Price");
if (priceElement == null) { return; }
bool hasMinDefined = PriceInfo.HasMinAmountDefined(priceElement);
bool hasMaxDefined = PriceInfo.HasMaxAmountDefined(priceElement);
string minRaw = PriceInfo.GetMinAmountString(priceElement);
string maxRaw = PriceInfo.GetMaxAmountString(priceElement);
int minLevelDifficulty = PriceInfo.GetMinLevelDifficulty(priceElement, 0);
// Get the resolved values (what PriceInfo would actually use)
var priceInfo = new PriceInfo(priceElement);
int resolvedMin = priceInfo.MinAvailableAmount;
int resolvedMax = priceInfo.MaxAvailableAmount;
string minStatus = hasMinDefined ? $"XML:{minRaw}" : "DEFAULT:1";
string maxStatus = hasMaxDefined ? $"XML:{maxRaw}" : "DEFAULT:5";
string minLevelInfo = minLevelDifficulty > 0 ? $" | MinLvl: {minLevelDifficulty}" : "";
NewMessage($"{item.Name} ({item.Identifier}) | Min: {minStatus} → {resolvedMin} | Max: {maxStatus} → {resolvedMax}{minLevelInfo}", Color.White);
if (hideStoreOverrides) { return; }
var storeOverrides = priceElement.Elements().Where(e => e.Name == "Price")
.Select(p => {
string storeId = PriceInfo.GetStoreIdentifier(p, "unknown");
string storeMin = PriceInfo.GetMinAmountString(p);
string storeMax = PriceInfo.GetMaxAmountString(p);
bool? storeSold = PriceInfo.HasSoldDefined(p) ? PriceInfo.GetSold(p, true) : null;
// Check if this store overrides anything
if (storeMin != null || storeMax != null || storeSold != null)
{
var parts = new List<string>();
if (storeMin != null || storeMax != null)
{
parts.Add($"min:{storeMin ?? "base"}, max:{storeMax ?? "base"}");
}
if (storeSold != null)
{
parts.Add($"sold:{storeSold.Value.ToString().ToLowerInvariant()}");
}
return $"{storeId}({string.Join(", ", parts)})";
}
return null;
})
.Where(s => s != null)
.ToList();
if (storeOverrides.Count != 0)
{
NewMessage($" Store overrides: {string.Join(", ", storeOverrides)}", Color.Gray);
}
}
}
}
@@ -397,6 +397,8 @@ namespace Barotrauma
private static void InitProjectSpecific()
{
InitShowSoldItems();
commands.Add(new Command("eosStat", "Query and display all logged in EOS users. Normally this is at most two users, but in a developer environment it could be more.", args =>
{
if (!EosInterface.Core.IsInitialized)
@@ -3466,6 +3468,13 @@ namespace Barotrauma
}
}
}));
commands.Add(new Command("multiclienttestmode", "Makes the client enable some special logic (such as using a client-specific folder for downloads) to prevent conflicts between multiple clients on the same machine. Useful for testing the campaign with multiple clients running locally.", (string[] args) =>
{
GameClient.MultiClientTestMode = !GameClient.MultiClientTestMode;
NewMessage($"{(GameClient.MultiClientTestMode ? "Enabled" : "Disabled")} MultiClientTestMode on the client.");
}));
AssignRelayToServer("multiclienttestmode", false);
#endif
commands.Add(new Command("reloadcorepackage", "", (string[] args) =>
@@ -4204,8 +4213,12 @@ namespace Barotrauma
NewMessage("Minimum main path width: " + (Level.Loaded.LevelData?.MinMainPathWidth?.ToString() ?? "unknown"));
}
});
}
private static void ReloadWearables(Character character, int variant = 0)
{
foreach (var limb in character.AnimController.Limbs)
@@ -4442,7 +4455,9 @@ namespace Barotrauma
#endif
System.Threading.Thread.Sleep(1000);
}
#if DEBUG
GameClient.MultiClientTestMode = true;
#endif
GameMain.Client = new GameClient("client1",
new LidgrenEndpoint(System.Net.IPAddress.Loopback, NetConfig.DefaultPort), "localhost", Option<int>.None());
@@ -4454,9 +4469,9 @@ namespace Barotrauma
{
System.Threading.Thread.Sleep(1000);
#if WINDOWS
Process.Start("Barotrauma.exe", arguments: "-connect server localhost -username client" + i);
Process.Start("Barotrauma.exe", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode");
#else
Process.Start("./Barotrauma", arguments: "-connect server localhost -username client" + i);
Process.Start("./Barotrauma", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode");
#endif
}
}
@@ -2,7 +2,10 @@
{
partial class GoToMission : Mission
{
public override bool DisplayAsCompleted => State >= Prefab.MaxProgressState;
public override bool DisplayAsCompleted =>
State >= Prefab.MaxProgressState &&
//if there's some additional check for completion, don't display as completed until we've checked it and set the mission as completed
(Completed || completeCheckDataAction == null);
public override bool DisplayAsFailed => false;
}
}
@@ -139,6 +139,8 @@ namespace Barotrauma
}
}
public override IEnumerable<Entity> HudIconTargets => targets.Where(static t => !t.Retrieved && t.Item?.GetRootInventoryOwner() is not Character { IsLocalPlayer: true }).Select(static t => t.Item);
public override IEnumerable<Entity> HudIconTargets => targets
.Where(static t => t.Item != null && !t.Retrieved && t.Item?.GetRootInventoryOwner() is not Character { IsLocalPlayer: true })
.Select(static t => t.Item);
}
}
@@ -6,6 +6,7 @@ using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma
@@ -50,9 +51,15 @@ namespace Barotrauma
}
private RichString selectedTip;
private string selectedTipString;
private ImmutableArray<RichTextData>? selectedTipRichTextData;
private void SetSelectedTip(LocalizedString tip)
{
selectedTip = RichString.Rich(tip);
selectedTipString = string.Empty;
selectedTipRichTextData = null;
}
public float LoadState;
@@ -165,13 +172,20 @@ namespace Barotrauma
textPos.Y += GUIStyle.LargeFont.MeasureString(loadText.ToUpper()).Y * 1.2f;
}
if (GUIStyle.Font.HasValue && selectedTip != null)
if (GUIStyle.Font.HasValue && selectedTip != null && !selectedTip.SanitizedValue.IsNullOrEmpty())
{
string wrappedTip = ToolBox.WrapText(selectedTip.SanitizedValue, GameMain.GraphicsWidth * 0.3f, GUIStyle.Font.Value);
string[] lines = wrappedTip.Split('\n');
float lineHeight = GUIStyle.Font.MeasureString(selectedTip).Y;
//store the string value of the LocalizedString to prevent the text from changing if/when new text packs are loaded during the loading screen
if (selectedTipString.IsNullOrEmpty())
{
selectedTipString = selectedTip.SanitizedValue;
selectedTipRichTextData = selectedTip.RichTextData;
}
if (selectedTip.RichTextData != null)
string wrappedTip = ToolBox.WrapText(selectedTipString, GameMain.GraphicsWidth * 0.3f, GUIStyle.Font.Value);
string[] lines = wrappedTip.Split('\n');
float lineHeight = GUIStyle.Font.MeasureString(selectedTipString).Y;
if (selectedTipRichTextData != null)
{
int rtdOffset = 0;
for (int i = 0; i < lines.Length; i++)
@@ -179,7 +193,7 @@ namespace Barotrauma
GUIStyle.Font.DrawStringWithColors(spriteBatch, lines[i],
new Vector2(textPos.X, (int)(textPos.Y + i * lineHeight)),
Color.White,
0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTip.RichTextData.Value, rtdOffset);
0f, Vector2.Zero, 1f, SpriteEffects.None, 0f, selectedTipRichTextData.Value, rtdOffset);
rtdOffset += lines[i].Length;
}
}
@@ -860,26 +860,22 @@ namespace Barotrauma
FilterStoreItems(category, searchBox.Text);
}
private static KeyValuePair<Identifier, float>? GetReputationRequirement(PriceInfo priceInfo)
private static float GetReputationRequirement(PriceInfo priceInfo, Identifier faction)
{
return GameMain.GameSession?.Campaign is not null
? priceInfo.MinReputation.FirstOrNull()
: null;
return priceInfo.MinReputation.GetValueOrDefault(faction);
}
private static KeyValuePair<Identifier, float>? GetTooLowReputation(PriceInfo priceInfo)
private static bool ReputationRequirementsMet(PriceInfo priceInfo, Identifier faction)
{
if (priceInfo.MinReputation.None()) { return true; }
if (GameMain.GameSession?.Campaign is CampaignMode campaign)
{
foreach (var minRep in priceInfo.MinReputation)
if (priceInfo.MinReputation.TryGetValue(faction, out float requirement))
{
if (MathF.Round(campaign.GetReputation(minRep.Key)) < minRep.Value)
{
return minRep;
}
return MathF.Round(campaign.GetReputation(faction)) >= requirement;
}
}
return null;
return false;
}
int prevDailySpecialCount, prevRequestedGoodsCount, prevSubRequestedGoodsCount;
@@ -948,7 +944,7 @@ namespace Barotrauma
SetPriceGetters(itemFrame, true);
}
SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0 && !GetTooLowReputation(priceInfo).HasValue);
SetItemFrameStatus(itemFrame, hasPermissions && quantity > 0 && ReputationRequirementsMet(priceInfo, ActiveStore.GetMerchantOrLocationFactionIdentifier()));
existingItemFrames.Add(itemFrame);
}
}
@@ -1464,8 +1460,9 @@ namespace Barotrauma
PriceInfo priceInfo2 = item2.ItemPrefab.GetPriceInfo(ActiveStore);
if (priceInfo1 != null && priceInfo2 != null)
{
var requiredReputation1 = GetTooLowReputation(priceInfo1)?.Value ?? 0.0f;
var requiredReputation2 = GetTooLowReputation(priceInfo2)?.Value ?? 0.0f;
Identifier faction = ActiveStore.GetMerchantOrLocationFactionIdentifier();
float requiredReputation1 = ReputationRequirementsMet(priceInfo1, faction) ? 0.0f : GetReputationRequirement(priceInfo1, faction);
float requiredReputation2 = ReputationRequirementsMet(priceInfo2, faction) ? 0.0f : GetReputationRequirement(priceInfo2, faction);
return requiredReputation1.CompareTo(requiredReputation2);
}
return 0;
@@ -1942,14 +1939,15 @@ namespace Barotrauma
var campaign = GameMain.GameSession?.Campaign;
if (priceInfo != null && campaign != null)
{
var requiredReputation = GetReputationRequirement(priceInfo);
if (requiredReputation != null)
Identifier faction = ActiveStore.GetMerchantOrLocationFactionIdentifier();
float requiredReputation = GetReputationRequirement(priceInfo, faction);
if (requiredReputation > 0)
{
var repStr = TextManager.GetWithVariables(
"campaignstore.reputationrequired",
("[amount]", ((int)requiredReputation.Value.Value).ToString()),
("[faction]", TextManager.Get("faction." + requiredReputation.Value.Key).Value));
Color color = MathF.Round(campaign.GetReputation(requiredReputation.Value.Key)) < requiredReputation.Value.Value ?
("[amount]", ((int)requiredReputation).ToString()),
("[faction]", TextManager.Get("faction." + faction).Value));
Color color = MathF.Round(campaign.GetReputation(faction)) < requiredReputation ?
GUIStyle.Orange : GUIStyle.Green;
toolTip += $"\n‖color:{color.ToStringHex()}‖{repStr}‖color:end‖";
}
@@ -1743,12 +1743,12 @@ namespace Barotrauma
}
}
static void CreateMaterialCosts(GUIListBox list, UpgradePrefab prefab, int targetLevel)
static void CreateMaterialCosts(GUIListBox list, UpgradePrefab upgradePrefab, int targetLevel)
{
list.Content.ClearChildren();
var allItems = CargoManager.FindAllItemsOnPlayerAndSub(Character.Controlled);
var resources = prefab.GetApplicableResources(targetLevel);
var resources = upgradePrefab.GetApplicableResources(targetLevel);
foreach (ApplicableResourceCollection collection in resources)
{
@@ -1769,7 +1769,7 @@ namespace Barotrauma
bool hasItems = collection.Cost.Amount <= allItems.Count(collection.Cost.MatchesItem);
Sprite icon = defaultItemPrefab.InventoryIcon ?? prefab.Sprite;
Sprite icon = defaultItemPrefab.InventoryIcon ?? defaultItemPrefab.Sprite;
Color iconColor = defaultItemPrefab.InventoryIcon is null ? defaultItemPrefab.SpriteColor : defaultItemPrefab.InventoryIconColor;
GUIImage itemIcon = new GUIImage(new RectTransform(Vector2.One, itemFrame.RectTransform, scaleBasis: ScaleBasis.Smallest, anchor: Anchor.Center), sprite: icon, scaleToFit: true)
@@ -1798,7 +1798,7 @@ namespace Barotrauma
if (index > length) { index = 0; }
ItemPrefab currentPrefab = collection.MatchingItems[(int)MathF.Floor(index)];
Sprite icon = currentPrefab.InventoryIcon ?? prefab.Sprite;
Sprite icon = currentPrefab.InventoryIcon ?? currentPrefab.Sprite;
Color iconColor = currentPrefab.InventoryIcon is null ? currentPrefab.SpriteColor : currentPrefab.InventoryIconColor;
itemIcon.Sprite = icon;
itemIcon.Color = hasItems ? iconColor : iconColor * 0.9f;
@@ -270,6 +270,13 @@ namespace Barotrauma
ConnectCommand = Option<ConnectCommand>.None();
}
#if DEBUG
if (ConsoleArguments.Contains("-multiclienttestmode"))
{
DebugConsole.NewMessage("Enabled MultiClientTestMode on the client");
GameClient.MultiClientTestMode = true;
}
#endif
GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window);
PerformanceCounter = new PerformanceCounter();
@@ -2866,10 +2866,11 @@ namespace Barotrauma
}
contextualOrders.RemoveAll(o => !IsOrderAvailable(o));
var offsets = MathUtils.GetPointsOnCircumference(Vector2.Zero, nodeDistance, contextualOrders.Count, MathHelper.ToRadians(90f + 180f / contextualOrders.Count));
bool disableNode = !CanCharacterBeHeard();
bool canCharacterBeHeard = !CanCharacterBeHeard();
for (int i = 0; i < contextualOrders.Count; i++)
{
var order = contextualOrders[i];
bool disableNode = !canCharacterBeHeard && !order.TargetAllCharacters;
int hotkey = (i + 1) % 10;
var component = order.Option.IsEmpty ?
CreateOrderNode(nodeSize, commandFrame.RectTransform, offsets[i].ToPoint(), order, hotkey, disableNode: disableNode, checkIfOrderCanBeHeard: false) :
@@ -258,9 +258,9 @@ namespace Barotrauma
{
SlideshowPlayer = new SlideshowPlayer(GUICanvas.Instance, slideshow);
}
var outpost = GameMain.GameSession.Level.StartOutpost;
var borders = outpost.GetDockedBorders();
borders.Location += outpost.WorldPosition.ToPoint();
var subToFocusTo = GameMain.GameSession.Level.StartOutpost ?? Submarine.MainSub;
var borders = subToFocusTo.GetDockedBorders();
borders.Location += subToFocusTo.WorldPosition.ToPoint();
GameMain.GameScreen.Cam.Position = new Vector2(borders.X + borders.Width / 2, borders.Y - borders.Height / 2);
float startZoom = 0.8f /
((float)Math.Max(borders.Width, borders.Height) / (float)GameMain.GameScreen.Cam.Resolution.X);
@@ -646,9 +646,12 @@ namespace Barotrauma
modeElement.Add(GameMain.GameSession?.EventManager.Save());
}
foreach (Identifier unlockedRecipe in GameMain.GameSession.UnlockedRecipes)
foreach ((CharacterTeamType team, Identifier unlockedRecipe) in GameMain.GameSession.UnlockedRecipes)
{
modeElement.Add(new XElement("unlockedrecipe", new XAttribute("identifier", unlockedRecipe)));
modeElement.Add(
new XElement("unlockedrecipe",
new XAttribute("identifier", unlockedRecipe),
new XAttribute("team", team)));
}
//save and remove all items that are in someone's inventory so they don't get included in the sub file as well
@@ -197,8 +197,8 @@ namespace Barotrauma.Items.Components
};
LocalizedString labelText = GetUILabel();
GUITextBlock label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform, Anchor.TopCenter),
labelText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.CenterLeft, wrap: true)
GUITextBlock label = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform, Anchor.TopLeft),
labelText, font: GUIStyle.SubHeadingFont, textAlignment: Alignment.TopLeft, wrap: true)
{
IgnoreLayoutGroups = true
};
@@ -206,6 +206,8 @@ namespace Barotrauma.Items.Components
int buttonSize = GUIStyle.ItemFrameTopBarHeight;
Point margin = new Point(buttonSize / 4, buttonSize / 6);
int buttonCount = 0;
GUILayoutGroup buttonArea = new GUILayoutGroup(new RectTransform(new Point(content.Rect.Width, buttonSize - margin.Y * 2), content.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(0, margin.Y) },
isHorizontal: true, childAnchor: Anchor.TopRight)
{
@@ -213,24 +215,37 @@ namespace Barotrauma.Items.Components
};
if (Inventory.Capacity > 1)
{
new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "SortItemsButton")
if (ShowSortButton)
{
ToolTip = TextManager.Get("SortItemsAlphabetically"),
OnClicked = (btn, userdata) =>
buttonCount++;
new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "SortItemsButton")
{
SortItems();
return true;
}
};
new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "MergeStacksButton")
ToolTip = TextManager.Get("SortItemsAlphabetically"),
OnClicked = (btn, userdata) =>
{
SortItems();
return true;
}
};
}
if (ShowMergeButton)
{
ToolTip = TextManager.Get("MergeItemStacks"),
OnClicked = (btn, userdata) =>
buttonCount++;
new GUIButton(new RectTransform(Vector2.One, buttonArea.RectTransform, scaleBasis: ScaleBasis.Smallest), style: "MergeStacksButton")
{
MergeStacks();
return true;
}
};
ToolTip = TextManager.Get("MergeItemStacks"),
OnClicked = (btn, userdata) =>
{
MergeStacks();
return true;
}
};
}
}
if (buttonCount > 0)
{
label.RectTransform.MaxSize = new Point(label.Parent.Rect.Width - buttonCount * buttonSize, int.MaxValue);
}
float minInventoryAreaSize = 0.5f;
@@ -1078,7 +1078,7 @@ namespace Barotrauma.Items.Components
if (hullData is null)
{
hullData = new HullData();
GetLinkedHulls(hull, hullData.LinkedHulls);
hull.GetLinkedHulls(hullData.LinkedHulls);
hullDatas.Add(hull, hullData);
}
@@ -1586,19 +1586,6 @@ namespace Barotrauma.Items.Components
}
}
public static void GetLinkedHulls(Hull hull, List<Hull> linkedHulls)
{
foreach (var linkedEntity in hull.linkedTo)
{
if (linkedEntity is Hull linkedHull)
{
if (linkedHulls.Contains(linkedHull) || linkedHull.IsHidden) { continue; }
linkedHulls.Add(linkedHull);
GetLinkedHulls(linkedHull, linkedHulls);
}
}
}
public static GUIFrame CreateMiniMap(Submarine sub, GUIComponent parent, MiniMapSettings settings)
{
return CreateMiniMap(sub, parent, settings, null, out _);
@@ -1791,7 +1778,7 @@ namespace Barotrauma.Items.Components
if (combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull))) { continue; }
List<Hull> linkedHulls = new List<Hull>();
GetLinkedHulls(hull, linkedHulls);
hull.GetLinkedHulls(linkedHulls);
linkedHulls.Remove(hull);
@@ -133,7 +133,7 @@ namespace Barotrauma.Items.Components
partial void UpdateProjSpecific(float deltaTime)
{
if (FlowPercentage < 0.0f)
if (currFlow < 0f)
{
foreach (var (position, emitter) in pumpOutEmitters)
{
@@ -154,10 +154,10 @@ namespace Barotrauma.Items.Components
}
emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle,
velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, -FlowPercentage / 100.0f));
velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, -currFlow / maxFlow));
}
}
else if (FlowPercentage > 0.0f)
else if (currFlow > 0f)
{
foreach (var (position, emitter) in pumpInEmitters)
{
@@ -174,7 +174,7 @@ namespace Barotrauma.Items.Components
relativeParticlePos.Y = -relativeParticlePos.Y;
}
emitter.Emit(deltaTime, item.WorldPosition + relativeParticlePos, item.CurrentHull, angle,
velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, FlowPercentage / 100.0f));
velocityMultiplier: MathHelper.Lerp(0.5f, 1.0f, currFlow / maxFlow));
}
}
}
@@ -185,7 +185,7 @@ namespace Barotrauma.Items.Components
{
autoControlIndicator.Selected = IsAutoControlled;
PowerButton.Enabled = isActiveLockTimer <= 0.0f;
if (HasPower)
if (HasPower && !Disabled)
{
flickerTimer = 0;
powerLight.Selected = IsActive;
@@ -229,6 +229,7 @@ namespace Barotrauma.Items.Components
float flowPercentage = msg.ReadRangedInteger(-10, 10) * 10.0f;
bool isActive = msg.ReadBoolean();
bool hijacked = msg.ReadBoolean();
bool disabled = msg.ReadBoolean();
float? targetLevel;
if (msg.ReadBoolean())
{
@@ -250,6 +251,7 @@ namespace Barotrauma.Items.Components
FlowPercentage = flowPercentage;
IsActive = isActive;
Hijacked = hijacked;
Disabled = disabled;
TargetLevel = targetLevel;
}
}
@@ -34,8 +34,8 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize("0.5,0.5)", IsPropertySaveable.No)]
public Vector2 Origin { get; set; } = new Vector2(0.5f, 0.5f);
[Serialize("0.5,0.5", IsPropertySaveable.No)]
public Vector2 Origin { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "")]
public bool BreakFromMiddle
@@ -314,9 +314,12 @@ namespace Barotrauma.Items.Components
textColors.Add(GUIStyle.Orange);
}
int oxygenTextIndex = MathHelper.Clamp((int)Math.Floor((1.0f - (target.Oxygen / 100.0f)) * OxygenTexts.Length), 0, OxygenTexts.Length - 1);
texts.Add(OxygenTexts[oxygenTextIndex]);
textColors.Add(Color.Lerp(GUIStyle.Red, GUIStyle.Green, target.Oxygen / 100.0f));
if (target.NeedsOxygen)
{
int oxygenTextIndex = MathHelper.Clamp((int)Math.Floor((1.0f - (target.Oxygen / 100.0f)) * OxygenTexts.Length), 0, OxygenTexts.Length - 1);
texts.Add(OxygenTexts[oxygenTextIndex]);
textColors.Add(Color.Lerp(GUIStyle.Red, GUIStyle.Green, target.Oxygen / 100.0f));
}
if (target.Bleeding > 0.0f)
{
@@ -359,7 +359,7 @@ namespace Barotrauma
#endif
if (!item.Prefab.UnlockedRecipeInToolTip.IsEmpty && GameMain.GameSession is { } GameSession)
{
if (GameSession.UnlockedRecipes.Contains(item.Prefab.UnlockedRecipeInToolTip))
if (GameSession.HasUnlockedRecipe(Character.Controlled, item.Prefab.UnlockedRecipeInToolTip))
{
toolTip += TextManager.Get("unlockedrecipe.true");
}
@@ -1435,8 +1435,20 @@ namespace Barotrauma
{
if (giver == null || receiver == null || draggedItems.None()) { return false; }
if (receiver == giver) { return false; }
CharacterInventory.AccessLevel accessLevel;
if (draggedItems.Any(it => it.HasTag(Tags.HandLockerItem)))
{
//handcuffs can't be given to players by dragging and dropping (because it can allow handcuffing them)
accessLevel = CharacterInventory.AccessLevel.AllowBotsAndPets;
}
else
{
accessLevel = IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.AllowFriendly : CharacterInventory.AccessLevel.AllowBotsAndPets;
}
return
receiver.IsInventoryAccessibleTo(giver, IsDragAndDropGiveAllowed ? CharacterInventory.AccessLevel.Allowed : CharacterInventory.AccessLevel.Limited) &&
receiver.IsInventoryAccessibleTo(giver, accessLevel) &&
receiver.Inventory.CanBePut(draggedItems.FirstOrDefault(), InvSlotType.Any);
}
@@ -501,10 +501,14 @@ namespace Barotrauma
{
float newCutoff = MathHelper.Lerp(0.0f, 0.65f, Sections[i].damage / MaxHealth);
if (Math.Abs(newCutoff - Submarine.DamageEffectCutoff) > 0.05f)
//change the parameters of the damage effect and start a new sprite batch if the damage is different by 5% or more
if (Math.Abs(newCutoff - Submarine.DamageEffectCutoff) > 0.01f ||
//if we were previously rendering some small amount of damage but now 0 damage, make sure we update the parameters
//"no damage" vs "just a tiny fraction of damage" makes a difference, even though normally 5% differences in damage aren't noticeable
MathUtils.NearlyEqual(newCutoff, 0.0f) != MathUtils.NearlyEqual(Submarine.DamageEffectCutoff, 0.0f))
{
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.BackToFront,
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied, SamplerState.LinearWrap,
null, null,
damageEffect,
@@ -160,37 +160,39 @@ namespace Barotrauma
public static float DamageEffectCutoff;
private static readonly List<Structure> depthSortedDamageable = new List<Structure>();
public static void DrawDamageable(SpriteBatch spriteBatch, Effect damageEffect, bool editing = false, Predicate<MapEntity> predicate = null)
{
if (!editing && visibleEntities != null)
var entitiesToRender = !editing && visibleEntities != null ? visibleEntities : MapEntity.MapEntityList;
depthSortedDamageable.Clear();
//insertion sort according to draw depth
foreach (MapEntity e in entitiesToRender)
{
foreach (MapEntity e in visibleEntities)
if (e is Structure structure && structure.DrawDamageEffect)
{
if (e is Structure structure && structure.DrawDamageEffect)
if (predicate != null)
{
if (predicate != null)
{
if (!predicate(structure)) { continue; }
}
structure.DrawDamage(spriteBatch, damageEffect, editing);
if (!predicate(e)) { continue; }
}
}
}
else
{
foreach (Structure structure in Structure.WallList)
{
if (structure.DrawDamageEffect)
float drawDepth = structure.GetDrawDepth();
int i = 0;
while (i < depthSortedDamageable.Count)
{
if (predicate != null)
{
if (!predicate(structure)) { continue; }
}
structure.DrawDamage(spriteBatch, damageEffect, editing);
float otherDrawDepth = depthSortedDamageable[i].GetDrawDepth();
if (otherDrawDepth < drawDepth) { break; }
i++;
}
depthSortedDamageable.Insert(i, structure);
}
}
foreach (Structure s in depthSortedDamageable)
{
s.DrawDamage(spriteBatch, damageEffect, editing);
}
}
public static void DrawPaintedColors(SpriteBatch spriteBatch, bool editing = false, Predicate<MapEntity> predicate = null)
@@ -287,7 +289,7 @@ namespace Barotrauma
if (combinedHulls.ContainsKey(hull) || combinedHulls.Values.Any(hh => hh.Contains(hull))) { continue; }
List<Hull> linkedHulls = new List<Hull>();
MiniMap.GetLinkedHulls(hull, linkedHulls);
hull.GetLinkedHulls(linkedHulls);
linkedHulls.Remove(hull);
@@ -297,7 +299,6 @@ namespace Barotrauma
{
combinedHulls.Add(hull, new HashSet<Hull>());
}
combinedHulls[hull].Add(linkedHull);
}
}
@@ -567,6 +568,8 @@ namespace Barotrauma
{
if (item.GetComponent<OxygenGenerator>() is not OxygenGenerator oxygenGenerator) { continue; }
oxygenGenerator.GetVents();
Dictionary<Hull, float> hullOxygenFlow = new Dictionary<Hull, float>();
foreach (var linkedTo in item.linkedTo)
@@ -256,6 +256,15 @@ namespace Barotrauma.Networking
}
string downloadFolder = downloadFolders[(FileTransferType)fileType];
#if CLIENT && DEBUG
if (GameClient.MultiClientTestMode)
{
//append the name of the client to the download folder to avoid multiple clients
//from trying to download a file into the same path at the same time
downloadFolder += "_" + GameMain.Client.Name;
}
#endif
if (!Directory.Exists(downloadFolder))
{
try
@@ -11,7 +11,6 @@ using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.PerkBehaviors;
namespace Barotrauma.Networking
{
@@ -26,6 +25,8 @@ namespace Barotrauma.Networking
#if DEBUG
public float DebugServerVoipAmplitude;
public static bool MultiClientTestMode;
#endif
public override Voting Voting { get; }
@@ -873,8 +874,9 @@ namespace Barotrauma.Networking
ReadAchievement(inc);
break;
case ServerPacketHeader.UNLOCKRECIPE:
CharacterTeamType team = (CharacterTeamType)inc.ReadByte();
Identifier identifier = inc.ReadIdentifier();
GameMain.GameSession.UnlockRecipe(identifier, showNotifications: true);
GameMain.GameSession?.UnlockRecipe(team, identifier, showNotifications: true);
break;
case ServerPacketHeader.ACHIEVEMENT_STAT:
ReadAchievementStat(inc);
@@ -10,30 +10,31 @@ sealed class DualStackP2PSocket : P2PSocket
private DualStackP2PSocket(
Callbacks callbacks,
Option<EosP2PSocket> eosSocket,
Option<SteamListenSocket> steamSocket) :
base(callbacks)
Option<SteamListenSocket> steamSocket,
OwnerOrClient type) :
base(callbacks, type)
{
this.eosSocket = eosSocket;
this.steamSocket = steamSocket;
}
public static Result<P2PSocket, Error> Create(Callbacks callbacks)
public static Result<P2PSocket, Error> Create(Callbacks callbacks, OwnerOrClient type)
{
var eosP2PSocketResult = EosP2PSocket.Create(callbacks);
var steamP2PSocketResult = SteamListenSocket.Create(callbacks);
var eosP2PSocketResult = EosP2PSocket.Create(callbacks, type);
var steamP2PSocketResult = SteamListenSocket.Create(callbacks, type);
if (eosP2PSocketResult.TryUnwrapFailure(out var eosError)
&& steamP2PSocketResult.TryUnwrapFailure(out var steamError))
{
return Result.Failure(new Error(eosError, steamError));
}
return Result.Success((P2PSocket)new DualStackP2PSocket(
callbacks,
eosP2PSocketResult.TryUnwrapSuccess(out var eosP2PSocket)
? Option.Some((EosP2PSocket)eosP2PSocket)
: Option.None,
steamP2PSocketResult.TryUnwrapSuccess(out var steamP2PSocket)
? Option.Some((SteamListenSocket)steamP2PSocket)
: Option.None));
return Result.Success<P2PSocket>(new DualStackP2PSocket(
callbacks,
eosP2PSocketResult.TryUnwrapSuccess(out var eosP2PSocket)
? Option.Some((EosP2PSocket)eosP2PSocket)
: Option.None,
steamP2PSocketResult.TryUnwrapSuccess(out var steamP2PSocket)
? Option.Some((SteamListenSocket)steamP2PSocket)
: Option.None, type));
}
public override void ProcessIncomingMessages()
@@ -8,13 +8,14 @@ sealed class EosP2PSocket : P2PSocket
private EosP2PSocket(
Callbacks callbacks,
EosInterface.P2PSocket eosSocket)
: base(callbacks)
EosInterface.P2PSocket eosSocket,
OwnerOrClient type)
: base(callbacks, type)
{
this.eosSocket = eosSocket;
}
public static Result<P2PSocket, Error> Create(Callbacks callbacks)
public static Result<P2PSocket, Error> Create(Callbacks callbacks, OwnerOrClient type)
{
if (!EosInterface.Core.IsInitialized) { return Result.Failure(new Error(ErrorCode.EosNotInitialized)); }
@@ -26,19 +27,25 @@ sealed class EosP2PSocket : P2PSocket
var socketCreateResult = EosInterface.P2PSocket.Create(puids[0], eosSocketId);
if (!socketCreateResult.TryUnwrapSuccess(out var eosSocket)) { return Result.Failure(new Error(ErrorCode.FailedToCreateEosP2PSocket, socketCreateResult.ToString())); }
var retVal = new EosP2PSocket(callbacks, eosSocket);
var retVal = new EosP2PSocket(callbacks, eosSocket, type);
eosSocket.HandleIncomingConnection.Register("Event".ToIdentifier(), retVal.OnIncomingConnection);
eosSocket.HandleClosedConnection.Register("Event".ToIdentifier(), retVal.OnConnectionClosed);
return Result.Success((P2PSocket)retVal);
return Result.Success<P2PSocket>(retVal);
}
public override void ProcessIncomingMessages()
{
foreach (var msg in eosSocket.GetMessageBatch())
{
callbacks.OnData(new EosP2PEndpoint(msg.Sender), new ReadWriteMessage(msg.Buffer, 0, msg.ByteLength * 8, false));
EosP2PEndpoint endpoint = new EosP2PEndpoint(msg.Sender);
callbacks.OnData(endpoint, new ReadWriteMessage(msg.Buffer, 0, msg.ByteLength * 8, false));
if (Type is OwnerOrClient.Owner)
{
dosProtection.OnPacket(endpoint);
}
}
}
@@ -8,6 +8,15 @@ namespace Barotrauma.Networking;
abstract class P2PSocket : IDisposable
{
public readonly P2POwnerDoSProtection dosProtection;
public readonly OwnerOrClient Type;
public enum OwnerOrClient
{
Client,
Owner
}
public enum ErrorCode
{
EosNotInitialized,
@@ -38,12 +47,16 @@ abstract class P2PSocket : IDisposable
public readonly record struct Callbacks(
Predicate<P2PEndpoint> OnIncomingConnection,
Action<P2PEndpoint, PeerDisconnectPacket> OnConnectionClosed,
P2POwnerDoSProtection.ExcessivePacketDelegate OnExcessivePackets,
Action<P2PEndpoint, IReadMessage> OnData);
protected readonly Callbacks callbacks;
protected P2PSocket(Callbacks callbacks)
protected P2PSocket(Callbacks callbacks, OwnerOrClient type)
{
this.callbacks = callbacks;
Type = type;
dosProtection = new P2POwnerDoSProtection(callbacks.OnExcessivePackets);
}
public abstract void ProcessIncomingMessages();
@@ -64,13 +64,13 @@ sealed class SteamConnectSocket : P2PSocket
private readonly SteamP2PEndpoint expectedEndpoint;
private readonly ConnectionManager connectionManager;
private SteamConnectSocket(SteamP2PEndpoint expectedEndpoint, Callbacks callbacks, ConnectionManager connectionManager) : base(callbacks)
private SteamConnectSocket(SteamP2PEndpoint expectedEndpoint, Callbacks callbacks, ConnectionManager connectionManager, OwnerOrClient type) : base(callbacks, type)
{
this.expectedEndpoint = expectedEndpoint;
this.connectionManager = connectionManager;
}
public static Result<P2PSocket, Error> Create(SteamP2PEndpoint endpoint, Callbacks callbacks)
public static Result<P2PSocket, Error> Create(SteamP2PEndpoint endpoint, Callbacks callbacks, OwnerOrClient type)
{
if (!SteamManager.IsInitialized) { return Result.Failure(new Error(ErrorCode.SteamNotInitialized)); }
@@ -87,7 +87,7 @@ sealed class SteamConnectSocket : P2PSocket
if (connectionManager is null) { return Result.Failure(new Error(ErrorCode.FailedToCreateSteamP2PSocket)); }
connectionManager.SetEndpointAndCallbacks(endpoint, callbacks);
return Result.Success((P2PSocket)new SteamConnectSocket(endpoint, callbacks, connectionManager));
return Result.Success<P2PSocket>(new SteamConnectSocket(endpoint, callbacks, connectionManager, type));
}
public override void ProcessIncomingMessages()
@@ -10,12 +10,14 @@ sealed class SteamListenSocket : P2PSocket
private sealed class SocketManager : Steamworks.SocketManager, Steamworks.ISocketManager
{
private Callbacks callbacks;
private P2PSocket socket;
private readonly Dictionary<SteamP2PEndpoint, Steamworks.Data.Connection> endpointToConnection = new();
public void SetCallbacks(Callbacks callbacks)
{
this.callbacks = callbacks;
}
=> this.callbacks = callbacks;
public void SetSocket(P2PSocket socket)
=> this.socket = socket;
public override void OnConnecting(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info)
{
@@ -65,7 +67,7 @@ sealed class SteamListenSocket : P2PSocket
callbacks.OnConnectionClosed(remoteEndpoint, peerDisconnectPacket);
base.OnDisconnected(connection, info);
}
public override void OnMessage(Steamworks.Data.Connection connection, Steamworks.Data.NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel)
{
if (!identity.IsSteamId || data == IntPtr.Zero) { return; }
@@ -75,6 +77,11 @@ sealed class SteamListenSocket : P2PSocket
Marshal.Copy(source: data, destination: dataArray, startIndex: 0, length: size);
callbacks.OnData(endpoint, new ReadWriteMessage(dataArray, bitPos: 0, lBits: size * 8, copyBuf: false));
if (socket?.Type is OwnerOrClient.Owner)
{
socket.dosProtection.OnPacket(endpoint);
}
}
internal bool SendMessage(SteamP2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod)
@@ -107,26 +114,49 @@ sealed class SteamListenSocket : P2PSocket
private SteamListenSocket(
Callbacks callbacks,
SocketManager socketManager)
: base(callbacks)
SocketManager socketManager,
OwnerOrClient type)
: base(callbacks, type)
{
this.socketManager = socketManager;
}
public static Result<P2PSocket, Error> Create(Callbacks callbacks)
public static Result<P2PSocket, Error> Create(Callbacks callbacks, OwnerOrClient type)
{
if (!SteamManager.IsInitialized) { return Result.Failure(new Error(ErrorCode.SteamNotInitialized)); }
var socketManager = Steamworks.SteamNetworkingSockets.CreateRelaySocket<SocketManager>();
if (socketManager is null) { return Result.Failure(new Error(ErrorCode.FailedToCreateSteamP2PSocket)); }
socketManager.SetCallbacks(callbacks);
return Result.Success((P2PSocket)new SteamListenSocket(callbacks, socketManager));
socketManager.SetCallbacks(callbacks);
P2PSocket socket = new SteamListenSocket(callbacks, socketManager, type);
socketManager.SetSocket(socket);
return Result.Success(socket);
}
public override void ProcessIncomingMessages()
{
socketManager.Receive();
const int bufferSize = 32;
const int maxIterations = 100;
// could technically cause a stack overflow since the call is recursive,
// use a while loop instead
int iteration;
for (iteration = 0; iteration < maxIterations; iteration++)
{
int received = socketManager.Receive(bufferSize: bufferSize, receiveToEnd: false);
if (received < bufferSize)
{
break;
}
}
if (iteration >= maxIterations)
{
DebugConsole.ThrowError("Steam P2P socket received too many messages in a single frame.");
}
}
public override bool SendMessage(P2PEndpoint endpoint, IWriteMessage outMsg, DeliveryMethod deliveryMethod)
@@ -59,11 +59,11 @@ namespace Barotrauma.Networking
ServerConnection = ServerEndpoint.MakeConnectionFromEndpoint();
var socketCallbacks = new P2PSocket.Callbacks(OnIncomingConnection, OnConnectionClosed, OnP2PData);
var socketCallbacks = new P2PSocket.Callbacks(OnIncomingConnection, OnConnectionClosed, OnExcessivePackets, OnP2PData);
var socketCreateResult = ServerEndpoint switch
{
EosP2PEndpoint => EosP2PSocket.Create(socketCallbacks),
SteamP2PEndpoint steamP2PEndpoint => SteamConnectSocket.Create(steamP2PEndpoint, socketCallbacks),
EosP2PEndpoint => EosP2PSocket.Create(socketCallbacks, P2PSocket.OwnerOrClient.Client),
SteamP2PEndpoint steamP2PEndpoint => SteamConnectSocket.Create(steamP2PEndpoint, socketCallbacks, P2PSocket.OwnerOrClient.Client),
_ => throw new Exception($"Invalid server endpoint: {ServerEndpoint.GetType()} {ServerEndpoint}")
};
socket = socketCreateResult.TryUnwrapSuccess(out var s)
@@ -97,6 +97,11 @@ namespace Barotrauma.Networking
isActive = true;
}
private void OnExcessivePackets(P2PEndpoint endpoint, bool shouldBan)
{
// do nothing
}
private bool OnIncomingConnection(P2PEndpoint remoteEndpoint)
{
if (remoteEndpoint == ServerEndpoint)
@@ -163,7 +168,7 @@ namespace Barotrauma.Networking
int completeMessageLengthBits = completeMessage.Length * 8;
incomingDataMessages.Add(new ReadWriteMessage(completeMessage.ToArray(), 0, completeMessageLengthBits, copyBuf: false));
}
else if (packetHeader.IsHeartbeatMessage())
else if (packetHeader.IsHeartbeatMessage() || packetHeader.IsDoSProtectionMessage())
{
return; //TODO: implement heartbeats
}
@@ -88,8 +88,8 @@ namespace Barotrauma.Networking
remotePeers.Clear();
var socketCallbacks = new P2PSocket.Callbacks(OnIncomingConnection, OnConnectionClosed, OnP2PData);
var socketCreateResult = DualStackP2PSocket.Create(socketCallbacks);
var socketCallbacks = new P2PSocket.Callbacks(OnIncomingConnection, OnConnectionClosed, OnExcessivePackets, OnP2PData);
var socketCreateResult = DualStackP2PSocket.Create(socketCallbacks, type: P2PSocket.OwnerOrClient.Owner);
socket = socketCreateResult.TryUnwrapSuccess(out var s)
? s
: throw new Exception($"Failed to create dual-stack socket: {socketCreateResult}");
@@ -187,6 +187,29 @@ namespace Barotrauma.Networking
}
}
private void OnExcessivePackets(P2PEndpoint endpoint, bool shouldBan)
{
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteNetSerializableStruct(new P2POwnerToServerHeader
{
EndpointStr = selfPrimaryEndpoint.StringRepresentation,
AccountInfo = selfAccountInfo
});
msg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsDoSProtectionMessage
});
msg.WriteNetSerializableStruct(new DoSProtectionPacket(endpoint.StringRepresentation, shouldBan));
string dcMsg = TextManager.Get(shouldBan ? "DoSProtectionBanned" : "DoSProtectionKicked")
.Fallback(TextManager.Get("DoSProtectionKicked")).Value;
msg.WriteNetSerializableStruct(shouldBan
? PeerDisconnectPacket.Banned(dcMsg)
: PeerDisconnectPacket.Kicked(dcMsg));
ForwardToServerProcess(msg);
}
private void StartAuthTask(IReadMessage inc, RemotePeer remotePeer)
{
remotePeer.AuthStatus = RemotePeer.AuthenticationStatus.AuthenticationPending;
@@ -612,7 +612,7 @@ namespace Barotrauma.Networking
}
public bool Equals(ServerInfo other)
=> other.Endpoints.Any(e => Endpoints.Contains(e));
=> other != null && other.Endpoints.Any(Endpoints.Contains);
public override int GetHashCode() => Endpoints.First().GetHashCode();
@@ -9,6 +9,8 @@ namespace Barotrauma.Networking
{
public partial class ServerLog
{
const int MaxLines = 500;
public GUIButton LogFrame;
private GUIListBox listBox;
private GUIButton reverseButton;
@@ -17,6 +19,8 @@ namespace Barotrauma.Networking
private bool reverseOrder = false;
private readonly bool[] msgTypeHidden = new bool[Enum.GetValues(typeof(MessageType)).Length];
private bool OnReverseClicked(GUIButton btn, object obj)
{
SetMessageReversal(!reverseOrder);
@@ -105,7 +109,10 @@ namespace Barotrauma.Networking
reverseButton.Children.ForEach(c => c.SpriteEffects = reverseOrder ? SpriteEffects.FlipVertically : SpriteEffects.None);
reverseButton.OnClicked = OnReverseClicked;
listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), listBoxLayout.RectTransform));
listBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), listBoxLayout.RectTransform))
{
AutoHideScrollBar = false
};
GUIButton closeButton = new GUIButton(new RectTransform(new Vector2(0.25f, 0.05f), rightColumn.RectTransform), TextManager.Get("Close"))
{
@@ -127,7 +134,8 @@ namespace Barotrauma.Networking
listBox.UpdateScrollBarSize();
if (listBox.BarScroll == 0.0f || listBox.BarScroll == 1.0f) { listBox.BarScroll = 1.0f; }
//scrolled all the way down by default
listBox.BarScroll = 1.0f;
msgFilter = "";
}
@@ -189,11 +197,19 @@ namespace Barotrauma.Networking
{
float prevSize = listBox.BarSize;
GUIComponent firstVisibleLine = listBox.Content.Children.FirstOrDefault(c => c.Rect.Y > listBox.Content.Rect.Y);
int firstVisibileYPos = firstVisibleLine?.Rect.Y ?? 0;
while (listBox.Content.CountChildren > MaxLines)
{
listBox.Content.RemoveChild(reverseOrder ? listBox.Content.Children.Last() : listBox.Content.Children.First());
}
GUIFrame textContainer = null;
Anchor anchor = Anchor.TopLeft;
Pivot pivot = Pivot.TopLeft;
RichString richString = line.Text as RichString;
RichString richString = line.Text;
if (richString != null && richString.RichTextData.HasValue)
{
foreach (var data in richString.RichTextData.Value)
@@ -217,7 +233,7 @@ namespace Barotrauma.Networking
line.Text, wrap: true, font: GUIStyle.SmallFont)
{
TextColor = messageColor[line.Type],
Visible = !msgTypeHidden[(int)line.Type],
Visible = !ShouldFilterMessage(line),
CanBeFocused = false,
UserData = line
};
@@ -247,31 +263,47 @@ namespace Barotrauma.Networking
}
}
if ((prevSize == 1.0f && listBox.BarScroll == 0.0f) || (prevSize < 1.0f && listBox.BarScroll == 1.0f)) listBox.BarScroll = 1.0f;
//if the list was scrolled to the bottom, or to the top while the list wasn't full yet,
//keep it scrolled to the bottom
if ((MathUtils.NearlyEqual(prevSize, 1.0f) && MathUtils.NearlyEqual(listBox.BarScroll, 0.0f)) ||
(prevSize < 1.0f && MathUtils.NearlyEqual(listBox.BarScroll, 1.0f)))
{
listBox.BarScroll = 1.0f;
}
//otherwise modify the scroll so the topmost element stays where it was (list doesn't jump as new lines are added when scrolled up)
else if (firstVisibleLine != null)
{
listBox.UpdateScrollBarSize();
listBox.RecalculateChildren();
int diff = firstVisibleLine.Rect.Y - firstVisibileYPos;
if (diff != 0)
{
listBox.BarScroll += diff / listBox.TotalSize * (prevSize / listBox.BarSize);
}
}
}
private bool FilterMessages()
{
string filter = msgFilter == null ? "" : msgFilter.ToLower();
foreach (GUIComponent child in listBox.Content.Children)
{
if (!(child is GUITextBlock textBlock)) { continue; }
if (child is not GUITextBlock) { continue; }
child.Visible = true;
if (msgTypeHidden[(int)((LogMessage)child.UserData).Type])
{
child.Visible = false;
continue;
}
textBlock.Visible = string.IsNullOrEmpty(filter) || textBlock.Text.ToLower().Contains(filter);
child.Visible = !ShouldFilterMessage((LogMessage)child.UserData);
}
listBox.UpdateScrollBarSize();
listBox.BarScroll = 0.0f;
listBox.BarScroll = 1.0f;
return true;
}
private bool ShouldFilterMessage(LogMessage message)
{
if (msgTypeHidden[(int)message.Type]) { return true; }
string text = message.Text.SanitizedValue;
return !string.IsNullOrEmpty(msgFilter) && !text.Contains(msgFilter, StringComparison.InvariantCultureIgnoreCase);
}
private void SetMessageReversal(bool reverse)
{
if (reverseOrder == reverse) { return; }
@@ -84,7 +84,7 @@ namespace Barotrauma
}
if (IsValidShape(Radius, Height, Width))
{
DrawShape(drawPosition, DrawRotation, color);
DrawShape(DrawPosition, DrawRotation, color);
}
if (LastServerState != null)
@@ -233,7 +233,7 @@ namespace Barotrauma
protected virtual Color BackgroundColor => new Color(150, 150, 150);
private void DrawBack(SpriteBatch spriteBatch)
protected virtual void DrawBack(SpriteBatch spriteBatch)
{
Color outlineColor = Color.White * 0.8f;
Color fontColor = Color.White;
@@ -253,9 +253,19 @@ namespace Barotrauma
GUI.DrawRectangle(spriteBatch, HeaderRectangle, outlineColor, isFilled: false, depth: 1.0f, thickness: (int) Math.Max(1, 1.25f / camZoom));
GUI.DrawRectangle(spriteBatch, bodyRect, outlineColor, isFilled: false, depth: 1.0f, thickness: (int) Math.Max(1, 1.25f / camZoom));
DrawConnections(spriteBatch);
Vector2 headerSize = GUIStyle.SubHeadingFont.MeasureString(Name);
GUIStyle.SubHeadingFont.DrawString(spriteBatch, Name, HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2), fontColor);
}
protected virtual void DrawConnections(SpriteBatch spriteBatch)
{
int x = 0, y = 0;
foreach (EventEditorNodeConnection connection in Connections)
{
if (!ShouldDrawConnection(connection)) { continue; }
switch (connection.Type.NodeSide)
{
case NodeConnectionType.Side.Left:
@@ -268,9 +278,11 @@ namespace Barotrauma
break;
}
}
}
Vector2 headerSize = GUIStyle.SubHeadingFont.MeasureString(Name);
GUIStyle.SubHeadingFont.DrawString(spriteBatch, Name, HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2), fontColor);
protected virtual bool ShouldDrawConnection(EventEditorNodeConnection connection)
{
return true; // Base implementation draws all connections
}
public void AddConnection(NodeConnectionType connectionType)
@@ -337,6 +349,11 @@ namespace Barotrauma
{
private readonly Type type;
protected override Color BackgroundColor =>
EventEditorScreen.ConversationMode && !IsInstanceOf(type, typeof(ConversationAction))
? new Color(80, 80, 80) // Darker for non-conversation nodes in conversation mode
: new Color(150, 150, 150); // Normal color
public EventNode(Type type, string name) : base(name)
{
this.type = type;
@@ -387,8 +404,15 @@ namespace Barotrauma
Type? t = Type.GetType(element.GetAttributeString("type", string.Empty));
if (t == null) { return null; }
string name = element.GetAttributeString("name", string.Empty);
int id = element.GetAttributeInt("i", -1);
EventNode newNode = new EventNode(t, element.GetAttributeString("name", string.Empty)) { ID = element.GetAttributeInt("i", -1) };
// Create the appropriate node type based on whether it's a conversation action
EditorNode newNode = IsInstanceOf(t, typeof(ConversationAction))
? new EventConversationNode(t, name) { ID = id }
: new EventNode(t, name) { ID = id };
float posX = element.GetAttributeFloat("xpos", 0f);
float posY = element.GetAttributeFloat("ypos", 0f);
newNode.Position = new Vector2(posX, posY);
@@ -0,0 +1,398 @@
#nullable enable
using System;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
namespace Barotrauma
{
/// <summary>
/// Base class for event nodes that display text content and can have inner Text nodes
/// </summary>
internal abstract class EventTextDisplayNode(Type type, string name) : EventNode(type, name)
{
protected virtual bool ShowOptions => false;
private new Rectangle HeaderRectangle
{
get
{
if (!EventEditorScreen.ConversationMode) { return base.HeaderRectangle; }
Rectangle drawRect = GetDrawRectangle();
return new Rectangle(Position.ToPoint(), new Point(drawRect.Width, 32));
}
}
public override Rectangle GetDrawRectangle()
{
if (!EventEditorScreen.ConversationMode) { return base.GetDrawRectangle(); }
var textConnection = Connections.Find(c => string.Equals(c.Attribute, "Text", StringComparison.OrdinalIgnoreCase));
var optionConnections = ShowOptions ? Connections.Where(c => c.Type == NodeConnectionType.Option) : Enumerable.Empty<EventEditorNodeConnection>();
const int width = 300;
int height = 50;
// Calculate height for text section
if (textConnection != null)
{
string textContent = GetTextContent(textConnection);
if (!string.IsNullOrEmpty(textContent) && GUIStyle.Font.Value != null)
{
string wrappedText = ToolBox.WrapText(textContent, width - 16, GUIStyle.Font.Value);
Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText);
height += (int)textSize.Y + 10;
}
else
{
height += 25;
}
}
// Calculate height for each option (only for conversation nodes)
if (ShowOptions)
{
int optionIndex = 0;
foreach (var option in optionConnections)
{
string optionText = GetOptionText(option, optionIndex);
if (GUIStyle.Font.Value != null)
{
string wrappedOption = ToolBox.WrapText(optionText, width - 40, GUIStyle.Font.Value);
Vector2 optionSize = GUIStyle.Font.MeasureString(wrappedOption);
height += (int)optionSize.Y + 20;
}
else
{
height += 40;
}
optionIndex++;
}
}
Rectangle rect = Rectangle;
return new Rectangle(rect.X, rect.Y, width, height);
}
protected override void DrawBack(SpriteBatch spriteBatch)
{
if (!EventEditorScreen.ConversationMode)
{
base.DrawBack(spriteBatch);
return;
}
Rectangle bodyRect = GetDrawRectangle();
// Background colors
Color headerColor = IsSelected ? new Color(100, 150, 200) : new Color(120, 170, 220);
Color bodyColor = new Color(90, 120, 150);
Color borderColor = Color.LightBlue;
// Draw background
GUI.DrawRectangle(spriteBatch, HeaderRectangle, headerColor, isFilled: true, depth: 1.0f);
GUI.DrawRectangle(spriteBatch, bodyRect, bodyColor, isFilled: true, depth: 1.0f);
GUI.DrawRectangle(spriteBatch, HeaderRectangle, borderColor, isFilled: false, depth: 1.0f);
GUI.DrawRectangle(spriteBatch, bodyRect, borderColor, isFilled: false, depth: 1.0f);
// Draw header text
Vector2 headerSize = GUIStyle.SubHeadingFont.MeasureString(Name);
Vector2 headerPos = HeaderRectangle.Location.ToVector2() + (HeaderRectangle.Size.ToVector2() / 2) - (headerSize / 2);
GUIStyle.SubHeadingFont.DrawString(spriteBatch, Name, headerPos, Color.White);
// Draw text content
DrawTextContent(spriteBatch, bodyRect);
// Let base class handle standard connections (Activate, Next, etc.)
DrawConnections(spriteBatch);
}
protected virtual void DrawTextContent(SpriteBatch spriteBatch, Rectangle bodyRect)
{
var textConnection = Connections.Find(c => string.Equals(c.Attribute, "Text", StringComparison.OrdinalIgnoreCase));
var optionConnections = ShowOptions ? Connections.Where(c => c.Type == NodeConnectionType.Option) : Enumerable.Empty<EventEditorNodeConnection>();
Vector2 mousePos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition);
mousePos.Y = -mousePos.Y;
const int padding = 8;
int currentY = bodyRect.Y + padding + 30;
// Draw text section
if (textConnection != null)
{
string textContent = GetTextContent(textConnection);
// Wrap text and calculate height
string wrappedText = textContent;
int textHeight = 25;
if (GUIStyle.Font.Value != null)
{
wrappedText = ToolBox.WrapText(textContent, bodyRect.Width - 24, GUIStyle.Font.Value);
Vector2 textSize = GUIStyle.Font.MeasureString(wrappedText);
textHeight = (int)textSize.Y + 10;
}
Rectangle textRect = new Rectangle(bodyRect.X + padding, currentY, bodyRect.Width - padding * 2, textHeight);
// background
GUI.DrawRectangle(spriteBatch, textRect, new Color(70, 100, 130), isFilled: true);
GUI.DrawRectangle(spriteBatch, textRect, Color.CornflowerBlue, isFilled: false);
// wrapped text
Vector2 textPos = new Vector2(textRect.X + 4, textRect.Y + 4);
GUI.DrawString(spriteBatch, textPos, wrappedText, Color.Yellow, font: GUIStyle.Font);
// tooltip
if (textRect.Contains(mousePos))
{
string rawTextKey = GetRawTextKey(textConnection);
if (!string.IsNullOrEmpty(rawTextKey))
{
EventEditorScreen.DrawnTooltip = rawTextKey;
}
}
currentY += textHeight + 5;
}
// Draw options (only for conversation nodes)
if (ShowOptions)
{
DrawOptions(spriteBatch, bodyRect, optionConnections, currentY);
}
}
protected static void DrawOptions(SpriteBatch spriteBatch, Rectangle bodyRect, IEnumerable<EventEditorNodeConnection> optionConnections, int startY)
{
Vector2 mousePos = Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition);
mousePos.Y = -mousePos.Y;
const int padding = 8;
int currentY = startY;
int optionIndex = 0;
foreach (var option in optionConnections)
{
string optionText = GetOptionText(option, optionIndex);
// Wrap option text and calculate height
string wrappedOption = optionText;
int optionHeight = 30;
if (GUIStyle.Font.Value != null)
{
wrappedOption = ToolBox.WrapText(optionText, bodyRect.Width - 40, GUIStyle.Font.Value);
Vector2 optionSize = GUIStyle.Font.MeasureString(wrappedOption);
optionHeight = (int)optionSize.Y + 16;
}
Rectangle optionRect = new Rectangle(bodyRect.X + padding, currentY, bodyRect.Width - padding * 2, optionHeight);
// background - red for end conversation, blue for normal
Color optionBg = option.EndConversation ? new Color(120, 80, 80) : new Color(80, 80, 120);
GUI.DrawRectangle(spriteBatch, optionRect, optionBg, isFilled: true);
GUI.DrawRectangle(spriteBatch, optionRect, Color.White, isFilled: false);
Vector2 optionPos = new Vector2(optionRect.X + 4, optionRect.Y + 4);
GUI.DrawString(spriteBatch, optionPos, wrappedOption, Color.White, font: GUIStyle.Font);
// tooltip
if (optionRect.Contains(mousePos))
{
string rawOptionKey = option.OptionText ?? "";
if (!string.IsNullOrEmpty(rawOptionKey))
{
EventEditorScreen.DrawnTooltip = rawOptionKey;
}
}
// connection point
Rectangle connRect = new Rectangle(bodyRect.Right - 1, optionRect.Y + optionHeight / 2 - 8, 16, 16);
GUI.DrawRectangle(spriteBatch, connRect, Color.DarkGray, isFilled: true);
GUI.DrawRectangle(spriteBatch, connRect, Color.White, isFilled: false);
option.DrawRectangle = connRect;
// connection lines
foreach (var connected in option.ConnectedTo)
{
Vector2 start = new Vector2(connRect.Right, connRect.Center.Y);
Vector2 end = new Vector2(connected.DrawRectangle.Left, connected.DrawRectangle.Center.Y);
float knobLength = 24;
var (points, _) = ToolBox.GetSquareLineBetweenPoints(start, end, knobLength);
Color lineColor = GUIStyle.Red;
float lineWidth = Math.Max(2.0f, 2.0f / (Screen.Selected is EventEditorScreen eventEditor ? eventEditor.Cam.Zoom : 1.0f));
GUI.DrawLine(spriteBatch, points[0], points[1], lineColor, width: (int)lineWidth);
GUI.DrawLine(spriteBatch, points[1], points[2], lineColor, width: (int)lineWidth);
GUI.DrawLine(spriteBatch, points[2], points[3], lineColor, width: (int)lineWidth);
GUI.DrawLine(spriteBatch, points[3], points[4], lineColor, width: (int)lineWidth);
GUI.DrawLine(spriteBatch, points[4], points[5], lineColor, width: (int)lineWidth);
}
currentY += optionHeight + 5;
optionIndex++;
}
}
private static string GetOptionText(EventEditorNodeConnection option, int optionIndex)
{
string optionTextKey = option.OptionText ?? $"Option {optionIndex + 1}";
var allVariants = TextManager.GetAll(optionTextKey);
int variantCount = allVariants.Count();
return variantCount switch
{
> 1 => $"[{variantCount} variants] {string.Join(" / ", allVariants)}",
1 => allVariants.First(),
_ => optionTextKey
};
}
private string GetTextContent(EventEditorNodeConnection textConnection)
{
string textContent = "";
// First check if there's a direct text attribute
if (textConnection.OverrideValue != null)
{
textContent = textConnection.OverrideValue.ToString() ?? "";
}
else
{
object? connectedValue = textConnection.GetValue();
if (connectedValue != null)
{
textContent = connectedValue.ToString() ?? "";
}
}
// If no direct text, check for inner Text nodes via Add connections
if (string.IsNullOrEmpty(textContent))
{
var addConnection = Connections.FirstOrDefault(c => c.Type == NodeConnectionType.Add);
if (addConnection != null && addConnection.ConnectedTo.Any())
{
var connectedNode = addConnection.ConnectedTo.First();
if (connectedNode.Parent?.Name == "Text")
{
// Get the text content from the connected Text node
var textNodeConnection = connectedNode.Parent.Connections.FirstOrDefault(c =>
string.Equals(c.Attribute, "tag", StringComparison.OrdinalIgnoreCase));
if (textNodeConnection?.OverrideValue != null)
{
textContent = textNodeConnection.OverrideValue.ToString() ?? "";
}
}
}
}
// Translate the text if we found any
if (!string.IsNullOrEmpty(textContent))
{
var translated = TextManager.Get(textContent);
if (translated.Loaded)
{
textContent = translated.Value;
}
}
return textContent;
}
private string GetRawTextKey(EventEditorNodeConnection textConnection)
{
string textKey = "";
// First check if there's a direct text attribute
if (textConnection.OverrideValue != null)
{
textKey = textConnection.OverrideValue.ToString() ?? "";
}
else
{
var connectedValue = textConnection.GetValue();
if (connectedValue != null)
{
textKey = connectedValue.ToString() ?? "";
}
}
// If no direct text, check for inner Text nodes via Add connections
if (string.IsNullOrEmpty(textKey))
{
var addConnection = Connections.FirstOrDefault(c => c.Type == NodeConnectionType.Add);
if (addConnection != null && addConnection.ConnectedTo.Any())
{
var connectedNode = addConnection.ConnectedTo.First();
if (connectedNode.Parent?.Name == "Text")
{
// Get the text key from the connected Text node
var textNodeConnection = connectedNode.Parent.Connections.FirstOrDefault(c =>
string.Equals(c.Attribute, "tag", StringComparison.OrdinalIgnoreCase));
if (textNodeConnection?.OverrideValue != null)
{
textKey = textNodeConnection.OverrideValue.ToString() ?? "";
}
}
}
}
return textKey;
}
protected override bool ShouldDrawConnection(EventEditorNodeConnection connection)
{
if (!EventEditorScreen.ConversationMode) { return base.ShouldDrawConnection(connection); }
// In conversation mode, exclude Options and Text since we draw them manually
// Also exclude Add connections since we hide the child Text nodes and display their content inline
return connection.Type == NodeConnectionType.Activate ||
connection.Type == NodeConnectionType.Next;
}
protected override void DrawConnections(SpriteBatch spriteBatch)
{
if (!EventEditorScreen.ConversationMode)
{
base.DrawConnections(spriteBatch);
return;
}
// In conversation mode, use the correct rectangle for connection positioning
Rectangle correctRect = GetDrawRectangle();
int x = 0, y = 0;
foreach (EventEditorNodeConnection connection in Connections)
{
if (!ShouldDrawConnection(connection)) { continue; }
switch (connection.Type.NodeSide)
{
case NodeConnectionType.Side.Left:
connection.Draw(spriteBatch, correctRect, y);
y++;
break;
case NodeConnectionType.Side.Right:
connection.Draw(spriteBatch, correctRect, x);
x++;
break;
}
}
}
}
internal class EventConversationNode(Type type, string name) : EventTextDisplayNode(type, name)
{
protected override bool ShowOptions => true;
}
internal class EventLogNode(Type type, string name) : EventTextDisplayNode(type, name)
{
protected override bool ShowOptions => false;
}
}
@@ -18,6 +18,8 @@ namespace Barotrauma
public override Camera Cam { get; }
public static string? DrawnTooltip { get; set; }
public static bool ConversationMode { get; set; }
public static readonly List<EditorNode> nodeList = new List<EditorNode>();
@@ -37,6 +39,11 @@ namespace Barotrauma
private LocationType? lastTestType;
private GUITickBox? isTraitorEventBox;
private GUITickBox? conversationModeBox;
private readonly LanguageIdentifier originalLanguage;
private GUIDropDown? languageDropdown;
private static int CreateID()
{
@@ -50,25 +57,99 @@ namespace Barotrauma
{
Cam = new Camera();
nodeList.Clear();
originalLanguage = GameSettings.CurrentConfig.Language;
CreateGUI();
}
public override void Select()
{
GUI.PreventPauseMenuToggle = false;
projectName = TextManager.Get("EventEditor.Unnamed").Value;
UpdateLanguageDropdownSelection();
base.Select();
}
private void UpdateLanguageDropdownSelection()
{
if (languageDropdown == null) { return; }
languageDropdown.SelectItem(GameSettings.CurrentConfig.Language);
}
protected override void DeselectEditorSpecific()
{
// Restore the original language when leaving the editor
var config = GameSettings.CurrentConfig;
config.Language = originalLanguage;
GameSettings.SetCurrentConfig(config);
TextManager.LanguageChanged();
}
private static readonly HashSet<EditorNode> hiddenNodesInConversationMode = new HashSet<EditorNode>();
private static bool ShouldHideNodeInConversationMode(EditorNode node)
{
return hiddenNodesInConversationMode.Contains(node);
}
private static void UpdateHiddenNodesInConversationMode()
{
hiddenNodesInConversationMode.Clear();
// Find all text display nodes (ConversationAction and EventLogAction) and mark their inner Text nodes (and descendants) as hidden
foreach (var textDisplayNode in nodeList.Where(IsEventTextDisplayNode))
{
var addConnection = textDisplayNode.Connections.FirstOrDefault(c => c.Type == NodeConnectionType.Add);
if (addConnection != null && addConnection.ConnectedTo.Any())
{
foreach (var connectedNode in addConnection.ConnectedTo)
{
if (connectedNode.Parent?.Name == "Text")
{
MarkNodeAndDescendantsAsHidden(connectedNode.Parent);
}
}
}
}
}
private static bool IsEventTextDisplayNode(EditorNode node) => node is EventTextDisplayNode;
private static void MarkNodeAndDescendantsAsHidden(EditorNode node)
{
hiddenNodesInConversationMode.Add(node);
// Recursively mark all connected child nodes as hidden
foreach (var connection in node.Connections)
{
foreach (var connectedNode in connection.ConnectedTo)
{
if (connectedNode.Parent != null && !hiddenNodesInConversationMode.Contains(connectedNode.Parent))
{
MarkNodeAndDescendantsAsHidden(connectedNode.Parent);
}
}
}
}
private void CreateGUI()
{
GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.4f), GUI.Canvas) { MinSize = new Point(300, 420) });
GuiFrame = new GUIFrame(new RectTransform(new Vector2(0.2f, 0.5f), GUI.Canvas) { MinSize = new Point(300, 520) });
GUILayoutGroup layoutGroup = new GUILayoutGroup(RectTransform(0.9f, 0.9f, GuiFrame, Anchor.Center)) { Stretch = true, AbsoluteSpacing = GUI.IntScale(5) };
// === BUTTONS === //
GUILayoutGroup buttonLayout = new GUILayoutGroup(RectTransform(1.0f, 0.50f, layoutGroup)) { RelativeSpacing = 0.04f };
GUIButton newProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.NewProject"));
GUIButton saveProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.SaveProject"));
GUIButton loadProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.LoadProject"));
GUIButton exportProjectButton = new GUIButton(RectTransform(1.0f, 0.33f, buttonLayout), TextManager.Get("EventEditor.Export"));
GUILayoutGroup buttonLayout = new GUILayoutGroup(RectTransform(1.0f, 0.40f, layoutGroup)) { RelativeSpacing = 0.04f };
GUIButton newProjectButton = new GUIButton(RectTransform(1.0f, 0.25f, buttonLayout), TextManager.Get("EventEditor.NewProject"));
GUIButton saveProjectButton = new GUIButton(RectTransform(1.0f, 0.25f, buttonLayout), TextManager.Get("EventEditor.SaveProject"));
GUIButton loadProjectButton = new GUIButton(RectTransform(1.0f, 0.25f, buttonLayout), TextManager.Get("EventEditor.LoadProject"));
GUIButton exportProjectButton = new GUIButton(RectTransform(1.0f, 0.25f, buttonLayout), TextManager.Get("EventEditor.Export"));
// === LOAD PREFAB === //
GUILayoutGroup loadEventLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup));
GUILayoutGroup loadEventLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup));
new GUITextBlock(RectTransform(1.0f, 0.5f, loadEventLayout), TextManager.Get("EventEditor.LoadEvent"), font: GUIStyle.SubHeadingFont);
GUILayoutGroup loadDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, loadEventLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft);
@@ -76,8 +157,7 @@ namespace Barotrauma
GUIButton loadButton = new GUIButton(RectTransform(0.2f, 1.0f, loadDropdownLayout), TextManager.Get("Load"));
// === ADD ACTION === //
GUILayoutGroup addActionLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup));
GUILayoutGroup addActionLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup));
new GUITextBlock(RectTransform(1.0f, 0.5f, addActionLayout), TextManager.Get("EventEditor.AddAction"), font: GUIStyle.SubHeadingFont);
GUILayoutGroup addActionDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addActionLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft);
@@ -85,7 +165,7 @@ namespace Barotrauma
GUIButton addActionButton = new GUIButton(RectTransform(0.2f, 1.0f, addActionDropdownLayout), TextManager.Get("EventEditor.Add"));
// === ADD VALUE === //
GUILayoutGroup addValueLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup));
GUILayoutGroup addValueLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup));
new GUITextBlock(RectTransform(1.0f, 0.5f, addValueLayout), TextManager.Get("EventEditor.AddValue"), font: GUIStyle.SubHeadingFont);
GUILayoutGroup addValueDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addValueLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft);
@@ -93,7 +173,7 @@ namespace Barotrauma
GUIButton addValueButton = new GUIButton(RectTransform(0.2f, 1.0f, addValueDropdownLayout), TextManager.Get("EventEditor.Add"));
// === ADD SPECIAL === //
GUILayoutGroup addSpecialLayout = new GUILayoutGroup(RectTransform(1.0f, 0.125f, layoutGroup));
GUILayoutGroup addSpecialLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup));
new GUITextBlock(RectTransform(1.0f, 0.5f, addSpecialLayout), TextManager.Get("EventEditor.AddSpecial"), font: GUIStyle.SubHeadingFont);
GUILayoutGroup addSpecialDropdownLayout = new GUILayoutGroup(RectTransform(1.0f, 0.5f, addSpecialLayout), isHorizontal: true, childAnchor: Anchor.CenterLeft);
GUIDropDown addSpecialDropdown = new GUIDropDown(RectTransform(0.8f, 1.0f, addSpecialDropdownLayout), elementCount: 1);
@@ -156,7 +236,45 @@ namespace Barotrauma
return true;
};
isTraitorEventBox = new GUITickBox(RectTransform(1.0f, 0.125f, layoutGroup), "Traitor event");
isTraitorEventBox = new GUITickBox(RectTransform(1.0f, 0.10f, layoutGroup), "Traitor event");
// === CONVERSATION MODE CHECKBOX === //
conversationModeBox = new GUITickBox(RectTransform(1.0f, 0.10f, layoutGroup), "Conversation Mode");
conversationModeBox.Selected = ConversationMode;
conversationModeBox.OnSelected = box =>
{
ConversationMode = !ConversationMode;
UpdateHiddenNodesInConversationMode();
return true;
};
// === LANGUAGE SELECTION === //
GUILayoutGroup languageLayout = new GUILayoutGroup(RectTransform(1.0f, 0.10f, layoutGroup));
new GUITextBlock(RectTransform(1.0f, 0.5f, languageLayout), TextManager.Get("Language"), font: GUIStyle.SubHeadingFont);
var languages = TextManager.AvailableLanguages
.OrderBy(l => TextManager.GetTranslatedLanguageName(l).ToIdentifier());
languageDropdown = new GUIDropDown(RectTransform(1.0f, 0.5f, languageLayout), elementCount: 10);
foreach (var language in languages)
{
languageDropdown.AddItem(TextManager.GetTranslatedLanguageName(language), language);
}
// Select current language
languageDropdown.SelectItem(GameSettings.CurrentConfig.Language);
languageDropdown.OnSelected = (component, userData) =>
{
if (userData is LanguageIdentifier selectedLanguage)
{
var config = GameSettings.CurrentConfig;
config.Language = selectedLanguage;
GameSettings.SetCurrentConfig(config);
TextManager.LanguageChanged();
}
return true;
};
screenResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight);
}
@@ -323,6 +441,9 @@ namespace Barotrauma
{
GUI.NotifyPrompt(TextManager.Get("EventEditor.RandomGenerationHeader"), TextManager.Get("EventEditor.RandomGenerationBody"));
}
// Update hidden nodes after loading
UpdateHiddenNodesInConversationMode();
return true;
});
return true;
@@ -334,7 +455,22 @@ namespace Barotrauma
Vector2 spawnPos = Cam.WorldViewCenter;
spawnPos.Y = -spawnPos.Y;
EventNode newNode = new EventNode(type, type.Name) { ID = CreateID() };
// Create the appropriate node type based on the action type
EditorNode newNode;
if (EditorNode.IsInstanceOf(type, typeof(ConversationAction)))
{
newNode = new EventConversationNode(type, type.Name) { ID = CreateID() };
}
else if (EditorNode.IsInstanceOf(type, typeof(EventLogAction)))
{
newNode = new EventLogNode(type, type.Name) { ID = CreateID() };
}
else
{
newNode = new EventNode(type, type.Name) { ID = CreateID() };
}
newNode.Position = spawnPos - newNode.Size / 2;
nodeList.Add(newNode);
return true;
@@ -399,7 +535,19 @@ namespace Barotrauma
Type? t = Type.GetType($"Barotrauma.{subElement.Name}");
if (t != null && EditorNode.IsInstanceOf(t, typeof(EventAction)))
{
newNode = new EventNode(t, subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() };
// Create the appropriate node type based on the action type
if (EditorNode.IsInstanceOf(t, typeof(ConversationAction)))
{
newNode = new EventConversationNode(t, subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() };
}
else if (EditorNode.IsInstanceOf(t, typeof(EventLogAction)))
{
newNode = new EventLogNode(t, subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() };
}
else
{
newNode = new EventNode(t, subElement.Name.ToString()) { Position = new Vector2(ident, 0), ID = CreateID() };
}
}
else
{
@@ -547,13 +695,6 @@ namespace Barotrauma
return new RectTransform(new Vector2(x, y), parent.RectTransform, anchor);
}
public override void Select()
{
GUI.PreventPauseMenuToggle = false;
projectName = TextManager.Get("EventEditor.Unnamed").Value;
base.Select();
}
public override void AddToGUIUpdateList()
{
GuiFrame.AddToGUIUpdateList();
@@ -671,6 +812,7 @@ namespace Barotrauma
private static void Load(XElement saveElement)
{
nodeList.Clear();
hiddenNodesInConversationMode.Clear();
projectName = saveElement.GetAttributeString("name", TextManager.Get("EventEditor.Unnamed").Value);
foreach (XElement element in saveElement.Elements())
{
@@ -702,6 +844,9 @@ namespace Barotrauma
}
}
}
// Update hidden nodes after loading
UpdateHiddenNodesInConversationMode();
}
private static void CreateContextMenu(EditorNode node, EventEditorNodeConnection? connection = null)
@@ -971,21 +1116,25 @@ namespace Barotrauma
foreach (EditorNode node in nodeList.Where(node => node is SpecialNode))
{
if (ConversationMode && ShouldHideNodeInConversationMode(node)) { continue; }
node.Draw(spriteBatch);
}
// Render value nodes below event nodes
foreach (EditorNode node in nodeList.Where(node => node is ValueNode))
{
if (ConversationMode && ShouldHideNodeInConversationMode(node)) { continue; }
node.Draw(spriteBatch);
}
foreach (EditorNode node in nodeList.Where(node => node is EventNode))
{
if (ConversationMode && ShouldHideNodeInConversationMode(node)) { continue; }
node.Draw(spriteBatch);
}
draggedNode?.Draw(spriteBatch);
foreach (var (node, _) in markedNodes)
{
node.Draw(spriteBatch);
@@ -1013,6 +1162,11 @@ namespace Barotrauma
CreateGUI();
}
if (PlayerInput.KeyHit(Keys.R) && PlayerInput.KeyDown(Keys.LeftShift))
{
CreateGUI();
}
Cam.MoveCamera((float) deltaTime, allowMove: true, allowZoom: GUI.MouseOn == null);
Vector2 mousePos = Cam.ScreenToWorld(PlayerInput.MousePosition);
mousePos.Y = -mousePos.Y;
@@ -319,14 +319,21 @@ namespace Barotrauma
graphics.Clear(Color.Transparent);
DamageEffect.CurrentTechnique = DamageEffect.Techniques["StencilShader"];
DamageEffect.CurrentTechnique.Passes[0].Apply();
spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, SamplerState.LinearWrap, effect: DamageEffect, transformMatrix: cam.Transform);
//reset so any parameters left over from previous usages of the shader don't persist
ResetDamageEffect();
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.LinearWrap, effect: DamageEffect, transformMatrix: cam.Transform);
Submarine.DrawDamageable(spriteBatch, DamageEffect, false);
DamageEffect.Parameters["aCutoff"].SetValue(0.0f);
DamageEffect.Parameters["cCutoff"].SetValue(0.0f);
Submarine.DamageEffectCutoff = 0.0f;
DamageEffect.CurrentTechnique.Passes[0].Apply();
spriteBatch.End();
//reset so parameters set in DrawDamageable don't persist
ResetDamageEffect();
void ResetDamageEffect()
{
DamageEffect.Parameters["aCutoff"].SetValue(0.0f);
DamageEffect.Parameters["cCutoff"].SetValue(0.0f);
Submarine.DamageEffectCutoff = 0.0f;
DamageEffect.CurrentTechnique.Passes[0].Apply();
}
sw.Stop();
GameMain.PerformanceCounter.AddElapsedTicks("Draw:Map:FrontDamageable", sw.ElapsedTicks);
@@ -2090,7 +2090,10 @@ namespace Barotrauma
};
serverLogReverseButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), serverLogListboxLayout.RectTransform), style: "UIToggleButtonVertical");
serverLogBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), serverLogListboxLayout.RectTransform));
serverLogBox = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.95f), serverLogListboxLayout.RectTransform))
{
AutoHideScrollBar = false
};
//filter tickbox list ------------------------------------------------------------------
@@ -2198,7 +2201,7 @@ namespace Barotrauma
OnClicked = (btn, obj) =>
{
if (GameMain.Client == null) { return true; }
GUI.CreateVerificationPrompt(GameMain.GameSession.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd",
GUI.CreateVerificationPrompt(GameMain.GameSession?.GameMode is CampaignMode ? "PauseMenuReturnToServerLobbyVerification" : "EndRoundSubNotAtLevelEnd",
() =>
{
GameMain.Client?.RequestEndRound(save: false);
@@ -3304,11 +3307,17 @@ namespace Barotrauma
VoteType voteType;
if (component.Parent == GameMain.NetLobbyScreen.SubList.Content)
{
if (SelectedMode == GameModePreset.PvP && MultiplayerPreferences.Instance.TeamPreference is not (CharacterTeamType.Team1 or CharacterTeamType.Team2))
if (SelectedMode == GameModePreset.PvP &&
MultiplayerPreferences.Instance.TeamPreference is not (CharacterTeamType.Team1 or CharacterTeamType.Team2))
{
if (TeamPreferenceListBox == null)
{
//refresh player frame to ensure we create the team preference list box
UpdatePlayerFrame(characterInfo: GameMain.Client?.CharacterInfo);
}
// we are in PvP but don't have a team selected, so we can't select a sub
// and also highlight the team selection list
foreach (GUIComponent child in TeamPreferenceListBox.Content.Children)
{
if (child.UserData is CharacterTeamType.None) { continue; }
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Linq;
@@ -42,6 +43,8 @@ namespace Barotrauma
protected override void Update(float deltaTime)
{
if (slideshowPrefab.Slides.IsEmpty) { return; }
var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)];
if (!Visible || (Finished && timer > slide.FadeOutDuration)) { return; }
@@ -104,6 +107,7 @@ namespace Barotrauma
private void RefreshText()
{
if (slideshowPrefab.Slides.IsEmpty) { return; }
var slide = slideshowPrefab.Slides[Math.Min(state, slideshowPrefab.Slides.Length - 1)];
currentText = slide.Text
.Replace("[submarine]", Submarine.MainSub?.Info.Name ?? GameMain.GameSession?.SubmarineInfo?.Name ?? "Unknown")
@@ -2088,7 +2088,7 @@ namespace Barotrauma
if (packageToSaveTo != null)
{
var modProject = new ModProject(packageToSaveTo);
var fileListPath = packageToSaveTo.Path;
string fileListPath = packageToSaveTo.Path;
if (packageToSaveTo == ContentPackageManager.VanillaCorePackage)
{
#if !DEBUG
@@ -2104,10 +2104,12 @@ namespace Barotrauma
SubmarineType.Wreck => "Content/Map/Wrecks/{0}",
SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}",
SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}",
SubmarineType.OutpostModule => MainSub.Info.FilePath.Contains("RuinModules") ? "Content/Map/RuinModules/{0}" : "Content/Map/Outposts/{0}",
SubmarineType.OutpostModule => MainSub.Info.FilePath != null && MainSub.Info.FilePath.Contains("RuinModules") ? "Content/Map/RuinModules/{0}" : "Content/Map/Outposts/{0}",
_ => throw new InvalidOperationException()
}, savePath);
modProject.ModVersion = "";
addSubAndSave(modProject, savePath, fileListPath);
return true;
}
else
{
@@ -2116,27 +2118,41 @@ namespace Barotrauma
if (existingFilePath != null)
{
savePath = existingFilePath;
addSubAndSave(modProject, savePath, fileListPath);
return true;
}
//otherwise make sure we're not trying to overwrite another sub in the same package
else
{
savePath = Path.Combine(packageToSaveTo.Dir, savePath);
if (File.Exists(savePath))
var existingSubInContentPackage =
SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Type == MainSub?.Info?.Type && packageToSaveTo.GetFiles<BaseSubFile>().Any(f => f.Path == s.FilePath));
if (existingSubInContentPackage != null)
{
var verification = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("subeditor.duplicatesubinpackage"),
new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
string directoryName = Path.GetDirectoryName(existingSubInContentPackage.FilePath);
string directoryNameRelativeToPackage = Path.GetRelativePath(Path.GetDirectoryName(packageToSaveTo.Path), directoryName);
var verification = new GUIMessageBox(string.Empty, TextManager.GetWithVariable("subeditor.saveinexistingfolderprompt", "[folder]", directoryNameRelativeToPackage),
[TextManager.GetWithVariable("subeditor.saveinexistingfolderprompt.yes", "[folder]", directoryNameRelativeToPackage), TextManager.Get("subeditor.saveinexistingfolderprompt.no")]);
verification.Buttons[0].OnClicked = (_, _) =>
{
addSubAndSave(modProject, savePath, fileListPath);
savePath = Path.Combine(directoryNameRelativeToPackage, savePath);
trySaveWithDuplicateCheck(modProject, fileListPath);
verification.Close();
return true;
}; verification.Buttons[1].OnClicked = (_, _) =>
{
trySaveWithDuplicateCheck(modProject, fileListPath);
verification.Close();
return true;
};
verification.Buttons[1].OnClicked = verification.Close;
return false;
return true;
}
else
{
trySaveWithDuplicateCheck(modProject, fileListPath);
return true;
}
}
}
addSubAndSave(modProject, savePath, fileListPath);
}
else
{
@@ -2150,9 +2166,28 @@ namespace Barotrauma
{
ModProject modProject = new ModProject { Name = name };
addSubAndSave(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName));
return true;
}
}
void trySaveWithDuplicateCheck(ModProject modProject, string fileListPath)
{
savePath = Path.Combine(packageToSaveTo.Dir, savePath);
if (File.Exists(savePath))
{
var verification = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("subeditor.duplicatesubinpackage"),
[TextManager.Get("yes"), TextManager.Get("no")]);
verification.Buttons[0].OnClicked = (_, _) =>
{
addSubAndSave(modProject, savePath, fileListPath);
verification.Close();
return true;
};
verification.Buttons[1].OnClicked = verification.Close;
}
addSubAndSave(modProject, savePath, fileListPath);
}
void addSubAndSave(ModProject modProject, string filePath, string packagePath)
{
filePath = filePath.CleanUpPath();
@@ -2234,8 +2269,6 @@ namespace Barotrauma
subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width);
}
}
return false;
}
private void CreateSaveScreen(bool quickSave = false)
@@ -2653,13 +2686,15 @@ namespace Barotrauma
//---------------------------------------
var extraSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.5f), subTypeDependentSettingFrame.RectTransform))
var extraSettingsContainer = new GUILayoutGroup(new RectTransform(new Vector2(1, 0.75f), subTypeDependentSettingFrame.RectTransform))
{
CanBeFocused = true,
Visible = false,
Stretch = true
};
var extraSubInfo = GetExtraSubmarineInfo(MainSub?.Info);
var minDifficultyGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), extraSettingsContainer.RectTransform), isHorizontal: true)
{
Stretch = true
@@ -2668,12 +2703,12 @@ namespace Barotrauma
TextManager.Get("minleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true);
var numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), minDifficultyGroup.RectTransform), NumberType.Int)
{
IntValue = (int)(MainSub?.Info?.GetExtraSubmarineInfo?.MinLevelDifficulty ?? 0),
IntValue = (int)(extraSubInfo?.MinLevelDifficulty ?? 0),
MinValueInt = 0,
MaxValueInt = 100,
OnValueChanged = (numberInput) =>
{
MainSub.Info.GetExtraSubmarineInfo.MinLevelDifficulty = numberInput.IntValue;
extraSubInfo.MinLevelDifficulty = numberInput.IntValue;
}
};
minDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize;
@@ -2685,16 +2720,17 @@ namespace Barotrauma
TextManager.Get("maxleveldifficulty"), textAlignment: Alignment.CenterLeft, wrap: true);
numInput = new GUINumberInput(new RectTransform(new Vector2(0.4f, 1.0f), maxDifficultyGroup.RectTransform), NumberType.Int)
{
IntValue = (int)(MainSub?.Info?.GetExtraSubmarineInfo?.MaxLevelDifficulty ?? 100),
IntValue = (int)(extraSubInfo?.MaxLevelDifficulty ?? 100),
MinValueInt = 0,
MaxValueInt = 100,
OnValueChanged = (numberInput) =>
{
MainSub.Info.GetExtraSubmarineInfo.MaxLevelDifficulty = numberInput.IntValue;
extraSubInfo.MaxLevelDifficulty = numberInput.IntValue;
}
};
maxDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize;
GUITextBox missionTagsBox = CreateMissionTagsUI(extraSettingsContainer, extraSubInfo?.MissionTags ?? Enumerable.Empty<Identifier>(), ChangeMissionTags);
//---------------------------------------
@@ -2759,15 +2795,13 @@ namespace Barotrauma
triggerMissionTagsGroup.RectTransform.MaxSize = triggerMissionTagsBox.RectTransform.MaxSize;
//---------------------------------------
var enemySubmarineSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, subTypeDependentSettingFrame.RectTransform))
var enemySubmarineSettingsContainer = new GUILayoutGroup(new RectTransform(Vector2.One, extraSettingsContainer.RectTransform))
{
CanBeFocused = true,
Visible = false,
Stretch = true
};
// -------------------
var enemySubmarineRewardGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), enemySubmarineSettingsContainer.RectTransform), isHorizontal: true)
{
Stretch = true
@@ -2802,26 +2836,6 @@ namespace Barotrauma
}
};
enemySubmarineDifficultyGroup.RectTransform.MaxSize = numInput.TextBox.RectTransform.MaxSize;
var enemySubmarineTagsGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), enemySubmarineSettingsContainer.RectTransform), isHorizontal: true)
{
Stretch = true
};
new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), enemySubmarineTagsGroup.RectTransform),
TextManager.Get("sp.item.tags.name"), textAlignment: Alignment.CenterLeft, wrap: true);
var tagsBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), enemySubmarineTagsGroup.RectTransform))
{
OnEnterPressed = ChangeEnemySubTags,
OverflowClip = true,
Text = "default"
};
tagsBox.OnDeselected += (textbox, _) => ChangeEnemySubTags(textbox, textbox.Text);
if (MainSub?.Info?.EnemySubmarineInfo?.MissionTags != null)
{
tagsBox.Text = string.Join(',', MainSub.Info.EnemySubmarineInfo.MissionTags);
}
enemySubmarineTagsGroup.RectTransform.MaxSize = tagsBox.RectTransform.MaxSize;
enemySubmarineSettingsContainer.RectTransform.MinSize = new Point(0, enemySubmarineSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0));
//--------------------------------------------------------
@@ -2870,7 +2884,6 @@ namespace Barotrauma
return true;
}
};
beaconSettingsContainer.RectTransform.MinSize = new Point(0, beaconSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0));
//------------------------------------------------------------------
@@ -3110,13 +3123,26 @@ namespace Barotrauma
{
MainSub.Info.EnemySubmarineInfo ??= new EnemySubmarineInfo(MainSub.Info);
}
// Update mission tags UI when submarine type changes
var newExtraSubInfo = GetExtraSubmarineInfo(MainSub.Info);
if (newExtraSubInfo != null)
{
missionTagsBox.Text = string.Join(',', newExtraSubInfo.MissionTags);
}
previewImageButtonHolder.Children.ForEach(c => c.Enabled = MainSub.Info.AllowPreviewImage);
outpostModuleSettingsContainer.Visible = type == SubmarineType.OutpostModule;
extraSettingsContainer.Visible = type == SubmarineType.BeaconStation || type == SubmarineType.Wreck;
extraSettingsContainer.Visible = newExtraSubInfo != null;
beaconSettingsContainer.Visible = type == SubmarineType.BeaconStation;
beaconSettingsContainer.IgnoreLayoutGroups = !beaconSettingsContainer.Visible;
enemySubmarineSettingsContainer.Visible = type == SubmarineType.EnemySubmarine;
enemySubmarineSettingsContainer.IgnoreLayoutGroups = !enemySubmarineSettingsContainer.Visible;
subSettingsContainer.Visible = type == SubmarineType.Player;
outpostSettingsContainer.Visible = type == SubmarineType.Outpost;
extraSettingsContainer.Recalculate();
return true;
};
subSettingsContainer.RectTransform.MinSize = new Point(0, subSettingsContainer.RectTransform.Children.Sum(c => c.Children.Any() ? c.Children.Max(c2 => c2.MinSize.Y) : 0));
@@ -3412,6 +3438,18 @@ namespace Barotrauma
if (quickSave) { SaveSub(packageToSaveInList.SelectedData as ContentPackage); }
}
private static ExtraSubmarineInfo GetExtraSubmarineInfo(SubmarineInfo subInfo)
{
if (subInfo == null) { return null; }
return subInfo.Type switch
{
SubmarineType.BeaconStation => subInfo.BeaconStationInfo,
SubmarineType.Wreck => subInfo.WreckInfo,
SubmarineType.EnemySubmarine => subInfo.EnemySubmarineInfo,
_ => null,
};
}
private void CreateSaveAssemblyScreen()
{
SetMode(Mode.Default);
@@ -4948,29 +4986,46 @@ namespace Barotrauma
return true;
}
private bool ChangeEnemySubTags(GUITextBox textBox, string text)
private bool ChangeMissionTags(GUITextBox textBox, string text)
{
if (string.IsNullOrWhiteSpace(text))
{
textBox.Flash(GUIStyle.Red);
return false;
}
// Get the ExtraSubmarineInfo (all types inherit MissionTags from the parent class)
var extraSubInfo = GetExtraSubmarineInfo(MainSub?.Info);
if (MainSub.Info.EnemySubmarineInfo is { } enemySubInfo)
if (extraSubInfo?.MissionTags != null)
{
enemySubInfo.MissionTags.Clear();
extraSubInfo.MissionTags.Clear();
string[] tags = text.Split(',');
foreach (string tag in tags)
{
enemySubInfo.MissionTags.Add(tag.ToIdentifier());
extraSubInfo.MissionTags.Add(tag.ToIdentifier());
}
}
textBox.Text = text;
textBox.Flash(GUIStyle.Green);
return true;
}
private static GUITextBox CreateMissionTagsUI(GUIComponent parent, IEnumerable<Identifier> missionTags, GUITextBox.OnEnterHandler onEnterPressed)
{
var missionTagsGroup = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.25f), parent.RectTransform), isHorizontal: true)
{
Stretch = true
};
new GUITextBlock(new RectTransform(new Vector2(0.6f, 1.0f), missionTagsGroup.RectTransform),
TextManager.Get("subeditor.missiontags"), textAlignment: Alignment.CenterLeft, wrap: true);
var tagsBox = new GUITextBox(new RectTransform(new Vector2(0.4f, 1.0f), missionTagsGroup.RectTransform))
{
ToolTip = TextManager.Get("subeditor.missiontags.tooltip"),
OnEnterPressed = onEnterPressed,
OverflowClip = true,
Text = missionTags != null ? string.Join(',', missionTags) : ""
};
tagsBox.OnDeselected += (textbox, _) => onEnterPressed(textbox, textbox.Text);
missionTagsGroup.RectTransform.MaxSize = tagsBox.RectTransform.MaxSize;
return tagsBox;
}
private void ChangeSubDescription(GUITextBox textBox, string text)
{
if (MainSub != null)
@@ -5851,7 +5906,8 @@ namespace Barotrauma
if (entity is Item item && item.Components.Any(ic => ic is not ConnectionPanel && ic is not Repairable && ic.GuiFrame != null))
{
var container = item.GetComponents<ItemContainer>().ToList();
if (!container.Any() || container.Any(ic => ic?.DrawInventory ?? false))
if (container.None() || container.Any(ic => ic?.DrawInventory ?? false) ||
item.GetComponent<CircuitBox>() != null)
{
OpenItem(item);
break;
@@ -36,11 +36,18 @@ namespace Barotrauma
{
public bool IsFiltered(ServerInfo info)
{
if (!Filters.Any()) { return false; }
if (Filters.IsEmpty) { return false; }
foreach (var (type, value) in Filters)
{
if (!IsFiltered(info, type, value)) { return false; }
try
{
if (!IsFiltered(info, type, value)) { return false; }
}
catch (Exception e)
{
DebugConsole.ThrowError($"Failed to check filter type {type} on the server info {(info.ServerName ?? "null")}.", e);
}
}
return true;
@@ -63,7 +70,9 @@ namespace Barotrauma
SpamServerFilterType.MessageEquals => CompareEquals(desc, value),
SpamServerFilterType.MessageContains => CompareContains(desc, value),
SpamServerFilterType.Endpoint => info.Endpoints.First().StringRepresentation.Equals(value, StringComparison.OrdinalIgnoreCase),
SpamServerFilterType.Endpoint =>
info.Endpoints != null &&
info.Endpoints.First().StringRepresentation.Equals(value, StringComparison.OrdinalIgnoreCase),
SpamServerFilterType.PlayerCountLarger => info.PlayerCount > parsedInt,
SpamServerFilterType.PlayerCountExact => info.PlayerCount == parsedInt,
@@ -79,10 +88,23 @@ namespace Barotrauma
};
static bool CompareEquals(string a, string b)
=> a.Equals(b, StringComparison.OrdinalIgnoreCase) || Homoglyphs.Compare(a, b);
{
if (a == null || b == null)
{
return a == b;
}
return a.Equals(b, StringComparison.OrdinalIgnoreCase) || Homoglyphs.Compare(a, b);
}
static bool CompareContains(string a, string b)
=> a.Contains(b, StringComparison.OrdinalIgnoreCase);
{
if (a == null || b == null)
{
return a == b;
}
return a.Contains(b, StringComparison.OrdinalIgnoreCase);
}
}
public XElement Serialize()
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Networking;
namespace Barotrauma
{
internal sealed class P2POwnerDoSProtection
{
/// <summary>
/// Delegate to be called when a client has sent too many packets in a short time.
/// </summary>
/// <param name="endpoint">The endpoint of the client.</param>
/// <param name="shouldBan">A suggestion to ban the client due to too many kicks.</param>
public delegate void ExcessivePacketDelegate(P2PEndpoint endpoint, bool shouldBan);
private readonly Dictionary<P2PEndpoint, int> packetCounts = new();
private readonly Dictionary<P2PEndpoint, int> kicksByEndpoint = new();
private readonly ExcessivePacketDelegate onExcessivePackets;
private double nextCheckTime;
// check every 10 seconds
private const int PacketCheckTimer = 10;
public P2POwnerDoSProtection(ExcessivePacketDelegate onExcessivePackets)
{
this.onExcessivePackets = onExcessivePackets;
nextCheckTime = Timing.TotalTime + PacketCheckTimer;
}
private static int MaxPacketCount
{
get
{
// Normally the packet limit is per second, but we want to check faster than that.
// multiply by 1.2 to allow for some leeway to allow the DoS protection deeper in the stack
// to handle this first.
const float limitMultiplier = (PacketCheckTimer / 60f) * 1.2f;
if (GameMain.Client?.ServerSettings is not { } serverSettings)
{
// Shouldn't happen, but just in case.
return (int)MathF.Ceiling(ServerSettings.PacketLimitDefault * limitMultiplier);
}
return (int)MathF.Ceiling(serverSettings.MaxPacketAmount * MathF.Max(serverSettings.TickRate / (float)ServerSettings.DefaultTickRate, 1f) * limitMultiplier);
}
}
private static bool ShouldCheck()
{
if (GameMain.Client?.ServerSettings is { } serverSettings)
{
return serverSettings.EnableDoSProtection && serverSettings.MaxPacketAmount > ServerSettings.PacketLimitMin;
}
return false;
}
public void OnPacket(P2PEndpoint endpoint)
{
if (!ShouldCheck()) { return; }
// count = default(int), if the endpoint is not in the dictionary
packetCounts.TryGetValue(endpoint, out int count);
packetCounts[endpoint] = ++count;
if (Timing.TotalTime > nextCheckTime)
{
foreach (P2PEndpoint e in packetCounts.Keys.ToArray())
{
CheckForExcessivePackets(e, count);
}
packetCounts.Clear();
nextCheckTime = Timing.TotalTime + PacketCheckTimer;
}
}
private void CheckForExcessivePackets(P2PEndpoint endpoint, int count)
{
if (count > MaxPacketCount)
{
kicksByEndpoint.TryGetValue(endpoint, out int kickCount);
kicksByEndpoint[endpoint] = ++kickCount;
onExcessivePackets(endpoint, kickCount > 3);
packetCounts.Remove(endpoint);
}
}
}
}