Release 1.10.5.0 - Autumn Update 2025
This commit is contained in:
@@ -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) :
|
||||
|
||||
+8
-5
@@ -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);
|
||||
|
||||
+14
-13
@@ -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()
|
||||
|
||||
+13
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-1
@@ -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();
|
||||
|
||||
+3
-3
@@ -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()
|
||||
|
||||
+40
-10
@@ -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)
|
||||
|
||||
+9
-4
@@ -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
|
||||
}
|
||||
|
||||
+25
-2
@@ -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);
|
||||
|
||||
+398
@@ -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;
|
||||
}
|
||||
}
|
||||
+178
-24
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user