Unstable v0.19.5.0

This commit is contained in:
Juan Pablo Arce
2022-09-14 12:47:17 -03:00
parent 3f2c843247
commit 1fd2a51bbb
158 changed files with 5702 additions and 4813 deletions

View File

@@ -1,5 +1,6 @@
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Barotrauma.Tutorials;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
@@ -398,36 +399,58 @@ namespace Barotrauma
progressBar.Draw(spriteBatch, cam);
}
foreach (Character npc in Character.CharacterList)
void DrawInteractionIcon(Entity entity, string iconStyle)
{
if (npc.CampaignInteractionType == CampaignMode.InteractionType.None || npc.Submarine != character.Submarine || npc.IsDead || npc.IsIncapacitated) { continue; }
if (entity == null || entity.Removed) { return; }
var characterEntity = entity as Character;
if (characterEntity is not null && (characterEntity.IsDead || characterEntity.IsIncapacitated)) { return; }
if (GUIStyle.GetComponentStyle(iconStyle) is not GUIComponentStyle style) { return; }
var iconStyle = GUIStyle.GetComponentStyle("CampaignInteractionIcon." + npc.CampaignInteractionType);
if (iconStyle == null) { continue; }
Range<float> visibleRange = new Range<float>(npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity);
if (npc.CampaignInteractionType == CampaignMode.InteractionType.Examine)
Hull currentHull = entity switch
{
Character character => character.CurrentHull,
Item item => item.CurrentHull,
_ => null
};
Range<float> visibleRange = new Range<float>(currentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity);
if (characterEntity?.CampaignInteractionType == CampaignMode.InteractionType.Examine)
{
//TODO: we could probably do better than just hardcoding
//a check for InteractionType.Examine here.
if (Vector2.DistanceSquared(character.Position, npc.Position) > 500f * 500f) { continue; }
if (Vector2.DistanceSquared(character.Position, entity.Position) > 500f * 500f) { return; }
var body = Submarine.CheckVisibility(character.SimPosition, npc.SimPosition, ignoreLevel: true);
if (body != null && body.UserData as Character != npc) { continue; }
var body = Submarine.CheckVisibility(character.SimPosition, entity.SimPosition, ignoreLevel: true);
if (body != null && body.UserData != entity) { return; }
visibleRange = new Range<float>(-100f, 500f);
}
float dist = Vector2.Distance(character.WorldPosition, npc.WorldPosition);
float dist = Vector2.Distance(character.WorldPosition, entity.WorldPosition);
float distFactor = 1.0f - MathUtils.InverseLerp(1000.0f, 3000.0f, dist);
float alpha = MathHelper.Lerp(0.3f, 1.0f, distFactor);
GUI.DrawIndicator(
spriteBatch,
npc.WorldPosition,
entity.WorldPosition,
cam,
visibleRange,
iconStyle.GetDefaultSprite(),
iconStyle.Color * alpha,
label: npc.Info?.Title);
style.GetDefaultSprite(),
style.Color * alpha,
label: characterEntity?.Info?.Title);
}
foreach (Character npc in Character.CharacterList)
{
if (npc.CampaignInteractionType == CampaignMode.InteractionType.None) { continue; }
DrawInteractionIcon(npc, "CampaignInteractionIcon." + npc.CampaignInteractionType);
}
if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode && tutorialMode.Tutorial is not null)
{
foreach (var (entity, iconStyle) in tutorialMode.Tutorial.Icons)
{
DrawInteractionIcon(entity, iconStyle);
}
}
foreach (Item item in Item.ItemList)

View File

@@ -521,8 +521,9 @@ namespace Barotrauma
Color skinColor = inc.ReadColorR8G8B8();
Color hairColor = inc.ReadColorR8G8B8();
Color facialHairColor = inc.ReadColorR8G8B8();
string ragdollFile = inc.ReadString();
string ragdollFile = inc.ReadString();
Identifier npcId = inc.ReadIdentifier();
uint jobIdentifier = inc.ReadUInt32();
int variant = inc.ReadByte();
@@ -539,9 +540,9 @@ namespace Barotrauma
}
// TODO: animations
CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant)
CharacterInfo ch = new CharacterInfo(speciesName, newName, originalName, jobPrefab, ragdollFile, variant, npcIdentifier: npcId)
{
ID = infoID,
ID = infoID
};
ch.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
ch.Head.SkinColor = skinColor;

View File

@@ -63,7 +63,7 @@ namespace Barotrauma
files = contentPackage.Files.Select(File.FromContentFile).ToList();
ModVersion = IncrementModVersion(contentPackage.ModVersion);
IsCore = contentPackage is CorePackage;
SteamWorkshopId = contentPackage.SteamWorkshopId;
UgcId = contentPackage.UgcId;
ExpectedHash = contentPackage.Hash;
InstallTime = contentPackage.InstallTime;
}
@@ -90,9 +90,9 @@ namespace Barotrauma
public bool IsCore = false;
public UInt64 SteamWorkshopId = 0;
public Option<ContentPackageId> UgcId = Option<ContentPackageId>.None();
public DateTime? InstallTime = null;
public Option<DateTime> InstallTime = Option<DateTime>.None();
public bool HasFile(File file)
=> Files.Any(f =>
@@ -120,7 +120,7 @@ namespace Barotrauma
public void DiscardHashAndInstallTime()
{
ExpectedHash = null;
InstallTime = null;
InstallTime = Option<DateTime>.None();
}
public static string IncrementModVersion(string modVersion)
@@ -155,11 +155,11 @@ namespace Barotrauma
addRootAttribute("name", Name);
if (!ModVersion.IsNullOrEmpty()) { addRootAttribute("modversion", ModVersion); }
addRootAttribute("corepackage", IsCore);
if (SteamWorkshopId != 0) { addRootAttribute("steamworkshopid", SteamWorkshopId); }
if (UgcId.TryUnwrap(out var ugcId) && ugcId is SteamWorkshopId steamWorkshopId) { addRootAttribute("steamworkshopid", steamWorkshopId.Value); }
addRootAttribute("gameversion", GameMain.Version);
if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); }
if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); }
if (InstallTime != null) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(InstallTime.Value)); }
if (InstallTime.TryUnwrap(out var installTime)) { addRootAttribute("installtime", ToolBox.Epoch.FromDateTime(installTime)); }
files.ForEach(f => rootElement.Add(f.ToXElement()));

View File

@@ -45,9 +45,11 @@ namespace Barotrauma
var needInstalling = subscribedItems.Where(item
=> !WorkshopPackages.Any(p
=> item.Id == p.SteamWorkshopId
&& p.InstallTime.HasValue
&& item.LatestUpdateTime <= p.InstallTime))
=> p.UgcId.TryUnwrap(out var ugcId)
&& ugcId is SteamWorkshopId workshopId
&& item.Id == workshopId.Value
&& p.InstallTime.TryUnwrap(out var installTime)
&& item.LatestUpdateTime <= installTime))
.ToArray();
if (needInstalling.Any())
{

View File

@@ -16,7 +16,7 @@ namespace Barotrauma.Transition
/// Class dedicated to transitioning away from the old, shitty
/// Mods + Submarines folders to the new LocalMods folder
/// </summary>
public static class UgcTransition
public static class LegacySteamUgcTransition
{
private const string readmeName = "LOCALMODS_README.txt";
@@ -168,7 +168,11 @@ namespace Barotrauma.Transition
addHeader(TextManager.Get("SubscribedMods"));
foreach (var mod in mods.Mods)
{
addTickbox(mod.Dir, mod.Name, ticked: !ContentPackageManager.LocalPackages.Any(p => p.SteamWorkshopId != 0 && p.SteamWorkshopId == mod.Item?.Id));
addTickbox(mod.Dir, mod.Name,
ticked: !(mod.Item is { } item && ContentPackageManager.LocalPackages.Any(p =>
p.UgcId.TryUnwrap(out var ugcId)
&& ugcId is SteamWorkshopId workshopId
&& workshopId.Value == item.Id)));
}
}

View File

@@ -2489,26 +2489,8 @@ namespace Barotrauma
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
}));
#if DEBUG
commands.Add(new Command("playovervc", "Plays a sound over voice chat.", (args) =>
{
VoipCapture.Instance?.SetOverrideSound(args.Length > 0 ? args[0] : null);
}));
commands.Add(new Command("querylobbies", "Queries all SteamP2P lobbies", (args) =>
{
TaskPool.Add("DebugQueryLobbies",
SteamManager.LobbyQueryRequest(), (t) =>
{
t.TryGetResult(out List<Steamworks.Data.Lobby> lobbies);
foreach (var lobby in lobbies)
{
NewMessage(lobby.GetData("name") + ", " + lobby.GetData("lobbyowner"), Color.Yellow);
}
NewMessage($"Retrieved a total of {lobbies.Count} lobbies", Color.Lime);
});
}));
commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) =>
{
if (args.Length != 1) { return; }
@@ -3056,11 +3038,6 @@ namespace Barotrauma
commands.Add(new Command("reloadwearables", "Reloads the sprites of all limbs and wearable sprites (clothing) of the controlled character. Provide id or name if you want to target another character.", args =>
{
if (GameMain.GameSession != null)
{
ThrowError("Using the command is not allowed during an active game session: the command is intended to be used in the character editor or in the main menu.");
return;
}
var character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, true);
if (character == null)
{
@@ -3072,11 +3049,6 @@ namespace Barotrauma
commands.Add(new Command("loadwearable", "Force select certain variant for the selected character.", args =>
{
if (GameMain.GameSession != null)
{
ThrowError("Using the command is not allowed during an active game session: the command is intended to be used in the character editor or in the main menu.");
return;
}
var character = Character.Controlled;
if (character == null)
{

View File

@@ -0,0 +1,73 @@
using Barotrauma.Tutorials;
using System;
using System.Linq;
namespace Barotrauma;
partial class MessageBoxAction : EventAction
{
partial void UpdateProjSpecific()
{
if (Type == ActionType.Create)
{
CreateMessageBox();
if (!ObjectiveTag.IsEmpty && GameMain.GameSession?.GameMode is TutorialMode tutorialMode)
{
Identifier id = Identifier.IsEmpty ? Text : Identifier;
var segment = Tutorial.Segment.CreateMessageBoxSegment(id, ObjectiveTag, CreateMessageBox);
tutorialMode.Tutorial?.TriggerTutorialSegment(segment);
}
}
else if (Type == ActionType.Close)
{
GUIMessageBox.Close(Tag);
}
}
public void CreateMessageBox()
{
new GUIMessageBox(
headerText: TextManager.Get(Header),
text: RichString.Rich(TextManager.ParseInputTypes(TextManager.Get(Text).Fallback(Text.ToString()), useColorHighlight: true)),
buttons: Array.Empty<LocalizedString>(),
type: GUIMessageBox.Type.Tutorial,
tag: Tag,
iconStyle: IconStyle,
autoCloseCondition: GetAutoCloseCondition(),
hideCloseButton: HideCloseButton)
{
FlashOnAutoCloseCondition = true
};
}
private Func<bool> GetAutoCloseCondition()
{
var character = ParentEvent.GetTargets(TargetTag).FirstOrDefault() as Character;
Func<bool> autoCloseCondition = null;
if (!string.IsNullOrEmpty(CloseOnInput) && Enum.TryParse(CloseOnInput, true, out InputType closeOnInput))
{
autoCloseCondition = () => PlayerInput.KeyDown(closeOnInput);
}
else if (!CloseOnSelectTag.IsEmpty)
{
autoCloseCondition = () => character?.SelectedItem != null && character.SelectedItem.HasTag(CloseOnSelectTag);
}
else if (!CloseOnPickUpTag.IsEmpty)
{
autoCloseCondition = () => character?.Inventory != null && character.Inventory.FindItemByTag(CloseOnPickUpTag, recursive: true) != null;
}
else if (!CloseOnEquipTag.IsEmpty)
{
autoCloseCondition = () => character != null && character.HasEquippedItem(CloseOnEquipTag);
}
else if (!CloseOnExitRoomName.IsEmpty)
{
autoCloseCondition = () => character?.CurrentHull == null || character.CurrentHull.RoomName.ToIdentifier() != CloseOnExitRoomName;
}
else if (!CloseOnInRoomName.IsEmpty)
{
autoCloseCondition = () => character?.CurrentHull != null && character.CurrentHull.RoomName.ToIdentifier() == CloseOnInRoomName;
}
return autoCloseCondition;
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.Xna.Framework;
namespace Barotrauma;
partial class TutorialHighlightAction : EventAction
{
private static readonly Color highlightColor = Color.OrangeRed;
partial void UpdateProjSpecific()
{
if (GameMain.GameSession?.GameMode is not TutorialMode) { return; }
foreach (var target in ParentEvent.GetTargets(TargetTag))
{
SetHighlight(target);
}
}
private void SetHighlight(Entity entity)
{
if (entity is Item i)
{
SetHighlight(i);
}
else if (entity is Structure s)
{
SetHighlight(s);
}
else if (entity is Character c)
{
SetHighlight(c);
}
}
private void SetHighlight(Item item)
{
if (item.ExternalHighlight == State) { return; }
item.SpriteColor = (State) ? highlightColor : Color.White;
item.ExternalHighlight = State;
}
private void SetHighlight(Structure structure)
{
structure.SpriteColor = (State) ? highlightColor : Color.White;
structure.ExternalHighlight = State;
}
private void SetHighlight(Character character)
{
character.ExternalHighlight = State;
}
}

View File

@@ -0,0 +1,50 @@
using Barotrauma.Tutorials;
namespace Barotrauma;
partial class TutorialSegmentAction : EventAction
{
private Tutorial.Segment segment;
partial void UpdateProjSpecific()
{
// Only need to create the segment when it's being triggered (otherwise the tutorial already has the segment instance)
if (Type == SegmentActionType.Trigger)
{
segment = Tutorial.Segment.CreateInfoBoxSegment(Id, ObjectiveTag, AutoPlayVideo ? Tutorials.AutoPlayVideo.Yes : Tutorials.AutoPlayVideo.No,
new Tutorial.Segment.Text(TextTag, Width, Height, Anchor.Center),
new Tutorial.Segment.Video(VideoFile, TextTag, Width, Height));
}
else if (Type == SegmentActionType.Add)
{
segment = Tutorial.Segment.CreateObjectiveSegment(Id, !ObjectiveTag.IsEmpty ? ObjectiveTag : Id);
}
if (GameMain.GameSession?.GameMode is TutorialMode tutorialMode)
{
if (tutorialMode.Tutorial is Tutorial tutorial)
{
switch (Type)
{
case SegmentActionType.Trigger:
case SegmentActionType.Add:
tutorial.TriggerTutorialSegment(segment);
break;
case SegmentActionType.Complete:
tutorial.CompleteTutorialSegment(Id);
break;
case SegmentActionType.Remove:
tutorial.RemoveTutorialSegment(Id);
break;
case SegmentActionType.CompleteAndRemove:
tutorial.CompleteTutorialSegment(Id);
tutorial.RemoveTutorialSegment(Id);
break;
}
}
}
else
{
DebugConsole.ShowError($"Error in event \"{ParentEvent.Prefab.Identifier}\": attempting to use TutorialSegmentAction during a non-Tutorial game mode!");
}
}
}

View File

@@ -6,23 +6,26 @@ using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using System.Threading;
using Barotrauma.Threading;
namespace Barotrauma
{
public class ScalableFont : IDisposable
{
private static List<ScalableFont> FontList = new List<ScalableFont>();
private static readonly List<ScalableFont> FontList = new List<ScalableFont>();
private static Library Lib = null;
private readonly object mutex = new object();
private static readonly object globalMutex = new object();
private readonly ReaderWriterLockSlim rwl = new ReaderWriterLockSlim();
private string filename;
private Face face;
private readonly string filename;
private readonly Face face;
private uint size;
private int baseHeight;
private Dictionary<uint, GlyphData> texCoords;
private List<Texture2D> textures;
private GraphicsDevice graphicsDevice;
private readonly Dictionary<uint, GlyphData> texCoords;
private readonly List<Texture2D> textures;
private readonly GraphicsDevice graphicsDevice;
private Vector2 currentDynamicAtlasCoords;
private int currentDynamicAtlasNextY;
@@ -49,7 +52,7 @@ namespace Barotrauma
set
{
size = value;
if (graphicsDevice != null) RenderAtlas(graphicsDevice, charRanges, texDims, baseChar);
if (graphicsDevice != null) { RenderAtlas(graphicsDevice, charRanges, texDims, baseChar); }
}
}
@@ -93,11 +96,15 @@ namespace Barotrauma
public ScalableFont(string filename, uint size, GraphicsDevice gd = null, bool dynamicLoading = false, bool isCJK = false)
{
lock (mutex)
lock (globalMutex)
{
Lib ??= new Library();
}
this.filename = filename;
this.face = null;
using (new ReadLock(rwl))
{
if (Lib == null) Lib = new Library();
this.filename = filename;
this.face = null;
foreach (ScalableFont font in FontList)
{
if (font.filename == filename)
@@ -106,19 +113,23 @@ namespace Barotrauma
break;
}
}
this.face ??= new Face(Lib, filename);
this.size = size;
this.textures = new List<Texture2D>();
this.texCoords = new Dictionary<uint, GlyphData>();
this.DynamicLoading = dynamicLoading;
this.IsCJK = isCJK;
this.graphicsDevice = gd;
}
if (gd != null && !dynamicLoading)
{
RenderAtlas(gd);
}
this.face ??= new Face(Lib, filename);
this.size = size;
this.textures = new List<Texture2D>();
this.texCoords = new Dictionary<uint, GlyphData>();
this.DynamicLoading = dynamicLoading;
this.IsCJK = isCJK;
this.graphicsDevice = gd;
if (gd != null && !dynamicLoading)
{
RenderAtlas(gd);
}
lock (globalMutex)
{
FontList.Add(this);
}
}
@@ -162,7 +173,7 @@ namespace Barotrauma
Vector2 currentCoords = Vector2.Zero;
int nextY = 0;
lock (mutex)
using (new WriteLock(rwl))
{
face.SetPixelSizes(0, size);
face.LoadGlyph(face.GetCharIndex(baseChar), LoadFlags.Default, LoadTarget.Normal);
@@ -175,19 +186,22 @@ namespace Barotrauma
for (uint j = start; j <= end; j++)
{
uint glyphIndex = face.GetCharIndex(j);
if (glyphIndex == 0) continue;
if (glyphIndex == 0)
{
texCoords.Add(j, new GlyphData(
advance: 0,
texIndex: -1));
continue;
}
face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal);
if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0)
{
if (face.Glyph.Metrics.HorizontalAdvance > 0)
{
//glyph is empty, but char still applies advance
GlyphData blankData = new GlyphData(
advance: (float)face.Glyph.Metrics.HorizontalAdvance,
texIndex: -1); //indicates no texture because the glyph is empty
//glyph is empty, but char might still apply advance
GlyphData blankData = new GlyphData(
advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f),
texIndex: -1); //indicates no texture because the glyph is empty
texCoords.Add(j, blankData);
}
texCoords.Add(j, blankData);
continue;
}
//stacktrace doesn't really work that well when RenderGlyph throws an exception
@@ -257,7 +271,7 @@ namespace Barotrauma
private void DynamicRenderAtlas(GraphicsDevice gd, uint character, int texDims = 1024, uint baseChar = 0x54)
{
bool missingCharacterFound = false;
lock (mutex)
using (new ReadLock(rwl))
{
missingCharacterFound = !texCoords.ContainsKey(character);
}
@@ -268,10 +282,9 @@ namespace Barotrauma
private void DynamicRenderAtlas(GraphicsDevice gd, string str, int texDims = 1024, uint baseChar = 0x54)
{
bool missingCharacterFound = false;
var distinctChrs = str.Distinct().Select(c => (uint)c).ToArray();
lock (mutex)
using (new ReadLock(rwl))
{
foreach (var character in distinctChrs)
foreach (var character in str)
{
if (texCoords.ContainsKey(character)) { continue; }
@@ -280,7 +293,7 @@ namespace Barotrauma
}
}
if (!missingCharacterFound) { return; }
DynamicRenderAtlas(gd, distinctChrs, texDims, baseChar);
DynamicRenderAtlas(gd, str.Select(c => (uint)c), texDims, baseChar);
}
private void DynamicRenderAtlas(GraphicsDevice gd, IEnumerable<uint> characters, int texDims = 1024, uint baseChar = 0x54)
@@ -299,7 +312,7 @@ namespace Barotrauma
Fixed26Dot6 horizontalAdvance;
Vector2 drawOffset;
lock (mutex)
using (new WriteLock(rwl))
{
if (textures.Count == 0)
{
@@ -318,20 +331,23 @@ namespace Barotrauma
if (texCoords.ContainsKey(character)) { continue; }
uint glyphIndex = face.GetCharIndex(character);
if (glyphIndex == 0) { continue; }
if (glyphIndex == 0)
{
texCoords.Add(character, new GlyphData(
advance: 0,
texIndex: -1));
continue;
}
face.SetPixelSizes(0, size);
face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal);
if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0)
{
if (face.Glyph.Metrics.HorizontalAdvance > 0)
{
//glyph is empty, but char still applies advance
GlyphData blankData = new GlyphData(
advance: (float)face.Glyph.Metrics.HorizontalAdvance,
texIndex: -1); //indicates no texture because the glyph is empty
texCoords.Add(character, blankData);
}
//glyph is empty, but char might still apply advance
GlyphData blankData = new GlyphData(
advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f),
texIndex: -1); //indicates no texture because the glyph is empty
texCoords.Add(character, blankData);
continue;
}

View File

@@ -730,7 +730,7 @@ namespace Barotrauma
public void DrawToolTip(SpriteBatch spriteBatch)
{
if (!Visible) { return; }
DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect);
DrawToolTip(spriteBatch, ToolTip, Rect);
}
public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos)
@@ -781,7 +781,7 @@ namespace Barotrauma
if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10)
{
toolTipBlock.RectTransform.AbsoluteOffset -= new Point(
(targetElement.Width / 2) * Math.Sign(targetElement.Center.X - toolTipBlock.Center.X),
0,
toolTipBlock.Rect.Bottom - (GameMain.GraphicsHeight - 10));
}
toolTipBlock.SetTextPos();

View File

@@ -204,15 +204,7 @@ namespace Barotrauma
currentHighestParent = FindHighestParent();
currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList;
rectT.ParentChanged += (RectTransform newParent) =>
{
currentHighestParent.GUIComponent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList;
if (newParent != null)
{
currentHighestParent = FindHighestParent();
currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList;
}
};
rectT.ParentChanged += _ => RefreshListBoxParent();
}
@@ -396,6 +388,15 @@ namespace Barotrauma
return true;
}
public void RefreshListBoxParent()
{
currentHighestParent.GUIComponent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList;
if (RectTransform.Parent == null) { return; }
currentHighestParent = FindHighestParent();
currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList;
}
private void AddListBoxToGUIUpdateList(GUIComponent parent)
{
//the parent is not our parent anymore :(
@@ -403,11 +404,13 @@ namespace Barotrauma
//and somewhere between this component and the higher parent a component was removed
for (int i = 1; i < parentHierarchy.Count; i++)
{
if (!parentHierarchy[i].IsParentOf(parentHierarchy[i - 1], recursive: false))
if (parentHierarchy[i].IsParentOf(parentHierarchy[i - 1], recursive: false))
{
parent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList;
return;
continue;
}
parent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList;
return;
}
if (Dropped)

View File

@@ -34,7 +34,7 @@ namespace Barotrauma
public GUIFrame InnerFrame { get; private set; }
public GUITextBlock Header { get; private set; }
public GUITextBlock Text { get; private set; }
public string Tag { get; private set; }
public Identifier Tag { get; private set; }
public bool Closed { get; private set; }
public bool DisplayInLoadingScreens;
@@ -78,6 +78,8 @@ namespace Barotrauma
/// </summary>
private readonly Func<bool> autoCloseCondition;
public bool FlashOnAutoCloseCondition { get; set; }
public Type MessageBoxType => type;
public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault();
@@ -142,7 +144,7 @@ namespace Barotrauma
}
GUIStyle.Apply(InnerFrame, "", this);
this.type = type;
Tag = tag;
Tag = tag.ToIdentifier();
#warning TODO: These should be broken into separate methods at least
if (type == Type.Default || type == Type.Vote)
@@ -199,6 +201,7 @@ namespace Barotrauma
var button = new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f / buttons.Length), buttonContainer.RectTransform), buttons[i]);
Buttons.Add(button);
}
GUITextBlock.AutoScaleAndNormalize(Buttons.Select(btn => btn.TextBlock));
}
else if (type == Type.InGame || type == Type.Tutorial)
{
@@ -540,9 +543,7 @@ namespace Barotrauma
if (type == Type.InGame || type == Type.Tutorial)
{
initialPos = new Vector2(0.0f, GameMain.GraphicsHeight);
defaultPos = type == Type.InGame ?
new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale) :
new Vector2(0.0f, GameMain.GraphicsHeight / 2);
defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale);
endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y);
}
else
@@ -574,10 +575,18 @@ namespace Barotrauma
inGameCloseTimer += deltaTime;
}
if (inGameCloseTimer >= inGameCloseTime || (autoCloseCondition != null && autoCloseCondition()))
if (inGameCloseTimer >= inGameCloseTime)
{
Close();
}
else if (autoCloseCondition != null && autoCloseCondition())
{
Close();
if (FlashOnAutoCloseCondition)
{
InnerFrame.Flash(GUIStyle.Green);
}
}
}
else
{
@@ -634,7 +643,6 @@ namespace Barotrauma
}
}
public void Close()
{
if (IsAnimated)
@@ -662,6 +670,19 @@ namespace Barotrauma
MessageBoxes.Clear();
}
public static void Close(Identifier tag)
{
foreach (var messageBox in MessageBoxes)
{
if (messageBox is GUIMessageBox mb && mb.Tag == tag)
{
mb.Close();
}
}
}
public static void Close(string tag) => Close(tag.ToIdentifier());
/// <summary>
/// Parent does not matter. It's overridden.
/// </summary>

View File

@@ -178,19 +178,19 @@ namespace Barotrauma
{
public GUIFont(string identifier) : base(identifier) { }
public bool HasValue => Prefabs.Any();
public bool HasValue => !Prefabs.IsEmpty;
public ScalableFont Value => Prefabs.ActivePrefab.Font;
public static implicit operator ScalableFont(GUIFont reference) => reference.Value;
public bool ForceUpperCase => HasValue && Value.ForceUpperCase;
public bool ForceUpperCase => Prefabs.ActivePrefab?.Font is { ForceUpperCase: true };
public uint Size => HasValue ? Value.Size : 0;
private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value);
private ScalableFont GetFontForStr(string str) =>
public ScalableFont GetFontForStr(string str) =>
TextManager.IsCJK(str) ? Prefabs.ActivePrefab.CjkFont : Prefabs.ActivePrefab.Font;
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth)

View File

@@ -161,7 +161,7 @@ namespace Barotrauma
public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale);
public static GUIComponentStyle GetComponentStyle(string name)
=> ComponentStyles.ContainsKey(name) ? ComponentStyles[name] : null;
=> ComponentStyles.TryGet(name, out var style) ? style : null;
public static void Apply(GUIComponent targetComponent, string styleName = "", GUIComponent parent = null)
{

View File

@@ -177,7 +177,7 @@ namespace Barotrauma
radioButtonGroup = rbg;
}
private void ResizeBox()
public void ResizeBox()
{
Vector2 textBlockScale = new Vector2(Math.Max(Rect.Width - box.Rect.Width, 0.0f) / Math.Max(Rect.Width, 1.0f), 1.0f);
text.RectTransform.RelativeSize = textBlockScale;

View File

@@ -233,7 +233,9 @@ namespace Barotrauma
get
{
Point absoluteOffset = ConvertOffsetRelativeToAnchor(AbsoluteOffset, Anchor);
Point relativeOffset = NonScaledParentRect.MultiplySize(RelativeOffset);
Point relativeOffset = new Point(
(int)(NonScaledParentSize.X * RelativeOffset.X),
(int)(NonScaledParentSize.Y * RelativeOffset.Y));
relativeOffset = ConvertOffsetRelativeToAnchor(relativeOffset, Anchor);
return AnchorPoint + PivotOffset + absoluteOffset + relativeOffset + ScreenSpaceOffset;
}
@@ -256,6 +258,7 @@ namespace Barotrauma
public Rectangle ParentRect => Parent != null ? Parent.Rect : UIRect;
protected Rectangle NonScaledRect => new Rectangle(NonScaledTopLeft, NonScaledSize);
protected virtual Rectangle NonScaledUIRect => NonScaledRect;
protected Point NonScaledParentSize => parent?.NonScaledSize ?? new Point(GUI.UIWidth, GameMain.GraphicsHeight);
protected Rectangle NonScaledParentRect => parent != null ? Parent.NonScaledRect : UIRect;
protected Rectangle NonScaledParentUIRect => parent != null ? Parent.NonScaledUIRect : UIRect;
protected Rectangle UIRect => new Rectangle(0, 0, GUI.UIWidth, GameMain.GraphicsHeight);
@@ -336,6 +339,11 @@ namespace Barotrauma
public event Action<RectTransform> ChildrenChanged;
public event Action ScaleChanged;
public event Action SizeChanged;
public void ResetSizeChanged()
{
SizeChanged = null;
}
#endregion
#region Initialization
@@ -730,17 +738,17 @@ namespace Barotrauma
get { return animTargetPos ?? AbsoluteOffset; }
}
public void MoveOverTime(Point targetPos, float duration)
public void MoveOverTime(Point targetPos, float duration, Action onDoneMoving = null)
{
animTargetPos = targetPos;
CoroutineManager.StartCoroutine(DoMoveAnimation(targetPos, duration));
CoroutineManager.StartCoroutine(DoMoveAnimation(targetPos, duration, onDoneMoving));
}
public void ScaleOverTime(Point targetSize, float duration)
{
CoroutineManager.StartCoroutine(DoScaleAnimation(targetSize, duration));
}
private IEnumerable<CoroutineStatus> DoMoveAnimation(Point targetPos, float duration)
private IEnumerable<CoroutineStatus> DoMoveAnimation(Point targetPos, float duration, Action onDoneMoving = null)
{
Vector2 startPos = AbsoluteOffset.ToVector2();
float t = 0.0f;
@@ -752,6 +760,7 @@ namespace Barotrauma
}
AbsoluteOffset = targetPos;
animTargetPos = null;
onDoneMoving?.Invoke();
yield return CoroutineStatus.Success;
}
private IEnumerable<CoroutineStatus> DoScaleAnimation(Point targetSize, float duration)

View File

@@ -226,17 +226,18 @@ namespace Barotrauma
};
submarineDisplayElement.submarineImage = new GUIImage(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), null, true);
submarineDisplayElement.middleTextBlock = new GUITextBlock(new RectTransform(new Vector2(0.8f, 1f), submarineDisplayElement.background.RectTransform, Anchor.Center), string.Empty, textAlignment: Alignment.Center);
submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont);
submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Left);
submarineDisplayElement.submarineTier = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter, Pivot.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Right);
submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter, Pivot.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont);
submarineDisplayElement.submarineName = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont);
submarineDisplayElement.submarineFee = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.BottomCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding) }, string.Empty, textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont);
submarineDisplayElement.selectSubmarineButton = new GUIButton(new RectTransform(Vector2.One, submarineDisplayElement.background.RectTransform), style: null);
submarineDisplayElement.previewButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, submarineDisplayElement.background.RectTransform, anchor: Anchor.BottomRight, pivot: Pivot.BottomRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point((int)(0.03f * background.Rect.Height)) }, style: "ExpandButton")
submarineDisplayElement.previewButton = new GUIButton(new RectTransform(Vector2.One * 0.12f, submarineDisplayElement.background.RectTransform, anchor: Anchor.BottomRight, scaleBasis: ScaleBasis.BothHeight) { AbsoluteOffset = new Point((int)(0.03f * background.Rect.Height)) }, style: "ExpandButton")
{
Color = Color.White,
HoverColor = Color.White,
PressedColor = Color.White
};
submarineDisplayElement.submarineClass = new GUITextBlock(new RectTransform(new Vector2(1f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopCenter) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Left);
submarineDisplayElement.submarineTier = new GUITextBlock(new RectTransform(new Vector2(0.5f, 0.1f), submarineDisplayElement.background.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(0, HUDLayoutSettings.Padding + (int)GUIStyle.Font.MeasureString(submarineDisplayElement.submarineName.Text).Y) }, string.Empty, textAlignment: Alignment.Right);
submarineDisplays[i] = submarineDisplayElement;
}
@@ -395,9 +396,13 @@ namespace Barotrauma
return true;
};
submarineDisplays[i].submarineName.Text = subToDisplay.DisplayName;
submarineDisplays[i].submarineName.Text = subToDisplay.DisplayName;
submarineDisplays[i].submarineClass.Text = TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{subToDisplay.SubmarineClass}"));
submarineDisplays[i].submarineClass.ToolTip = TextManager.Get("submarineclass.description") + "\n\n" + TextManager.Get($"submarineclass.{subToDisplay.SubmarineClass}.description");
submarineDisplays[i].submarineTier.Text = TextManager.Get($"submarinetier.{subToDisplay.Tier}");
submarineDisplays[i].submarineTier.ToolTip = TextManager.Get("submarinetier.description");
if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay))
{

View File

@@ -1722,7 +1722,11 @@ namespace Barotrauma
var subInfoTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedFrame.RectTransform));
LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ? $"{TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}")} ({TextManager.Get($"submarinetier.{sub.Info.Tier}")})" : TextManager.Get("shuttle");
LocalizedString className = !sub.Info.HasTag(SubmarineTag.Shuttle) ?
TextManager.GetWithVariables("submarine.classandtier",
("[class]", TextManager.Get($"submarineclass.{sub.Info.SubmarineClass}")),
("[tier]", TextManager.Get($"submarinetier.{sub.Info.Tier}"))) :
TextManager.Get("shuttle");
int nameHeight = (int)GUIStyle.LargeFont.MeasureString(sub.Info.DisplayName, true).Y;
int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y;
@@ -2140,18 +2144,17 @@ namespace Barotrauma
skillNames.Add(skillName);
skillName.RectTransform.MinSize = new Point(0, skillName.Rect.Height);
skillContainer.RectTransform.MinSize = new Point(0, skillName.Rect.Height);
new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), skillContainer.RectTransform), Math.Floor(skill.Level).ToString("F0"), textAlignment: Alignment.TopRight);
float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level;
if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level)))
{
int skillChange = (int)MathF.Floor(modifiedSkillLevel - skill.Level);
//TODO: if/when we upgrade to C# 9, do neater pattern matching here
string stringColor = true switch
string stringColor = skillChange switch
{
true when skillChange > 0 => XMLExtensions.ToStringHex(GUIStyle.Green),
true when skillChange < 0 => XMLExtensions.ToStringHex(GUIStyle.Red),
> 0 => XMLExtensions.ToStringHex(GUIStyle.Green),
< 0 => XMLExtensions.ToStringHex(GUIStyle.Red),
_ => XMLExtensions.ToStringHex(GUIStyle.TextColorNormal)
};

View File

@@ -1257,12 +1257,17 @@ namespace Barotrauma
List<Upgrade> upgrades = entity.GetUpgrades();
int upgradesCount = upgrades.Count;
const int maxUpgrades = 4;
itemName.Text = entity is Item ? entity.Name : TextManager.Get("upgradecategory.walls");
Item? item = entity as Item;
itemName.Text = item?.Name ?? TextManager.Get("upgradecategory.walls");
if (slotIndex > -1)
{
itemName.Text = TextManager.GetWithVariables("weaponslotwithname", ("[number]", slotIndex.ToString()), ("[weaponname]", itemName.Text));
}
if (item?.PendingItemSwap != null)
{
itemName.Text = RichString.Rich(itemName.Text + "\n" + TextManager.GetWithVariable("upgrades.pendingitem", "[itemname]", item.PendingItemSwap.Name));
}
upgradeList.Content.ClearChildren();
for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++)
{
@@ -1275,7 +1280,7 @@ namespace Barotrauma
// include pending upgrades into the tooltip
foreach (var (prefab, category, level) in upgradeManager.PendingUpgrades)
{
if (entity is Item item && category.CanBeApplied(item, prefab) || entity is Structure && category.IsWallUpgrade)
if (item != null && category.CanBeApplied(item, prefab) || entity is Structure && category.IsWallUpgrade)
{
bool found = false;
foreach (GUITextBlock textBlock in upgradeList.Content.Children.Where(c => c is GUITextBlock).Cast<GUITextBlock>())
@@ -1392,6 +1397,10 @@ namespace Barotrauma
if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); }
(itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement);
}
else
{
ScrollToCategory(data => data.Category.CanBeApplied(item, null));
}
}
}
else
@@ -1463,23 +1472,16 @@ namespace Barotrauma
// submarine name
new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.DisplayName, textAlignment: Alignment.Right, font: GUIStyle.LargeFont);
GUILayoutGroup classLayout = new GUILayoutGroup(rectT(1, 0.15f, submarineInfoFrame), isHorizontal: true) { Stretch = true };
LocalizedString classText = $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}";
// submarine class + tier
new GUITextBlock(rectT(0.8f, 1, classLayout), classText, textAlignment: Alignment.Right, font: GUIStyle.Font)
new GUITextBlock(rectT(1.0f, 0.15f, submarineInfoFrame), classText, textAlignment: Alignment.Right, font: GUIStyle.Font)
{
ToolTip = TextManager.Get("submarineclass.description") + '\n' + TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}.description")
ToolTip = TextManager.Get("submarineclass.description") + "\n\n" + TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}.description")
};
int tier = submarine.Info.Tier;
string tierStyle = $"SubmarineTier.{tier}";
if (GUIStyle.GetComponentStyle(tierStyle) != null)
new GUITextBlock(rectT(1.0f, 0.15f, submarineInfoFrame), TextManager.Get($"submarinetier.{submarine.Info.Tier}"), textAlignment: Alignment.Right, font: GUIStyle.Font)
{
LocalizedString tooltip = TextManager.Get("submarinetier.description").Fallback(string.Empty);
new GUIImage(rectT(0.15f, 1, classLayout), style: tierStyle, scaleToFit: false)
{
ToolTip = tooltip
};
}
ToolTip = TextManager.Get("submarinetier.description")
};
var description = new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.Description, textAlignment: Alignment.Right, wrap: true);
submarineInfoFrame.RectTransform.ScreenSpaceOffset = new Point(0, (int)(16 * GUI.Scale));
@@ -1750,14 +1752,26 @@ namespace Barotrauma
{
if (currentStoreLayout == null) { return; }
CategoryData? mostAppropriateCategory = null;
GUIComponent? mostAppropriateChild = null;
foreach (GUIComponent child in currentStoreLayout.Content.Children)
{
if (child.UserData is CategoryData data && predicate(data))
{
currentStoreLayout.ScrollToElement(child, playSelectSound);
break;
//choose the category with least items in it as the "most appropriate"
//e.g. when selecting junction boxes, we want to select the "junction boxes" category instead of "electrical repairs" which contains many electrical devices
if (mostAppropriateCategory == null ||
data.Category.ItemTags.Count() < mostAppropriateCategory.Value.Category.ItemTags.Count())
{
mostAppropriateCategory = data;
mostAppropriateChild = child;
}
}
}
if (mostAppropriateChild != null)
{
currentStoreLayout.ScrollToElement(mostAppropriateChild, playSelectSound);
}
}
/// <summary>

View File

@@ -461,7 +461,7 @@ namespace Barotrauma
yield return CoroutineStatus.Running;
UgcTransition.Prepare();
LegacySteamUgcTransition.Prepare();
var contentPackageLoadRoutine = ContentPackageManager.Init();
foreach (var progress in contentPackageLoadRoutine)
{
@@ -715,7 +715,7 @@ namespace Barotrauma
}
#endif
NetworkMember?.Update((float)Timing.Step);
Client?.Update((float)Timing.Step);
if (!HasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine))
{
@@ -874,7 +874,7 @@ namespace Barotrauma
}
}
NetworkMember?.Update((float)Timing.Step);
Client?.Update((float)Timing.Step);
GUI.Update((float)Timing.Step);
@@ -1181,7 +1181,7 @@ namespace Barotrauma
{
exiting = true;
DebugConsole.NewMessage("Exiting...");
NetworkMember?.Quit();
Client?.Quit();
SteamManager.ShutDown();
try

View File

@@ -3516,9 +3516,9 @@ namespace Barotrauma
if (node == null || characterContext != null) { return false; }
if (node.UserData is Order nodeOrder)
{
return !nodeOrder.TargetAllCharacters && !nodeOrder.Prefab.HasOptions &&
(!nodeOrder.MustSetTarget || itemContext != null ||
nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2);
return !nodeOrder.TargetAllCharacters &&
(!nodeOrder.Prefab.HasOptions || !nodeOrder.Option.IsEmpty) &&
(!nodeOrder.MustSetTarget || itemContext != null || nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2);
}
return false;
}

View File

@@ -1,22 +1,21 @@
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace Barotrauma.Tutorials
{
enum AutoPlayVideo { Yes, No };
enum TutorialSegmentType { MessageBox, InfoBox };
enum TutorialSegmentType { MessageBox, InfoBox, Objective };
class Tutorial
sealed class Tutorial
{
#region Constants
private const string PlayableContentPath = "Content/Tutorials/TutorialVideos/";
private const SpawnType SpawnPointType = SpawnType.Human;
private const float FadeOutTime = 3f;
private const float WaitBeforeFade = 4f;
@@ -25,19 +24,11 @@ namespace Barotrauma.Tutorials
#region Tutorial variables
public static ImmutableHashSet<Type> Types;
static Tutorial()
{
Types = ReflectionUtils.GetDerivedNonAbstract<Tutorial>()
.ToImmutableHashSet();
}
public readonly Identifier Identifier;
public LocalizedString DisplayName { get; }
public bool ContentRunning { get; protected set; }
public bool ContentRunning { get; private set; }
private GUIComponent infoBox;
private Action infoBoxClosedCallback;
@@ -51,47 +42,25 @@ namespace Barotrauma.Tutorials
private readonly LocalizedString objectiveTextTranslated;
private readonly List<Segment> ActiveObjectives = new List<Segment>();
private const float ObjectiveComponentRemovalTime = 1.5f;
private const float ObjectiveComponentAnimationTime = 1.5f;
private Segment ActiveContentSegment { get; set; }
public class Segment
{
public struct Text
public readonly record struct Text(
Identifier Tag,
int Width = DefaultWidth,
int Height = DefaultHeight,
Anchor Anchor = Anchor.Center);
public readonly record struct Video(
string FullPath,
Identifier TextTag,
int Width = DefaultWidth,
int Height = DefaultHeight)
{
private const Anchor DefaultAnchor = Anchor.Center;
public Identifier Tag;
public int Width;
public int Height;
public Anchor Anchor;
public Text(Identifier tag, int? width = null, int? height = null, Anchor? anchor = null)
{
Tag = tag;
Width = width ?? DefaultWidth;
Height = height ?? DefaultHeight;
Anchor = anchor ?? DefaultAnchor;
}
public Text(string tag, int? width = null, int? height = null, Anchor? anchor = null) : this(tag.ToIdentifier(), width, height, anchor) { }
}
public struct Video
{
public string File;
public Identifier TextTag;
public int Width;
public int Height;
public Video(string file, Identifier textTag, int? width = null, int? height = null)
{
File = file;
TextTag = textTag;
Width = width ?? DefaultWidth;
Height = height ?? DefaultHeight;
}
public Video(string file, string textTag, int? width = null, int? height = null) : this(file, textTag.ToIdentifier(), width, height) { }
public string FileName => Path.GetFileName(FullPath.CleanUpPath());
public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath());
}
private const int DefaultWidth = 450;
@@ -103,15 +72,30 @@ namespace Barotrauma.Tutorials
public LocalizedString ObjectiveText;
public readonly Identifier Id;
public readonly Text? TextContent;
public readonly Video? VideoContent;
public readonly Text TextContent;
public readonly Video VideoContent;
public readonly AutoPlayVideo AutoPlayVideo;
public Action OnClickToDisplayMessage;
public Action OnClickObjective;
public readonly TutorialSegmentType SegmentType;
public Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text? textContent = null, Video? videoContent = null)
public static Segment CreateInfoBoxSegment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default)
{
return new Segment(id, objectiveTextTag, autoPlayVideo, textContent, videoContent);
}
public static Segment CreateMessageBoxSegment(Identifier id, Identifier objectiveTextTag, Action onClickObjective)
{
return new Segment(id, objectiveTextTag, onClickObjective);
}
public static Segment CreateObjectiveSegment(Identifier id, Identifier objectiveTextTag)
{
return new Segment(id, objectiveTextTag);
}
private Segment(Identifier id, Identifier objectiveTextTag, AutoPlayVideo autoPlayVideo, Text textContent = default, Video videoContent = default)
{
Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
@@ -121,15 +105,21 @@ namespace Barotrauma.Tutorials
SegmentType = TutorialSegmentType.InfoBox;
}
public Segment(Identifier id, Action onClickToDisplayMessage)
private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective)
{
Id = id;
var objetiveTextTag = $"{id}.objective".ToIdentifier();
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objetiveTextTag));
OnClickToDisplayMessage = onClickToDisplayMessage;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
OnClickObjective = onClickObjective;
SegmentType = TutorialSegmentType.MessageBox;
}
}
private Segment(Identifier id, Identifier objectiveTextTag)
{
Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
SegmentType = TutorialSegmentType.Objective;
}
}
private bool completed;
public bool Completed
@@ -157,60 +147,57 @@ namespace Barotrauma.Tutorials
private Character character;
private readonly string submarinePath = "Content/Tutorials/Dugong_Tutorial.sub";
private readonly string startOutpostPath = "Content/Tutorials/TutorialOutpost.sub";
private readonly string levelSeed = "nLoZLLtza";
private readonly string levelParams = "ColdCavernsTutorial";
private string SubmarinePath => TutorialPrefab.SubmarinePath.Value;
private string StartOutpostPath => TutorialPrefab.OutpostPath.Value;
private string LevelSeed => TutorialPrefab.LevelSeed;
private string LevelParams => TutorialPrefab.LevelParams;
private SubmarineInfo startOutpost = null;
public readonly List<(Entity entity, string iconStyle)> Icons = new List<(Entity entity, string iconStyle)>();
#endregion
#region Tutorial Controls
public Tutorial(TutorialPrefab prefab)
{
Identifier = $"tutorial.{prefab?.Identifier ?? Identifier.Empty}".ToIdentifier();
Identifier = $"tutorial.{prefab.Identifier}".ToIdentifier();
DisplayName = TextManager.Get(Identifier);
objectiveTextTranslated = TextManager.Get("Tutorial.Objective");
TutorialPrefab = prefab;
submarinePath = prefab.SubmarinePath.Value;
startOutpostPath = prefab.OutpostPath.Value;
levelSeed = prefab.LevelSeed;
levelParams = prefab.LevelParams;
eventPrefab = EventSet.GetEventPrefab(prefab.EventIdentifier);
}
private IEnumerable<CoroutineStatus> Loading()
{
SubmarineInfo subInfo = new SubmarineInfo(submarinePath);
SubmarineInfo subInfo = new SubmarineInfo(SubmarinePath);
LevelGenerationParams generationParams = LevelGenerationParams.LevelParams.Find(p => p.Identifier == levelParams);
LevelGenerationParams.LevelParams.TryGet(LevelParams, out LevelGenerationParams generationParams);
yield return CoroutineStatus.Running;
GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null);
(GameMain.GameSession.GameMode as TutorialMode).Tutorial = this;
if (generationParams != null)
if (generationParams is not null)
{
Biome biome =
Biome.Prefabs.FirstOrDefault(b => generationParams.AllowedBiomeIdentifiers.Contains(b.Identifier)) ??
Biome.Prefabs.First();
if (!string.IsNullOrEmpty(startOutpostPath))
if (!string.IsNullOrEmpty(StartOutpostPath))
{
startOutpost = new SubmarineInfo(startOutpostPath);
startOutpost = new SubmarineInfo(StartOutpostPath);
}
LevelData tutorialLevel = new LevelData(levelSeed, 0, 0, generationParams, biome);
LevelData tutorialLevel = new LevelData(LevelSeed, 0, 0, generationParams, biome);
GameMain.GameSession.StartRound(tutorialLevel, startOutpost: startOutpost);
}
else
{
GameMain.GameSession.StartRound(levelSeed);
GameMain.GameSession.StartRound(LevelSeed);
}
GameMain.GameSession.EventManager.ActiveEvents.Clear();
@@ -274,6 +261,7 @@ namespace Barotrauma.Tutorials
private void Initialize()
{
GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch;
GameMain.GameSession.CrewManager.AutoHideCrewList();
if (Character.Controlled is Character character)
{
@@ -450,7 +438,7 @@ namespace Barotrauma.Tutorials
public void TriggerTutorialSegment(Segment segment)
{
if (segment.SegmentType == TutorialSegmentType.MessageBox)
if (segment.SegmentType != TutorialSegmentType.InfoBox)
{
ActiveObjectives.Add(segment);
AddToObjectiveList(segment);
@@ -462,7 +450,7 @@ namespace Barotrauma.Tutorials
ActiveContentSegment = segment;
var title = TextManager.Get(segment.Id);
LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Value.Tag);
LocalizedString tutorialText = TextManager.GetFormatted(segment.TextContent.Tag);
tutorialText = TextManager.ParseInputTypes(tutorialText);
switch (segment.AutoPlayVideo)
@@ -471,9 +459,9 @@ namespace Barotrauma.Tutorials
infoBox = CreateInfoFrame(
title,
tutorialText,
segment.TextContent.Value.Width,
segment.TextContent.Value.Height,
segment.TextContent.Value.Anchor,
segment.TextContent.Width,
segment.TextContent.Height,
segment.TextContent.Anchor,
hasButton: true,
onInfoBoxClosed: LoadActiveContentVideo);
break;
@@ -481,9 +469,9 @@ namespace Barotrauma.Tutorials
infoBox = CreateInfoFrame(
title,
tutorialText,
segment.TextContent.Value.Width,
segment.TextContent.Value.Height,
segment.TextContent.Value.Anchor,
segment.TextContent.Width,
segment.TextContent.Height,
segment.TextContent.Anchor,
hasButton: true,
onInfoBoxClosed: StopCurrentContentSegment,
onVideoButtonClicked: LoadActiveContentVideo);
@@ -493,7 +481,7 @@ namespace Barotrauma.Tutorials
public void CompleteTutorialSegment(Identifier segmentId)
{
if (!(GetActiveObjective(segmentId) is Segment segment))
if (GetActiveObjective(segmentId) is not Segment segment)
{
DebugConsole.AddWarning($"Warning: tried to complete the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!");
return;
@@ -501,29 +489,28 @@ namespace Barotrauma.Tutorials
if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style)
{
segment.ObjectiveStateIndicator.ApplyStyle(style);
segment.ObjectiveStateIndicator.Flash(color: GUIStyle.Green);
}
segment.ObjectiveStateIndicator.Parent.Flash(color: GUIStyle.Green, flashDuration: 0.35f, useRectangleFlash: true);
segment.ObjectiveButton.OnClicked = null;
segment.ObjectiveButton.CanBeFocused = false;
}
public void RemoveTutorialSegment(Identifier segmentId)
{
if (!(GetActiveObjective(segmentId) is Segment segment))
if (GetActiveObjective(segmentId) is not Segment segment)
{
DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!");
return;
}
segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentRemovalTime, false);
segment.LinkedTextBlock.FadeOut(ObjectiveComponentRemovalTime, false);
segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false);
segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false);
var parent = segment.LinkedTextBlock.Parent;
parent.FadeOut(ObjectiveComponentRemovalTime, true, onRemove: () =>
parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () =>
{
ActiveObjectives.Remove(segment);
objectiveGroup?.Recalculate();
});
var targetPos = new Point(GameMain.GraphicsWidth - parent.Rect.X, 0);
parent.RectTransform.MoveOverTime(targetPos, ObjectiveComponentRemovalTime);
parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime);
segment.ObjectiveButton.OnClicked = null;
segment.ObjectiveButton.CanBeFocused = false;
}
@@ -585,12 +572,14 @@ namespace Barotrauma.Tutorials
{
var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform)
{
AbsoluteOffset = GetObjectiveHiddenPosition(),
MinSize = new Point(0, objectiveGroup.AbsoluteSpacing)
};
var frame = new GUIFrame(frameRt, style: null)
{
CanBeFocused = true
};
objectiveGroup.Recalculate();
segment.LinkedTextBlock = new GUITextBlock(
new RectTransform(new Point(frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing, 0), frame.RectTransform, anchor: Anchor.TopRight),
@@ -612,6 +601,7 @@ namespace Barotrauma.Tutorials
segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null)
{
CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective,
ToolTip = objectiveTextTranslated,
OnClicked = (GUIButton btn, object userdata) =>
{
@@ -628,13 +618,15 @@ namespace Barotrauma.Tutorials
}
else if (segment.SegmentType == TutorialSegmentType.MessageBox)
{
segment.OnClickToDisplayMessage?.Invoke();
segment.OnClickObjective?.Invoke();
}
return true;
}
};
SetTransparent(segment.ObjectiveButton);
frameRt.MoveOverTime(new Point(0, frameRt.AbsoluteOffset.Y), ObjectiveComponentAnimationTime, onDoneMoving: () => objectiveGroup?.Recalculate());
static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent;
}
@@ -654,15 +646,20 @@ namespace Barotrauma.Tutorials
ActiveContentSegment = segment;
infoBox = CreateInfoFrame(
TextManager.Get(segment.Id),
TextManager.Get(segment.TextContent.Value.Tag),
segment.TextContent.Value.Width,
segment.TextContent.Value.Height,
segment.TextContent.Value.Anchor,
TextManager.Get(segment.TextContent.Tag),
segment.TextContent.Width,
segment.TextContent.Height,
segment.TextContent.Anchor,
hasButton: true,
onInfoBoxClosed: () => ContentRunning = false,
onVideoButtonClicked: () => LoadVideo(segment));
}
private Point GetObjectiveHiddenPosition(RectTransform rt = null)
{
return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0);
}
#endregion
#region InfoFrame
@@ -772,9 +769,9 @@ namespace Barotrauma.Tutorials
if (segment.AutoPlayVideo == AutoPlayVideo.Yes)
{
videoPlayer.LoadContent(
contentPath: PlayableContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.Value.File),
textSettings: new VideoPlayer.TextSettings(segment.VideoContent.Value.TextTag, segment.VideoContent.Value.Width),
contentPath: segment.VideoContent.ContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width),
contentId: segment.Id,
startPlayback: true,
objective: segment.ObjectiveText,
@@ -783,8 +780,8 @@ namespace Barotrauma.Tutorials
else
{
videoPlayer.LoadContent(
contentPath: PlayableContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.Value.File),
contentPath: segment.VideoContent.ContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
textSettings: null,
contentId: segment.Id,
startPlayback: true,

View File

@@ -153,7 +153,7 @@ namespace Barotrauma.Items.Components
GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused;
if (GUI.HideCursor)
{
crosshairSprite?.Draw(spriteBatch, crosshairPos, Color.White, 0, currentCrossHairScale);
crosshairSprite?.Draw(spriteBatch, crosshairPos, ReloadTimer <= 0.0f ? Color.White : Color.White * 0.2f, 0, currentCrossHairScale);
crosshairPointerSprite?.Draw(spriteBatch, crosshairPointerPos, 0, currentCrossHairPointerScale);
}

View File

@@ -369,6 +369,7 @@ namespace Barotrauma.Items.Components
loopingSoundChannel = loopingSound.RoundSound.Sound.Play(
new Vector3(position.X, position.Y, 0.0f),
0.01f,
freqMult: itemSound.RoundSound.GetRandomFrequencyMultiplier(),
muffle: SoundPlayer.ShouldMuffleSound(Character.Controlled, position, loopingSound.Range, Character.Controlled?.CurrentHull));
loopingSoundChannel.Looping = true;
//TODO: tweak

View File

@@ -1013,6 +1013,19 @@ namespace Barotrauma.Items.Components
if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; }
var i = unobtainedMinerals.FirstOrDefault();
if (i == null) { continue; }
bool disrupted = false;
foreach ((Vector2 disruptPos, float disruptStrength) in disruptedDirections)
{
float dot = Vector2.Dot(Vector2.Normalize(c.center - transducerCenter), disruptPos);
if (dot > 1.0f - disruptStrength)
{
disrupted = true;
break;
}
}
if (disrupted) { continue; }
DrawMarker(spriteBatch,
i.Name, "mineral".ToIdentifier(), "mineralcluster" + i,
c.center, transducerCenter,

View File

@@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components
private GUILayoutGroup extraButtonContainer;
private GUIComponent skillTextContainer;
private readonly List<ParticleEmitter> particleEmitters = new List<ParticleEmitter>();
//the corresponding particle emitter is active when the condition is within this range
private readonly List<Vector2> particleEmitterConditionRanges = new List<Vector2>();
@@ -132,9 +134,10 @@ namespace Barotrauma.Items.Components
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform),
TextManager.Get("RequiredRepairSkills"), font: GUIStyle.SubHeadingFont);
skillTextContainer = paddedFrame;
for (int i = 0; i < requiredSkills.Count; i++)
{
var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform),
var skillText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), skillTextContainer.RectTransform),
" - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) Math.Round(requiredSkills[i].Level * SkillRequirementMultiplier)).ToString()),
font: GUIStyle.SmallFont)
{
@@ -359,19 +362,11 @@ namespace Barotrauma.Items.Components
extraButtonContainer.Visible = SabotageButton.Visible || TinkerButton.Visible;
extraButtonContainer.IgnoreLayoutGroups = !extraButtonContainer.Visible;
foreach (GUIComponent c in GuiFrame.GetChild(0).Children)
foreach (GUIComponent c in skillTextContainer.Children)
{
if (!(c.UserData is Skill skill)) continue;
if (c.UserData is not Skill skill) { continue; }
GUITextBlock textBlock = (GUITextBlock)c;
if (character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier))
{
textBlock.TextColor = GUIStyle.Red;
}
else
{
textBlock.TextColor = Color.White;
}
textBlock.TextColor = character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier) ? GUIStyle.Red : GUIStyle.TextColorNormal;
}
}

View File

@@ -823,7 +823,7 @@ namespace Barotrauma
reloadTextureButton.OnClicked += (button, data) =>
{
Sprite.ReloadXML();
Sprite.ReloadTexture(updateAllSprites: true);
Sprite.ReloadTexture();
return true;
};
}

View File

@@ -163,7 +163,7 @@ namespace Barotrauma
OnClicked = (button, data) =>
{
Sprite.ReloadXML();
Sprite.ReloadTexture(updateAllSprites: true);
Sprite.ReloadTexture();
return true;
}
};

View File

@@ -93,10 +93,11 @@ namespace Barotrauma
{
float leftPanelWidth = 0.6f;
float rightPanelWidth = 0.4f / leftPanelWidth;
LocalizedString className = !HasTag(SubmarineTag.Shuttle)
? $"{TextManager.Get($"submarineclass.{SubmarineClass}")} ({TextManager.Get($"submarinetier.{Tier}")})"
: TextManager.Get("shuttle");
LocalizedString className = !HasTag(SubmarineTag.Shuttle) ?
TextManager.GetWithVariables("submarine.classandtier",
("[class]", TextManager.Get($"submarineclass.{SubmarineClass}")),
("[tier]", TextManager.Get($"submarinetier.{Tier}"))) :
TextManager.Get("shuttle");
int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y;
int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth);

View File

@@ -134,6 +134,14 @@ namespace Barotrauma.Networking
return Permissions.HasFlag(permission);
}
public void ResetVotes()
{
for (int i = 0; i < votes.Length; i++)
{
votes[i] = null;
}
}
partial void DisposeProjSpecific()
{
if (VoipQueue != null)

View File

@@ -2,6 +2,8 @@
using Barotrauma.Steam;
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Networking
{
@@ -10,37 +12,14 @@ namespace Barotrauma.Networking
public ImmutableArray<ServerContentPackage> ServerContentPackages { get; set; } =
ImmutableArray<ServerContentPackage>.Empty;
public delegate void MessageCallback(IReadMessage message);
public delegate void DisconnectCallback(bool disableReconnect);
public delegate void DisconnectMessageCallback(string message);
public delegate void PasswordCallback(int salt, int retries);
public delegate void InitializationCompleteCallback();
[Obsolete("TODO: delete in nr3-layer-1-2-cleanup")]
public readonly struct Callbacks
public readonly record struct Callbacks(
Callbacks.MessageCallback OnMessageReceived,
Callbacks.DisconnectCallback OnDisconnect,
Callbacks.InitializationCompleteCallback OnInitializationComplete)
{
public readonly MessageCallback OnMessageReceived;
public readonly DisconnectCallback OnDisconnect;
public readonly DisconnectMessageCallback OnDisconnectMessageReceived;
public readonly PasswordCallback OnRequestPassword;
public readonly InitializationCompleteCallback OnInitializationComplete;
public Callbacks(MessageCallback onMessageReceived,
DisconnectCallback onDisconnect,
DisconnectMessageCallback onDisconnectMessageReceived,
PasswordCallback onRequestPassword,
InitializationCompleteCallback onInitializationComplete)
{
OnMessageReceived = onMessageReceived;
OnDisconnect = onDisconnect;
OnDisconnectMessageReceived = onDisconnectMessageReceived;
OnRequestPassword = onRequestPassword;
OnInitializationComplete = onInitializationComplete;
}
public delegate void MessageCallback(IReadMessage message);
public delegate void DisconnectCallback(PeerDisconnectPacket disconnectPacket);
public delegate void InitializationCompleteCallback();
}
protected readonly Callbacks callbacks;
@@ -51,6 +30,8 @@ namespace Barotrauma.Networking
protected readonly bool isOwner;
protected readonly Option<int> ownerKey;
protected bool isActive;
public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option<int> ownerKey)
{
ServerEndpoint = serverEndpoint;
@@ -60,7 +41,7 @@ namespace Barotrauma.Networking
}
public abstract void Start();
public abstract void Close(string? msg = null, bool disableReconnect = false);
public abstract void Close(PeerDisconnectPacket peerDisconnectPacket);
public abstract void Update(float deltaTime);
public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true);
public abstract void SendPassword(string password);
@@ -71,6 +52,12 @@ namespace Barotrauma.Networking
public bool ContentPackageOrderReceived { get; protected set; }
protected int passwordSalt;
protected Steamworks.AuthTicket? steamAuthTicket;
private GUIMessageBox? passwordMsgBox;
public bool WaitingForPassword
=> isActive && initializationStep == ConnectionInitialization.Password
&& passwordMsgBox != null
&& GUIMessageBox.MessageBoxes.Contains(passwordMsgBox);
public struct IncomingInitializationMessage
{
@@ -112,8 +99,9 @@ namespace Barotrauma.Networking
}
case ConnectionInitialization.ContentPackageOrder:
{
if (initializationStep == ConnectionInitialization.SteamTicketAndVersion ||
initializationStep == ConnectionInitialization.Password)
if (initializationStep
is ConnectionInitialization.SteamTicketAndVersion
or ConnectionInitialization.Password)
{
initializationStep = ConnectionInitialization.ContentPackageOrder;
}
@@ -155,10 +143,57 @@ namespace Barotrauma.Networking
var passwordPacket = INetSerializableStruct.Read<ServerPeerPasswordPacket>(inc.Message);
if (WaitingForPassword) { return; }
passwordPacket.Salt.TryUnwrap(out passwordSalt);
passwordPacket.RetriesLeft.TryUnwrap(out var retries);
callbacks.OnRequestPassword.Invoke(passwordSalt, retries);
LocalizedString pwMsg = TextManager.Get("PasswordRequired");
passwordMsgBox = new GUIMessageBox(pwMsg, "", new LocalizedString[] { TextManager.Get("OK"), TextManager.Get("Cancel") },
relativeSize: new Vector2(0.25f, 0.1f), minSize: new Point(400, GUI.IntScale(170)));
var passwordHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.5f), passwordMsgBox.Content.RectTransform), childAnchor: Anchor.TopCenter);
var passwordBox = new GUITextBox(new RectTransform(new Vector2(0.8f, 1f), passwordHolder.RectTransform) { MinSize = new Point(0, 20) })
{
Censor = true
};
if (retries > 0)
{
var incorrectPasswordText = new GUITextBlock(new RectTransform(new Vector2(1f, 0.0f), passwordHolder.RectTransform), TextManager.Get("incorrectpassword"), GUIStyle.Red, GUIStyle.Font, textAlignment: Alignment.Center);
incorrectPasswordText.RectTransform.MinSize = new Point(0, (int)incorrectPasswordText.TextSize.Y);
passwordHolder.Recalculate();
}
passwordMsgBox.Content.Recalculate();
passwordMsgBox.Content.RectTransform.MinSize = new Point(0, passwordMsgBox.Content.RectTransform.Children.Sum(c => c.Rect.Height));
passwordMsgBox.Content.Parent.RectTransform.MinSize = new Point(0, (int)(passwordMsgBox.Content.RectTransform.MinSize.Y / passwordMsgBox.Content.RectTransform.RelativeSize.Y));
var okButton = passwordMsgBox.Buttons[0];
okButton.OnClicked += (_, __) =>
{
SendPassword(passwordBox.Text);
return true;
};
okButton.OnClicked += passwordMsgBox.Close;
var cancelButton = passwordMsgBox.Buttons[1];
cancelButton.OnClicked = (_, __) =>
{
Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
passwordMsgBox?.Close(); passwordMsgBox = null;
return true;
};
passwordBox.OnEnterPressed += (_, __) =>
{
okButton.OnClicked.Invoke(okButton, okButton.UserData);
return true;
};
passwordBox.Select();
break;
}
}

View File

@@ -9,7 +9,6 @@ namespace Barotrauma.Networking
{
internal sealed class LidgrenClientPeer : ClientPeer
{
private bool isActive;
private NetClient? netClient;
private readonly NetPeerConfiguration netPeerConfiguration;
@@ -94,7 +93,7 @@ namespace Barotrauma.Networking
if (isOwner && !(ChildServerRelay.Process is { HasExited: false }))
{
Close();
Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += (btn, obj) =>
{
@@ -140,8 +139,10 @@ namespace Barotrauma.Networking
var (_, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (packetHeader.IsConnectionInitializationStep() && initializationStep != ConnectionInitialization.Success)
if (packetHeader.IsConnectionInitializationStep())
{
if (initializationStep == ConnectionInitialization.Success) { return; }
ReadConnectionInitializationStep(new IncomingInitializationMessage
{
InitializationStep = initialization ?? throw new Exception("Initialization step missing"),
@@ -170,8 +171,9 @@ namespace Barotrauma.Networking
{
case NetConnectionStatus.Disconnected:
string disconnectMsg = inc.ReadString();
Close(disconnectMsg);
callbacks.OnDisconnectMessageReceived.Invoke(disconnectMsg);
var peerDisconnectPacket =
PeerDisconnectPacket.FromLidgrenStringRepresentation(disconnectMsg);
Close(peerDisconnectPacket.Fallback(PeerDisconnectPacket.WithReason(DisconnectReason.Unknown)));
break;
}
}
@@ -198,7 +200,7 @@ namespace Barotrauma.Networking
SendMsgInternal(headers, body);
}
public override void Close(string? msg = null, bool disableReconnect = false)
public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{
if (!isActive) { return; }
@@ -206,13 +208,13 @@ namespace Barotrauma.Networking
isActive = false;
netClient.Shutdown(msg ?? TextManager.Get("Disconnecting").Value);
netClient.Shutdown(peerDisconnectPacket.ToLidgrenStringRepresentation());
netClient = null;
steamAuthTicket?.Cancel();
steamAuthTicket = null;
callbacks.OnDisconnect.Invoke(disableReconnect);
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
}
public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true)

View File

@@ -10,7 +10,6 @@ namespace Barotrauma.Networking
{
internal sealed class SteamP2PClientPeer : ClientPeer
{
private bool isActive;
private readonly SteamId hostSteamId;
private double timeout;
private double heartbeatTimer;
@@ -97,8 +96,7 @@ namespace Barotrauma.Networking
if (steamId != hostSteamId.Value) { return; }
Close($"SteamP2P connection failed: {error}");
callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P connection failed: {error}");
Close(PeerDisconnectPacket.SteamP2PError(error));
}
private void OnP2PData(ulong steamId, byte[] data, int dataLength)
@@ -117,8 +115,10 @@ namespace Barotrauma.Networking
if (!packetHeader.IsServerMessage()) { return; }
if (packetHeader.IsConnectionInitializationStep() && initialization.HasValue)
if (packetHeader.IsConnectionInitializationStep())
{
if (!initialization.HasValue) { return; }
var relayPacket = INetSerializableStruct.Read<SteamP2PInitializationRelayPacket>(inc);
SteamManager.JoinLobby(relayPacket.LobbyID, false);
@@ -127,7 +127,7 @@ namespace Barotrauma.Networking
incomingInitializationMessages.Add(new IncomingInitializationMessage
{
InitializationStep = initialization.Value,
Message = relayPacket.Message.GetReadMessage()
Message = relayPacket.Message.GetReadMessageUncompressed()
});
}
}
@@ -138,13 +138,12 @@ namespace Barotrauma.Networking
else if (packetHeader.IsDisconnectMessage())
{
PeerDisconnectPacket packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
Close(packet.Message);
callbacks.OnDisconnectMessageReceived.Invoke(packet.Message);
Close(packet);
}
else
{
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
incomingDataMessages.Add(packet.GetReadMessage());
incomingDataMessages.Add(packet.GetReadMessage(packetHeader.IsCompressed(), ServerConnection!));
}
}
@@ -170,14 +169,12 @@ namespace Barotrauma.Networking
{
if (state.P2PSessionError != Steamworks.P2PSessionError.None)
{
Close($"SteamP2P error code: {state.P2PSessionError}");
callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P error code: {state.P2PSessionError}");
Close(PeerDisconnectPacket.SteamP2PError(state.P2PSessionError));
}
}
else
{
Close("SteamP2P connection could not be established");
callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PError.ToString());
Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
}
connectionStatusTimer = 1.0f;
@@ -212,8 +209,7 @@ namespace Barotrauma.Networking
if (timeout < 0.0)
{
Close("Timed out");
callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PTimeOut.ToString());
Close(PeerDisconnectPacket.WithReason(DisconnectReason.SteamP2PTimeOut));
return;
}
@@ -221,20 +217,26 @@ namespace Barotrauma.Networking
{
if (incomingDataMessages.Count > 0)
{
var incomingMessage = incomingDataMessages.First();
byte incomingHeader = incomingMessage.LengthBytes > 0 ? incomingMessage.PeekByte() : (byte)0;
if (ContentPackageOrderReceived)
void initializationError(string errorMsg, string analyticsTag)
{
#warning: TODO: do not allow completing initialization until content package order has been received?
string errorMsg = $"Error during connection initialization: completed initialization before receiving content package order. Incoming header: {incomingHeader}";
GameAnalyticsManager.AddErrorEventOnce("SteamP2PClientPeer.OnInitializationComplete:ContentPackageOrderNotReceived", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnInitializationComplete:{analyticsTag}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
DebugConsole.ThrowError(errorMsg);
Close(PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
}
if (!ContentPackageOrderReceived)
{
initializationError(
errorMsg: "Error during connection initialization: completed initialization before receiving content package order.",
analyticsTag: "ContentPackageOrderNotReceived");
return;
}
if (ServerContentPackages.Length == 0)
{
string errorMsg = $"Error during connection initialization: list of content packages enabled on the server was empty when completing initialization. Incoming header: {incomingHeader}";
GameAnalyticsManager.AddErrorEventOnce("SteamP2PClientPeer.OnInitializationComplete:NoContentPackages", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
DebugConsole.ThrowError(errorMsg);
initializationError(
errorMsg: "Error during connection initialization: list of content packages enabled on the server was empty when completing initialization.",
analyticsTag: "NoContentPackages");
return;
}
callbacks.OnInitializationComplete.Invoke();
initializationStep = ConnectionInitialization.Success;
@@ -320,7 +322,7 @@ namespace Barotrauma.Networking
SendMsgInternal(headers, body);
}
public override void Close(string? msg = null, bool disableReconnect = false)
public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{
if (!isActive) { return; }
@@ -334,12 +336,7 @@ namespace Barotrauma.Networking
PacketHeader = PacketHeader.IsDisconnectMessage,
Initialization = null
};
var body = new PeerDisconnectPacket
{
Message = msg ?? "Disconnected"
};
SendMsgInternal(headers, body);
SendMsgInternal(headers, peerDisconnectPacket);
Thread.Sleep(100);
@@ -349,7 +346,7 @@ namespace Barotrauma.Networking
steamAuthTicket?.Cancel();
steamAuthTicket = null;
callbacks.OnDisconnect.Invoke(disableReconnect);
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
}
protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body)

View File

@@ -9,8 +9,6 @@ namespace Barotrauma.Networking
{
sealed class SteamP2POwnerPeer : ClientPeer
{
private bool isActive;
private readonly SteamId selfSteamID;
private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0));
@@ -68,6 +66,7 @@ namespace Barotrauma.Networking
: throw new InvalidOperationException("Steamworks not initialized");
}
public override void Start()
{
if (isActive) { return; }
@@ -93,22 +92,13 @@ namespace Barotrauma.Networking
private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status)
{
RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId);
DebugConsole.Log($"{steamId} validation: {status}, {remotePeer != null}");
if (remotePeer == null) { return; }
if (remotePeer.Authenticated)
{
if (status != Steamworks.AuthResponse.OK)
{
DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam authentication status changed: {status}");
}
return;
}
if (status == Steamworks.AuthResponse.OK)
{
if (remotePeer.Authenticated) { return; }
SteamId ownerSteamId = new SteamId(ownerId);
remotePeer.OwnerSteamId = Option<SteamId>.Some(ownerSteamId);
remotePeer.Authenticated = true;
@@ -126,7 +116,7 @@ namespace Barotrauma.Networking
}
else
{
DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam authentication failed: {status}");
DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(status));
}
}
@@ -171,7 +161,7 @@ namespace Barotrauma.Networking
Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId);
if (authSessionStartState != Steamworks.BeginAuthResult.OK)
{
DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam auth session failed to start: {authSessionStartState}");
DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(authSessionStartState));
return;
}
}
@@ -197,9 +187,9 @@ namespace Barotrauma.Networking
{
if (!isActive) { return; }
if (ChildServerRelay.HasShutDown || (ChildServerRelay.Process?.HasExited ?? true))
if (ChildServerRelay.HasShutDown || !(ChildServerRelay.Process is { HasExited: false }))
{
Close();
Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += (btn, obj) =>
{
@@ -279,7 +269,7 @@ namespace Barotrauma.Networking
if (packetHeader.IsDisconnectMessage())
{
var packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
DisconnectPeer(peer, packet.Message);
DisconnectPeer(peer, packet);
return;
}
@@ -366,30 +356,20 @@ namespace Barotrauma.Networking
}
}
private void DisconnectPeer(RemotePeer peer, string msg)
private void DisconnectPeer(RemotePeer peer, PeerDisconnectPacket peerDisconnectPacket)
{
if (!string.IsNullOrWhiteSpace(msg))
{
peer.DisconnectTime ??= Timing.TotalTime + 1.0;
peer.DisconnectTime ??= Timing.TotalTime + 1.0;
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage
});
outMsg.WriteNetSerializableStruct(new PeerDisconnectPacket
{
Message = msg
});
Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable);
sentBytes += outMsg.LengthBytes;
}
else
IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders
{
ClosePeerSession(peer);
}
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsServerMessage | PacketHeader.IsDisconnectMessage
});
outMsg.WriteNetSerializableStruct(peerDisconnectPacket);
Steamworks.SteamNetworking.SendP2PPacket(peer.SteamId.Value, outMsg.Buffer, outMsg.LengthBytes, 0, Steamworks.P2PSend.Reliable);
sentBytes += outMsg.LengthBytes;
}
private void ClosePeerSession(RemotePeer peer)
@@ -403,7 +383,7 @@ namespace Barotrauma.Networking
//owner doesn't send passwords
}
public override void Close(string? msg = null, bool disableReconnect = false)
public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{
if (!isActive) { return; }
@@ -411,7 +391,7 @@ namespace Barotrauma.Networking
for (int i = remotePeers.Count - 1; i >= 0; i--)
{
DisconnectPeer(remotePeers[i], msg ?? DisconnectReason.ServerShutdown.ToString());
DisconnectPeer(remotePeers[i], peerDisconnectPacket);
}
Thread.Sleep(100);
@@ -423,7 +403,7 @@ namespace Barotrauma.Networking
ChildServerRelay.ClosePipes();
callbacks.OnDisconnect.Invoke(disableReconnect);
callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
SteamManager.LeaveLobby();
Steamworks.SteamNetworking.ResetActions();

View File

@@ -1,517 +0,0 @@
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace Barotrauma.Networking
{
class ServerInfo
{
public Endpoint Endpoint;
#region TODO: genericize
public int QueryPort;
public UInt64 LobbyID;
public Steamworks.Data.NetPingLocation? PingLocation;
#endregion
public bool OwnerVerified;
private string serverName;
public string ServerName
{
get { return serverName; }
set
{
serverName = value;
if (serverName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); }
}
}
public string ServerMessage;
public bool GameStarted;
public int PlayerCount;
public int MaxPlayers;
public bool HasPassword;
public bool PingChecked;
public int Ping = -1;
//null value means that the value isn't known (the server may be using
//an old version of the game that didn't report these values or the FetchRules query to Steam may not have finished yet)
// TODO: death to Nullable<T>!!!!
public SelectionMode? ModeSelectionMode;
public SelectionMode? SubSelectionMode;
public bool? AllowSpectating;
public bool? VoipEnabled;
public bool? KarmaEnabled;
public bool? FriendlyFireEnabled;
public bool? AllowRespawn;
public YesNoMaybe? TraitorsEnabled;
public Identifier GameMode;
public PlayStyle? PlayStyle;
public bool Recent;
public bool Favorite;
public bool? RespondedToSteamQuery = null;
public Steamworks.Friend? SteamFriend;
public Steamworks.SteamMatchmakingPingResponse MatchmakingPingResponse;
public string GameVersion;
public List<string> ContentPackageNames
{
get;
private set;
} = new List<string>();
public List<string> ContentPackageHashes
{
get;
private set;
} = new List<string>();
public List<ulong> ContentPackageWorkshopIds
{
get;
private set;
} = new List<ulong>();
public void CreatePreviewWindow(GUIFrame frame)
{
if (frame == null) { return; }
frame.ClearChildren();
var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont)
{
ToolTip = ServerName,
CanBeFocused = false
};
title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f));
GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight),
"", null, "GUIServerListFavoriteTickBox")
{
Selected = Favorite,
ToolTip = TextManager.Get(Favorite ? "removefromfavorites" : "addtofavorites"),
OnSelected = (tickbox) =>
{
if (tickbox.Selected)
{
GameMain.ServerListScreen.AddToFavoriteServers(this);
}
else
{
GameMain.ServerListScreen.RemoveFromFavoriteServers(this);
}
tickbox.ToolTip = TextManager.Get(tickbox.Selected ? "removefromfavorites" : "addtofavorites");
return true;
}
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform),
TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"),
string.IsNullOrEmpty(GameVersion) ? TextManager.Get("Unknown") : GameVersion))
{
CanBeFocused = false
};
bool hidePlaystyleBanner = !PlayStyle.HasValue;
if (!hidePlaystyleBanner)
{
PlayStyle playStyle = PlayStyle ?? Networking.PlayStyle.Serious;
Sprite playStyleBannerSprite = ServerListScreen.PlayStyleBanners[(int)playStyle];
float playStyleBannerAspectRatio = playStyleBannerSprite.SourceRect.Width / playStyleBannerSprite.SourceRect.Height;
var playStyleBanner = new GUIImage(new RectTransform(new Point(frame.Rect.Width, (int)(frame.Rect.Width / playStyleBannerAspectRatio)), frame.RectTransform),
playStyleBannerSprite, null, true);
var playStyleName = new GUITextBlock(
new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform)
{ RelativeOffset = new Vector2(0.0f, 0.06f) },
TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"),
TextManager.Get("servertag." + playStyle)), textColor: Color.White,
font: GUIStyle.SmallFont, textAlignment: Alignment.Center,
color: ServerListScreen.PlayStyleColors[(int)playStyle], style: "GUISlopedHeader");
playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint();
playStyleName.RectTransform.IsFixedSize = true;
}
var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform),
Endpoint.ServerTypeString,
textAlignment: Alignment.TopLeft)
{
CanBeFocused = false
};
serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f));
var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform))
{
Stretch = true
};
// playstyle tags -----------------------------------------------------------------------------
var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true)
{
Stretch = true,
RelativeSpacing = 0.01f,
CanBeFocused = true
};
var playStyleTags = GetPlayStyleTags();
foreach (string tag in playStyleTags)
{
if (!ServerListScreen.PlayStyleIcons.ContainsKey(tag)) { continue; }
new GUIImage(new RectTransform(Vector2.One, playStyleContainer.RectTransform),
ServerListScreen.PlayStyleIcons[tag], scaleToFit: true)
{
ToolTip = TextManager.Get("servertagdescription." + tag),
Color = ServerListScreen.PlayStyleIconColors[tag]
};
}
playStyleContainer.Recalculate();
// -----------------------------------------------------------------------------
float elementHeight = 0.075f;
// Spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null);
var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true };
var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage, font: GUIStyle.SmallFont, wrap: true)
{
CanBeFocused = false
};
serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); };
msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); };
var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode"));
new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform),
TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value),
textAlignment: Alignment.Right);
GUITextBlock playStyleText = null;
if (hidePlaystyleBanner && PlayStyle.HasValue)
{
PlayStyle playStyle = PlayStyle.Value;
playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle"));
new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right);
}
var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListSubSelection"));
new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(!SubSelectionMode.HasValue ? "Unknown" : SubSelectionMode.Value.ToString()), textAlignment: Alignment.Right);
var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListModeSelection"));
new GUITextBlock(new RectTransform(Vector2.One, modeSelection.RectTransform), TextManager.Get(!ModeSelectionMode.HasValue ? "Unknown" : ModeSelectionMode.Value.ToString()), textAlignment: Alignment.Right);
if (gameMode.TextSize.X + gameMode.GetChild<GUITextBlock>().TextSize.X > gameMode.Rect.Width ||
subSelection.TextSize.X + subSelection.GetChild<GUITextBlock>().TextSize.X > subSelection.Rect.Width ||
modeSelection.TextSize.X + modeSelection.GetChild<GUITextBlock>().TextSize.X > modeSelection.Rect.Width)
{
gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont;
gameMode.GetChild<GUITextBlock>().Font = subSelection.GetChild<GUITextBlock>().Font = modeSelection.GetChild<GUITextBlock>().Font = GUIStyle.SmallFont;
if (playStyleText != null)
{
playStyleText.Font = playStyleText.GetChild<GUITextBlock>().Font = GUIStyle.SmallFont;
}
}
var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListAllowSpectating"))
{
CanBeFocused = false
};
if (!AllowSpectating.HasValue)
new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), allowSpectating.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center);
else
allowSpectating.Selected = AllowSpectating.Value;
var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerSettingsAllowRespawning"))
{
CanBeFocused = false
};
if (!AllowRespawn.HasValue)
new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), allowRespawn.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center);
else
allowRespawn.Selected = AllowRespawn.Value;
/*var voipEnabledTickBox = new GUITickBox(new RectTransform(new Vector2(1.0f, elementHeight), bodyContainer.RectTransform), TextManager.Get("serversettingsvoicechatenabled"))
{
CanBeFocused = false
};
if (!VoipEnabled.HasValue)
new GUITextBlock(new RectTransform(new Vector2(0.8f, 0.8f), voipEnabledTickBox.Box.RectTransform, Anchor.Center), "?", textAlignment: Alignment.Center);
else
voipEnabledTickBox.Selected = VoipEnabled.Value;*/
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont);
var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform))
{
ScrollBarVisible = true,
OnSelected = (component, o) => false
};
if (ContentPackageNames.Count == 0)
{
new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center)
{
CanBeFocused = false
};
}
else
{
for (int i = 0; i < ContentPackageNames.Count; i++)
{
var packageText = new GUITickBox(
new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform)
{ MinSize = new Point(0, 15) },
ContentPackageNames[i])
{
Enabled = false
};
packageText.Box.Enabled = true;
packageText.TextBlock.Enabled = true;
if (i < ContentPackageHashes.Count)
{
if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == ContentPackageHashes[i]))
{
packageText.TextColor = GUIStyle.Green;
packageText.Selected = true;
}
//workshop download link found
else if (i < ContentPackageWorkshopIds.Count && ContentPackageWorkshopIds[i] != 0)
{
packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", ContentPackageNames[i]);
}
else //no package or workshop download link found (TODO: update text to say that they could be downloaded through the server)
{
packageText.TextColor = GameMain.VanillaContent.NameMatches(ContentPackageNames[i]) ? GUIStyle.Red : GUIStyle.Yellow;
packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage",
("[contentpackage]", ContentPackageNames[i]), ("[hash]", ContentPackageHashes[i]));
}
}
}
}
// -----------------------------------------------------------------------------
foreach (GUIComponent c in content.Children)
{
if (c is GUITextBlock textBlock) { textBlock.Padding = Vector4.Zero; }
}
}
public IEnumerable<string> GetPlayStyleTags()
{
List<string> tags = new List<string>();
if (KarmaEnabled.HasValue)
{
tags.Add(KarmaEnabled.Value ? "karma.true" : "karma.false");
}
if (TraitorsEnabled.HasValue)
{
tags.Add(TraitorsEnabled.Value == YesNoMaybe.Maybe ?
"traitors.maybe" :
(TraitorsEnabled.Value == YesNoMaybe.Yes ? "traitors.true" : "traitors.false"));
}
if (VoipEnabled.HasValue)
{
tags.Add(VoipEnabled.Value ? "voip.true" : "voip.false");
}
if (FriendlyFireEnabled.HasValue)
{
tags.Add(FriendlyFireEnabled.Value ? "friendlyfire.true" : "friendlyfire.false");
}
if (ContentPackageNames.Count > 0)
{
tags.Add(ContentPackageNames.Count > 1 || !GameMain.VanillaContent.NameMatches(ContentPackageNames[0]) ? "modded.true" : "modded.false");
}
return tags;
}
public static ServerInfo FromXElement(XElement element)
{
string endpointStr
= element.GetAttributeString("Endpoint", null)
?? element.GetAttributeString("OwnerID", null)
?? $"{element.GetAttributeString("IP", "")}:{element.GetAttributeInt("Port", 0)}";
if (!(Endpoint.Parse(endpointStr).TryUnwrap(out var endpoint))) { return null; }
ServerInfo info = new ServerInfo
{
ServerName = element.GetAttributeString("ServerName", ""),
ServerMessage = element.GetAttributeString("ServerMessage", ""),
Endpoint = endpoint,
QueryPort = !string.IsNullOrEmpty(element.GetAttributeString("QueryPort", string.Empty)) ? element.GetAttributeInt("QueryPort", 0) : 0,
GameMode = element.GetAttributeIdentifier("GameMode", Identifier.Empty),
GameVersion = element.GetAttributeString("GameVersion", ""),
MaxPlayers = Math.Min(element.GetAttributeInt("MaxPlayers", 0), NetConfig.MaxPlayers),
HasPassword = element.GetAttributeBool("HasPassword", false),
RespondedToSteamQuery = null
};
if (Enum.TryParse(element.GetAttributeString("PlayStyle", ""), out PlayStyle playStyleTemp)) { info.PlayStyle = playStyleTemp; }
if (Enum.TryParse(element.GetAttributeString("TraitorsEnabled", ""), out YesNoMaybe traitorsTemp)) { info.TraitorsEnabled = traitorsTemp; }
if (Enum.TryParse(element.GetAttributeString("SubSelectionMode", ""), out SelectionMode subSelectionTemp)) { info.SubSelectionMode = subSelectionTemp; }
if (Enum.TryParse(element.GetAttributeString("ModeSelectionMode", ""), out SelectionMode modeSelectionTemp)) { info.ModeSelectionMode = modeSelectionTemp; }
if (bool.TryParse(element.GetAttributeString("VoipEnabled", ""), out bool voipTemp)) { info.VoipEnabled = voipTemp; }
if (bool.TryParse(element.GetAttributeString("KarmaEnabled", ""), out bool karmaTemp)) { info.KarmaEnabled = karmaTemp; }
if (bool.TryParse(element.GetAttributeString("FriendlyFireEnabled", ""), out bool friendlyFireTemp)) { info.FriendlyFireEnabled = friendlyFireTemp; }
return info;
}
public void QueryLiveInfo(Action<Networking.ServerInfo> onServerRulesReceived, Action<Networking.ServerInfo> onQueryDone)
{
if (!SteamManager.IsInitialized) { return; }
if (QueryPort != 0 && Endpoint is LidgrenEndpoint { NetEndpoint: { Address: var ipAddress } })
{
if (MatchmakingPingResponse is { QueryActive: true })
{
MatchmakingPingResponse.Cancel();
}
MatchmakingPingResponse = new Steamworks.SteamMatchmakingPingResponse(
(server) =>
{
ServerName = server.Name;
RespondedToSteamQuery = true;
PlayerCount = server.Players;
MaxPlayers = server.MaxPlayers;
HasPassword = server.Passworded;
PingChecked = true;
Ping = server.Ping;
LobbyID = 0;
TaskPool.Add("QueryServerRules (QueryLiveInfo)", server.QueryRulesAsync(),
(t) =>
{
onQueryDone(this);
if (t.Status == TaskStatus.Faulted)
{
TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + ServerName);
return;
}
t.TryGetResult(out Dictionary<string, string> rules);
SteamManager.AssignServerRulesToServerInfo(rules, this);
onServerRulesReceived(this);
});
},
() =>
{
RespondedToSteamQuery = false;
});
MatchmakingPingResponse.HQueryPing(ipAddress, QueryPort);
}
else if (Endpoint is SteamP2PEndpoint { SteamId: var ownerId })
{
SteamFriend ??= new Steamworks.Friend(ownerId.Value);
if (LobbyID == 0)
{
TaskPool.Add("RequestSteamP2POwnerInfo", SteamFriend?.RequestInfoAsync(),
(t) =>
{
onQueryDone(this);
if ((SteamFriend?.IsPlayingThisGame ?? false) && ((SteamFriend?.GameInfo?.Lobby?.Id ?? 0) != 0))
{
LobbyID = SteamFriend?.GameInfo?.Lobby?.Id.Value ?? 0;
Steamworks.SteamMatchmaking.OnLobbyDataChanged += UpdateInfoFromSteamworksLobby;
SteamFriend?.GameInfo?.Lobby?.Refresh();
}
else
{
RespondedToSteamQuery = false;
}
});
}
else
{
onQueryDone(this);
}
}
}
private void UpdateInfoFromSteamworksLobby(Steamworks.Data.Lobby lobby)
{
if (lobby.Id != LobbyID) { return; }
Steamworks.SteamMatchmaking.OnLobbyDataChanged -= UpdateInfoFromSteamworksLobby;
if (string.IsNullOrWhiteSpace(lobby.GetData("haspassword"))) { return; }
bool.TryParse(lobby.GetData("haspassword"), out bool hasPassword);
int.TryParse(lobby.GetData("playercount"), out int currPlayers);
int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers);
if (!SteamId.Parse(lobby.GetData("lobbyowner")).TryUnwrap(out var ownerId)) { return; }
if (!(Endpoint is SteamP2PEndpoint { SteamId: var id }) || id != ownerId) { return; }
ServerName = lobby.GetData("name");
PlayerCount = currPlayers;
MaxPlayers = maxPlayers;
HasPassword = hasPassword;
RespondedToSteamQuery = true;
PingChecked = false;
OwnerVerified = true;
SteamManager.AssignLobbyDataToServerInfo(lobby, this);
}
public XElement ToXElement()
{
if (Endpoint is null)
{
return null; //can't save this one since it's not set up correctly
}
XElement element = new XElement("ServerInfo");
element.SetAttributeValue("ServerName", ServerName);
element.SetAttributeValue("ServerMessage", ServerMessage);
element.SetAttributeValue("Endpoint", Endpoint.ToString());
element.SetAttributeValue("GameMode", GameMode);
element.SetAttributeValue("GameVersion", GameVersion ?? "");
element.SetAttributeValue("MaxPlayers", MaxPlayers);
if (PlayStyle.HasValue) { element.SetAttributeValue("PlayStyle", PlayStyle.Value.ToString()); }
if (TraitorsEnabled.HasValue) { element.SetAttributeValue("TraitorsEnabled", TraitorsEnabled.Value.ToString()); }
if (SubSelectionMode.HasValue) { element.SetAttributeValue("SubSelectionMode", SubSelectionMode.Value.ToString()); }
if (ModeSelectionMode.HasValue) { element.SetAttributeValue("ModeSelectionMode", ModeSelectionMode.Value.ToString()); }
if (VoipEnabled.HasValue) { element.SetAttributeValue("VoipEnabled", VoipEnabled.Value.ToString()); }
if (KarmaEnabled.HasValue) { element.SetAttributeValue("KarmaEnabled", KarmaEnabled.Value.ToString()); }
if (FriendlyFireEnabled.HasValue) { element.SetAttributeValue("FriendlyFireEnabled", FriendlyFireEnabled.Value.ToString()); }
element.SetAttributeValue("HasPassword", HasPassword.ToString());
return element;
}
public override bool Equals(object obj)
{
return obj is ServerInfo other ? Equals(other) : base.Equals(obj);
}
public bool Equals(ServerInfo other)
{
return
other.Endpoint == Endpoint &&
(other.LobbyID == LobbyID || other.LobbyID == 0 || LobbyID == 0);
}
/// <summary>
/// This class is trash, so punish its use by making it horribly inefficient in hashsets
/// Doing anything else here would make it cause even more bugs
/// </summary>
public override int GetHashCode() => 0;
public bool MatchesByEndpoint(ServerInfo other)
{
return other.Endpoint == Endpoint;
}
}
}

View File

@@ -0,0 +1,11 @@
#nullable enable
namespace Barotrauma
{
abstract class FriendProvider
{
public abstract ServerListScreen.FriendInfo[] RetrieveFriends();
public abstract void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize);
public abstract string GetUserName();
}
}

View File

@@ -0,0 +1,67 @@
#nullable enable
using System;
using System.Linq;
using System.Threading.Tasks;
using Barotrauma.Networking;
using Barotrauma.Steam;
using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
class SteamFriendProvider : FriendProvider
{
private static ServerListScreen.FriendInfo FromSteamFriend(Steamworks.Friend steamFriend)
=> new ServerListScreen.FriendInfo(
steamFriend.Name,
new SteamId(steamFriend.Id),
steamFriend.State switch
{
Steamworks.FriendState.Offline => ServerListScreen.FriendInfo.Status.Offline,
Steamworks.FriendState.Invisible => ServerListScreen.FriendInfo.Status.Offline,
_ when steamFriend.IsPlayingThisGame => ServerListScreen.FriendInfo.Status.PlayingBarotrauma,
_ when steamFriend.GameInfo is { GameID: var gameId } && gameId > 0 => ServerListScreen.FriendInfo.Status.PlayingAnotherGame,
_ => ServerListScreen.FriendInfo.Status.NotPlaying
})
{
ServerName = steamFriend.GetRichPresence("servername"),
ConnectCommand = steamFriend.GetRichPresence("connect") is { } connectCmd
? ToolBox.ParseConnectCommand(ToolBox.SplitCommand(connectCmd))
: Option<ConnectCommand>.None()
};
public override ServerListScreen.FriendInfo[] RetrieveFriends()
=> SteamManager.IsInitialized
? Steamworks.SteamFriends.GetFriends().Select(FromSteamFriend).ToArray()
: Array.Empty<ServerListScreen.FriendInfo>();
public override void RetrieveAvatar(ServerListScreen.FriendInfo friend, ServerListScreen.AvatarSize avatarSize)
{
if (!(friend.Id is SteamId steamId)) { return; }
Func<Steamworks.SteamId, Task<Steamworks.Data.Image?>> avatarFunc = avatarSize switch
{
ServerListScreen.AvatarSize.Small => Steamworks.SteamFriends.GetSmallAvatarAsync,
ServerListScreen.AvatarSize.Medium => Steamworks.SteamFriends.GetMediumAvatarAsync,
ServerListScreen.AvatarSize.Large => Steamworks.SteamFriends.GetLargeAvatarAsync,
};
TaskPool.Add($"Get{avatarSize}AvatarAsync", avatarFunc(steamId.Value), task =>
{
if (!task.TryGetResult(out Steamworks.Data.Image? img)) { return; }
if (!(img is { } avatarImage)) { return; }
if (friend.Avatar.TryUnwrap(out var prevAvatar))
{
prevAvatar.Remove();
}
#warning TODO: create an avatar atlas?
var avatarTexture = new Texture2D(GameMain.Instance.GraphicsDevice, (int)avatarImage.Width, (int)avatarImage.Height);
avatarTexture.SetData(avatarImage.Data);
friend.Avatar = Option<Sprite>.Some(new Sprite(avatarTexture, null, null));
});
}
public override string GetUserName()
=> SteamManager.GetUsername();
}
}

View File

@@ -0,0 +1,196 @@
using Barotrauma.Steam;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading.Tasks;
using Steamworks.Data;
using Color = Microsoft.Xna.Framework.Color;
using Socket = System.Net.Sockets.Socket;
namespace Barotrauma.Networking
{
static class PingUtils
{
private static readonly Dictionary<IPAddress, int> activePings = new Dictionary<IPAddress, int>();
private static bool steamPingInfoReady;
public static void QueryPingData()
{
steamPingInfoReady = false;
if (SteamManager.IsInitialized)
{
TaskPool.Add("WaitForPingDataAsync (serverlist)", Steamworks.SteamNetworkingUtils.WaitForPingDataAsync(), task =>
{
steamPingInfoReady = true;
});
}
}
public static void GetServerPing(ServerInfo serverInfo, Action<ServerInfo> onPingDiscovered)
{
if (CoroutineManager.IsCoroutineRunning("ConnectToServer")) { return; }
switch (serverInfo.Endpoint)
{
case LidgrenEndpoint { NetEndpoint: { Address: var address } }:
GetIPAddressPing(serverInfo, address, onPingDiscovered);
break;
case SteamP2PEndpoint steamP2PEndpoint:
TaskPool.Add($"EstimateSteamLobbyPing ({steamP2PEndpoint.StringRepresentation})",
EstimateSteamLobbyPing(serverInfo),
t =>
{
if (!t.TryGetResult(out Option<int> ping)) { return; }
serverInfo.Ping = ping;
onPingDiscovered(serverInfo);
});
break;
}
}
private readonly ref struct LobbyDataChangedEventHandler
{
private readonly Action<Lobby> action;
public LobbyDataChangedEventHandler(Action<Lobby> action)
{
this.action = action;
Steamworks.SteamMatchmaking.OnLobbyDataChanged += action;
}
public void Dispose()
{
Steamworks.SteamMatchmaking.OnLobbyDataChanged -= action;
}
}
public static async Task<Lobby?> GetSteamLobbyForUser(SteamId steamId)
{
var steamFriend = new Steamworks.Friend(steamId.Value);
await steamFriend.RequestInfoAsync();
var friendLobby = steamFriend.GameInfo?.Lobby;
if (!(friendLobby is { } lobby)) { return null; }
bool waiting = true;
Lobby loadedLobby = default;
void finishWaiting(Steamworks.Data.Lobby l)
{
loadedLobby = l;
waiting = false;
}
using (new LobbyDataChangedEventHandler(finishWaiting))
{
lobby.Refresh();
for (int i = 0;; i++)
{
if (!waiting) { break; }
if (i >= 100) { return null; }
}
}
return loadedLobby;
}
private static async Task<Option<int>> EstimateSteamLobbyPing(ServerInfo serverInfo)
{
if (!(serverInfo.Endpoint is SteamP2PEndpoint { SteamId: var ownerId })) { return Option<int>.None(); }
while (!steamPingInfoReady) { await Task.Delay(50); }
Lobby lobby;
if (serverInfo.MetadataSource.TryUnwrap(out SteamP2PServerProvider.DataSource src))
{
lobby = src.Lobby;
}
else
{
var friendLobby = await GetSteamLobbyForUser(ownerId);
if (friendLobby is null) { return Option<int>.None(); }
lobby = friendLobby.Value;
}
var pingLocation = NetPingLocation.TryParseFromString(lobby.GetData("pinglocation"));
if (pingLocation.HasValue && Steamworks.SteamNetworkingUtils.LocalPingLocation.HasValue)
{
int ping = Steamworks.SteamNetworkingUtils.LocalPingLocation.Value.EstimatePingTo(pingLocation.Value);
return ping >= 0 ? Option<int>.Some(ping) : Option<int>.None();
}
else
{
return Option<int>.None();
}
}
private static void GetIPAddressPing(ServerInfo serverInfo, IPAddress address, Action<ServerInfo> onPingDiscovered)
{
lock (activePings)
{
if (activePings.ContainsKey(address)) { return; }
activePings.Add(address, activePings.Any() ? activePings.Values.Max() + 1 : 0);
}
serverInfo.Ping = Option<int>.None();
TaskPool.Add($"PingServerAsync ({address})", PingServerAsync(address, 1000),
rtt =>
{
if (!rtt.TryGetResult(out serverInfo.Ping)) { serverInfo.Ping = Option<int>.None(); }
onPingDiscovered(serverInfo);
lock (activePings)
{
activePings.Remove(address);
}
});
}
private static async Task<Option<int>> PingServerAsync(IPAddress ipAddress, int timeOut)
{
await Task.Yield();
bool shouldGo = false;
while (!shouldGo)
{
lock (activePings)
{
shouldGo = activePings.Count(kvp => kvp.Value < activePings[ipAddress]) < 25;
}
await Task.Delay(25);
}
if (ipAddress == null) { return Option<int>.None(); }
//don't attempt to ping if the address is IPv6 and it's not supported
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6 && !Socket.OSSupportsIPv6) { return Option<int>.None(); }
Ping ping = new Ping();
byte[] buffer = new byte[32];
try
{
PingReply pingReply = await ping.SendPingAsync(ipAddress, timeOut, buffer, new PingOptions(128, true));
return pingReply.Status switch
{
IPStatus.Success => Option<int>.Some((int)pingReply.RoundtripTime),
_ => Option<int>.None(),
};
}
catch (Exception ex)
{
GameAnalyticsManager.AddErrorEventOnce("ServerListScreen.PingServer:PingException" + ipAddress, GameAnalyticsManager.ErrorSeverity.Warning, "Failed to ping a server - " + (ex?.InnerException?.Message ?? ex.Message));
#if DEBUG
DebugConsole.NewMessage("Failed to ping a server (" + ipAddress + ") - " + (ex?.InnerException?.Message ?? ex.Message), Color.Red);
#endif
return Option<int>.None();
}
}
}
}

View File

@@ -0,0 +1,509 @@
#nullable enable
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Barotrauma.Steam;
namespace Barotrauma.Networking
{
sealed class ServerInfo : ISerializableEntity
{
public abstract class DataSource
{
public static Option<DataSource> Parse(XElement element)
=> ReflectionUtils.ParseDerived<DataSource, XElement>(element);
public abstract void Write(XElement element);
}
public Endpoint Endpoint { get; private set; }
public Option<DataSource> MetadataSource = Option<DataSource>.None();
[Serialize("", IsPropertySaveable.Yes)]
public string ServerName { get; set; } = "";
[Serialize("", IsPropertySaveable.Yes)]
public string ServerMessage { get; set; } = "";
public int PlayerCount { get; set; }
[Serialize(0, IsPropertySaveable.Yes)]
public int MaxPlayers { get; set; }
public bool GameStarted { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool HasPassword { get; set; }
[Serialize("", IsPropertySaveable.Yes)]
public Identifier GameMode { get; set; }
[Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)]
public SelectionMode ModeSelectionMode { get; set; }
[Serialize(SelectionMode.Manual, IsPropertySaveable.Yes)]
public SelectionMode SubSelectionMode { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AllowSpectating { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool VoipEnabled { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool KarmaEnabled { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool FriendlyFireEnabled { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool AllowRespawn { get; set; }
[Serialize(YesNoMaybe.No, IsPropertySaveable.Yes)]
public YesNoMaybe TraitorsEnabled { get; set; }
[Serialize(PlayStyle.Casual, IsPropertySaveable.Yes)]
public PlayStyle PlayStyle { get; set; }
public Version GameVersion { get; set; } = new Version(0,0,0,0);
public Option<int> Ping = Option<int>.None();
public bool Checked = false;
public readonly struct ContentPackageInfo
{
public readonly string Name;
public readonly string Hash;
public readonly Option<ContentPackageId> Id;
public ContentPackageInfo(string name, string hash, Option<ContentPackageId> id)
{
Name = name;
Hash = hash;
Id = id;
}
public ContentPackageInfo(ContentPackage pkg)
{
Name = pkg.Name;
Hash = pkg.Hash.StringRepresentation;
Id = pkg.UgcId;
}
}
public ImmutableArray<ContentPackageInfo> ContentPackages;
public bool IsModded => ContentPackages.Any(p => !GameMain.VanillaContent.NameMatches(p.Name));
public ServerInfo(Endpoint endpoint)
{
SerializableProperties = SerializableProperty.GetProperties(this);
Endpoint = endpoint;
ContentPackages = ImmutableArray<ContentPackageInfo>.Empty;
}
public static ServerInfo FromServerConnection(NetworkConnection connection, ServerSettings serverSettings)
{
var serverInfo = new ServerInfo(connection.Endpoint)
{
GameMode = GameMain.NetLobbyScreen.SelectedMode?.Identifier ?? Identifier.Empty,
GameStarted = Screen.Selected != GameMain.NetLobbyScreen,
GameVersion = GameMain.Version,
PlayerCount = GameMain.Client.ConnectedClients.Count,
ContentPackages = ContentPackageManager.EnabledPackages.All.Select(p => new ContentPackageInfo(p)).ToImmutableArray(),
Ping = GameMain.Client.Ping,
// -------------------------------------
// Settings that cannot be copied via
// SerializableProperty because they do
// not implement the attribute
ServerName = serverSettings.ServerName,
ServerMessage = serverSettings.ServerMessageText,
// -------------------------------------
// Settings that cannot be copied via
// SerializableProperty due to name mismatch
HasPassword = serverSettings.HasPassword,
VoipEnabled = serverSettings.VoiceChatEnabled,
FriendlyFireEnabled = serverSettings.AllowFriendlyFire,
// -------------------------------------
Checked = true
};
var serverInfoSerializableProperties
= SerializableProperty.GetProperties(serverInfo);
var serverSettingsSerializableProperties
= SerializableProperty.GetProperties(serverSettings);
var intersection = serverInfoSerializableProperties.Keys
.Where(serverSettingsSerializableProperties.ContainsKey);
foreach (var key in intersection)
{
var propToGet = serverSettingsSerializableProperties[key];
var propToSet = serverInfoSerializableProperties[key];
if (!propToGet.PropertyInfo.CanRead) { continue; }
if (!propToSet.PropertyInfo.CanWrite) { continue; }
propToSet.SetValue(
serverInfo,
propToGet.GetValue(serverSettings));
}
return serverInfo;
}
public void CreatePreviewWindow(GUIFrame frame)
{
frame.ClearChildren();
var serverListScreen = GameMain.ServerListScreen;
var title = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform), ServerName, font: GUIStyle.LargeFont)
{
ToolTip = ServerName,
CanBeFocused = false
};
title.Text = ToolBox.LimitString(title.Text, title.Font, (int)(title.Rect.Width * 0.85f));
bool isFavorite = serverListScreen.IsFavorite(this);
static LocalizedString favoriteTickBoxToolTip(bool isFavorite)
=> TextManager.Get(isFavorite ? "RemoveFromFavorites" : "AddToFavorites");
GUITickBox favoriteTickBox = new GUITickBox(new RectTransform(new Vector2(0.15f, 0.8f), title.RectTransform, Anchor.CenterRight),
"", null, "GUIServerListFavoriteTickBox")
{
UserData = this,
Selected = isFavorite,
ToolTip = favoriteTickBoxToolTip(isFavorite),
OnSelected = tickbox =>
{
ServerInfo info = (ServerInfo)tickbox.UserData;
if (tickbox.Selected)
{
GameMain.ServerListScreen.AddToFavoriteServers(info);
}
else
{
GameMain.ServerListScreen.RemoveFromFavoriteServers(info);
}
tickbox.ToolTip = favoriteTickBoxToolTip(tickbox.Selected);
return true;
}
};
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform),
TextManager.AddPunctuation(':', TextManager.Get("ServerListVersion"),
GameVersion.ToString()))
{
CanBeFocused = false
};
PlayStyle playStyle = PlayStyle;
Sprite? playStyleBannerSprite = GUIStyle.GetComponentStyle($"PlayStyleBanner.{playStyle}")?.GetSprite(GUIComponent.ComponentState.None);
GUIComponent playStyleBanner;
Color playStyleBannerColor;
if (playStyleBannerSprite != null)
{
float playStyleBannerAspectRatio = (float)playStyleBannerSprite.SourceRect.Width / (float)playStyleBannerSprite.SourceRect.Height;
playStyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 1.0f / playStyleBannerAspectRatio), frame.RectTransform, scaleBasis: ScaleBasis.BothWidth),
playStyleBannerSprite, null, true);
playStyleBannerColor = playStyleBannerSprite.SourceElement.GetAttributeColor("bannercolor", Color.Black);
}
else
{
playStyleBanner = new GUIFrame(new RectTransform((1.0f, 0.2f), frame.RectTransform), style: null)
{
Color = Color.Black,
DisabledColor = Color.Black,
OutlineColor = Color.Black,
PressedColor = Color.Black,
SelectedColor = Color.Black,
HoverColor = Color.Black
};
playStyleBannerColor = Color.Black;
}
var playStyleName = new GUITextBlock(
new RectTransform(new Vector2(0.15f, 0.0f), playStyleBanner.RectTransform)
{ RelativeOffset = new Vector2(0.0f, 0.06f) },
TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"),
TextManager.Get($"servertag.{playStyle}")), textColor: Color.White,
font: GUIStyle.SmallFont, textAlignment: Alignment.Center,
color: playStyleBannerColor, style: "GUISlopedHeader");
playStyleName.RectTransform.NonScaledSize = (playStyleName.Font.MeasureString(playStyleName.Text) + new Vector2(20, 5) * GUI.Scale).ToPoint();
playStyleName.RectTransform.IsFixedSize = true;
var serverType = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), frame.RectTransform),
Endpoint?.ServerTypeString ?? string.Empty,
textAlignment: Alignment.TopLeft)
{
CanBeFocused = false
};
serverType.RectTransform.MinSize = new Point(0, (int)(serverType.Rect.Height * 1.5f));
var content = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.6f), frame.RectTransform))
{
Stretch = true
};
// playstyle tags -----------------------------------------------------------------------------
var playStyleContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.15f), content.RectTransform), isHorizontal: true)
{
Stretch = true,
RelativeSpacing = 0.01f,
CanBeFocused = true
};
var playStyleTags = GetPlayStyleTags();
foreach (var tag in playStyleTags)
{
var playStyleIcon = GUIStyle.GetComponentStyle($"PlayStyleIcon.{tag}")
?.GetSprite(GUIComponent.ComponentState.None);
if (playStyleIcon is null) { continue; }
new GUIImage(new RectTransform(Vector2.One, playStyleContainer.RectTransform),
playStyleIcon, scaleToFit: true)
{
ToolTip = TextManager.Get($"servertagdescription.{tag}"),
Color = Color.White
};
}
playStyleContainer.Recalculate();
// -----------------------------------------------------------------------------
float elementHeight = 0.075f;
// Spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.025f), content.RectTransform), style: null);
var serverMsg = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), content.RectTransform)) { ScrollBarVisible = true };
var msgText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), serverMsg.Content.RectTransform), ServerMessage ?? string.Empty, font: GUIStyle.SmallFont, wrap: true)
{
CanBeFocused = false
};
serverMsg.Content.RectTransform.SizeChanged += () => { msgText.CalculateHeightFromText(); };
msgText.RectTransform.SizeChanged += () => { serverMsg.UpdateScrollBarSize(); };
var gameMode = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("GameMode"));
new GUITextBlock(new RectTransform(Vector2.One, gameMode.RectTransform),
TextManager.Get(GameMode.IsEmpty ? "Unknown" : "GameMode." + GameMode).Fallback(GameMode.Value),
textAlignment: Alignment.Right);
GUITextBlock playStyleText = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("serverplaystyle"));
new GUITextBlock(new RectTransform(Vector2.One, playStyleText.RectTransform), TextManager.Get("servertag." + playStyle), textAlignment: Alignment.Right);
var subSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListSubSelection"));
new GUITextBlock(new RectTransform(Vector2.One, subSelection.RectTransform), TextManager.Get(SubSelectionMode.ToString()), textAlignment: Alignment.Right);
var modeSelection = new GUITextBlock(new RectTransform(new Vector2(1.0f, elementHeight), content.RectTransform), TextManager.Get("ServerListModeSelection"));
new GUITextBlock(new RectTransform(Vector2.One, modeSelection.RectTransform), TextManager.Get(ModeSelectionMode.ToString()), textAlignment: Alignment.Right);
if (gameMode.TextSize.X + gameMode.GetChild<GUITextBlock>().TextSize.X > gameMode.Rect.Width ||
subSelection.TextSize.X + subSelection.GetChild<GUITextBlock>().TextSize.X > subSelection.Rect.Width ||
modeSelection.TextSize.X + modeSelection.GetChild<GUITextBlock>().TextSize.X > modeSelection.Rect.Width)
{
gameMode.Font = subSelection.Font = modeSelection.Font = GUIStyle.SmallFont;
gameMode.GetChild<GUITextBlock>().Font = subSelection.GetChild<GUITextBlock>().Font = modeSelection.GetChild<GUITextBlock>().Font = GUIStyle.SmallFont;
playStyleText.Font = playStyleText.GetChild<GUITextBlock>().Font = GUIStyle.SmallFont;
}
var allowSpectating = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerListAllowSpectating"))
{
CanBeFocused = false
};
allowSpectating.Selected = AllowSpectating;
var allowRespawn = new GUITickBox(new RectTransform(new Vector2(1, elementHeight), content.RectTransform), TextManager.Get("ServerSettingsAllowRespawning"))
{
CanBeFocused = false
};
allowRespawn.Selected = AllowRespawn;
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), content.RectTransform),
TextManager.Get("ServerListContentPackages"), textAlignment: Alignment.Center, font: GUIStyle.SubHeadingFont);
var contentPackageList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), frame.RectTransform))
{
ScrollBarVisible = true,
OnSelected = (component, o) => false
};
if (ContentPackages.Length == 0)
{
new GUITextBlock(new RectTransform(Vector2.One, contentPackageList.Content.RectTransform), TextManager.Get("Unknown"), textAlignment: Alignment.Center)
{
CanBeFocused = false
};
}
else
{
foreach (var package in ContentPackages)
{
var packageText = new GUITickBox(
new RectTransform(new Vector2(1.0f, 0.15f), contentPackageList.Content.RectTransform)
{ MinSize = new Point(0, 15) },
package.Name)
{
CanBeFocused = false
};
if (!string.IsNullOrEmpty(package.Hash))
{
if (ContentPackageManager.AllPackages.Any(contentPackage => contentPackage.Hash.StringRepresentation == package.Hash))
{
packageText.TextColor = GUIStyle.Green;
packageText.Selected = true;
}
//workshop download link found
else if (package.Id is Some<ContentPackageId> { Value: var ugcId } && ugcId is SteamWorkshopId)
{
packageText.ToolTip = TextManager.GetWithVariable("ServerListIncompatibleContentPackageWorkshopAvailable", "[contentpackage]", package.Name);
}
else //no package or workshop download link found
{
packageText.TextColor = GameMain.VanillaContent.NameMatches(package.Name) ? GUIStyle.Red : GUIStyle.Yellow;
packageText.ToolTip = TextManager.GetWithVariables("ServerListIncompatibleContentPackage",
("[contentpackage]", package.Name), ("[hash]", package.Hash));
}
}
}
}
// -----------------------------------------------------------------------------
foreach (GUIComponent c in content.Children)
{
if (c is GUITextBlock textBlock) { textBlock.Padding = Vector4.Zero; }
}
}
public IEnumerable<Identifier> GetPlayStyleTags()
{
yield return $"Karma.{KarmaEnabled}".ToIdentifier();
yield return (TraitorsEnabled == YesNoMaybe.Yes ? $"Traitors.True" : $"Traitors.False").ToIdentifier();
yield return $"VoIP.{VoipEnabled}".ToIdentifier();
yield return $"FriendlyFire.{FriendlyFireEnabled}".ToIdentifier();
yield return $"Modded.{ContentPackages.Any()}".ToIdentifier();
}
public void UpdateInfo(Func<string, string?> valueGetter)
{
ServerMessage = valueGetter("message") ?? "";
GameVersion = Version.TryParse(valueGetter("version"), out var version)
? version
: GameMain.Version;
if (int.TryParse(valueGetter("playercount"), out int playerCount)) { PlayerCount = playerCount; }
if (int.TryParse(valueGetter("maxplayernum"), out int maxPlayers)) { MaxPlayers = maxPlayers; }
if (Enum.TryParse(valueGetter("modeselectionmode"), out SelectionMode modeSelectionMode)) { ModeSelectionMode = modeSelectionMode; }
if (Enum.TryParse(valueGetter("subselectionmode"), out SelectionMode subSelectionMode)) { SubSelectionMode = subSelectionMode; }
HasPassword = getBool("haspassword");
GameStarted = getBool("gamestarted");
KarmaEnabled = getBool("karmaenabled");
FriendlyFireEnabled = getBool("friendlyfireenabled");
AllowSpectating = getBool("allowspectating");
AllowRespawn = getBool("allowrespawn");
VoipEnabled = getBool("voicechatenabled");
GameMode = valueGetter("gamemode")?.ToIdentifier() ?? Identifier.Empty;
if (Enum.TryParse(valueGetter("traitors"), out YesNoMaybe traitorsEnabled)) { TraitorsEnabled = traitorsEnabled; }
if (Enum.TryParse(valueGetter("playstyle"), out PlayStyle playStyle)) { PlayStyle = playStyle; }
ContentPackages = ExtractContentPackageInfo(valueGetter).ToImmutableArray();
bool getBool(string key)
{
string? data = valueGetter(key);
return bool.TryParse(data, out var result) && result;
}
}
private static ContentPackageInfo[] ExtractContentPackageInfo(Func<string, string?> valueGetter)
{
string? joinedNames = valueGetter("contentpackage");
string? joinedHashes = valueGetter("contentpackagehash");
string? joinedWorkshopIds = valueGetter("contentpackageid");
string[] contentPackageNames = joinedNames.IsNullOrEmpty() ? Array.Empty<string>() : joinedNames.Split(',');
string[] contentPackageHashes = joinedHashes.IsNullOrEmpty() ? Array.Empty<string>() : joinedHashes.Split(',');
#warning TODO: genericize
ulong[] contentPackageIds = joinedWorkshopIds.IsNullOrEmpty() ? new ulong[1] : SteamManager.ParseWorkshopIds(joinedWorkshopIds).ToArray();
if (contentPackageNames.Length != contentPackageHashes.Length
|| contentPackageHashes.Length != contentPackageIds.Length)
{
return Array.Empty<ContentPackageInfo>();
}
return contentPackageNames
.Zip(contentPackageHashes, (name, hash) => (name, hash))
.Zip(contentPackageIds, (t1, id) =>
new ContentPackageInfo(
t1.name,
t1.hash,
Option<ContentPackageId>.Some(new SteamWorkshopId(id))))
.ToArray();
}
public static Option<ServerInfo> FromXElement(XElement element)
{
string endpointStr
= element.GetAttributeString("Endpoint", null)
?? element.GetAttributeString("OwnerID", null)
?? $"{element.GetAttributeString("IP", "")}:{element.GetAttributeInt("Port", 0)}";
if (!Endpoint.Parse(endpointStr).TryUnwrap(out var endpoint)) { return Option<ServerInfo>.None(); }
var gameVersionStr = element.GetAttributeString("GameVersion", "");
if (!Version.TryParse(gameVersionStr, out var gameVersion)) { gameVersion = GameMain.Version; }
var info = new ServerInfo(endpoint)
{
GameVersion = gameVersion
};
SerializableProperty.DeserializeProperties(info, element);
info.MetadataSource = DataSource.Parse(element);
return Option<ServerInfo>.Some(info);
}
public XElement ToXElement()
{
XElement element = new XElement(GetType().Name);
element.SetAttributeValue("Endpoint", Endpoint.ToString());
element.SetAttributeValue("GameVersion", GameVersion.ToString());
SerializableProperty.SerializeProperties(this, element, saveIfDefault: true);
if (MetadataSource.TryUnwrap(out var dataSource))
{
dataSource.Write(element);
}
return element;
}
public override bool Equals(object? obj)
{
return obj is ServerInfo other && Equals(other);
}
public bool Equals(ServerInfo other)
=> other.Endpoint == Endpoint;
public override int GetHashCode() => Endpoint.GetHashCode();
string ISerializableEntity.Name => "ServerInfo";
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; }
}
}

View File

@@ -0,0 +1,35 @@
#nullable enable
using System;
using System.Collections.Immutable;
using Barotrauma.Extensions;
using Barotrauma.Networking;
namespace Barotrauma
{
class CompositeServerProvider : ServerProvider
{
private readonly ImmutableArray<ServerProvider> providers;
public CompositeServerProvider(params ServerProvider[] providers)
{
this.providers = providers.ToImmutableArray();
}
protected override void RetrieveServersImpl(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted)
{
int providersFinished = 0;
void ackFinishedProvider()
{
providersFinished++;
if (providersFinished == providers.Length)
{
onQueryCompleted();
}
}
providers.ForEach(p => p.RetrieveServers(onServerDataReceived, ackFinishedProvider));
}
public override void Cancel()
=> providers.ForEach(p => p.Cancel());
}
}

View File

@@ -0,0 +1,17 @@
#nullable enable
using System;
using Barotrauma.Networking;
namespace Barotrauma
{
abstract class ServerProvider
{
public void RetrieveServers(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted)
{
Cancel();
RetrieveServersImpl(onServerDataReceived, onQueryCompleted);
}
protected abstract void RetrieveServersImpl(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted);
public abstract void Cancel();
}
}

View File

@@ -0,0 +1,160 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Steam;
namespace Barotrauma
{
sealed class SteamDedicatedServerProvider : ServerProvider
{
public class DataSource : ServerInfo.DataSource
{
public readonly UInt16 QueryPort;
public DataSource(UInt16 queryPort)
{
QueryPort = queryPort;
}
/// Method is invoked via reflection,
/// see <see cref="ServerInfo.DataSource.Parse" />
public new static Option<DataSource> Parse(XElement element)
=> element.TryGetAttributeInt("QueryPort", out var result)
? result switch
{
var invalidPort when invalidPort <= 0 || invalidPort > UInt16.MaxValue => Option<DataSource>.None(),
var queryPort => Option<DataSource>.Some(new DataSource((UInt16)queryPort))
}
: Option<DataSource>.None();
public override void Write(XElement element) => element.SetAttributeValue("QueryPort", QueryPort);
}
private static Option<ServerInfo> InfoFromListEntry(Steamworks.Data.ServerInfo entry) =>
entry.Name.IsNullOrEmpty()
? Option<ServerInfo>.None()
: Option<ServerInfo>.Some(new ServerInfo(new LidgrenEndpoint(entry.Address, entry.ConnectionPort))
{
ServerName = entry.Name,
HasPassword = entry.Passworded,
PlayerCount = entry.Players,
MaxPlayers = entry.MaxPlayers,
MetadataSource = Option<ServerInfo.DataSource>.Some(new DataSource((UInt16)entry.QueryPort))
});
private static void HandleResponsiveServer(Steamworks.Data.ServerInfo entry, Action<ServerInfo> onServerDataReceived)
{
TaskPool.Add($"QueryServerRules (GetServers, {entry.Name}, {entry.Address})", entry.QueryRulesAsync(),
t =>
{
if (t.Status == TaskStatus.Faulted)
{
TaskPool.PrintTaskExceptions(t, $"Failed to retrieve rules for {entry.Name}");
return;
}
if (!t.TryGetResult(out Dictionary<string, string> rules)) { return; }
if (rules is null) { return; }
if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; }
serverInfo.UpdateInfo(key =>
{
if (rules.TryGetValue(key, out var val)) { return val; }
return null;
});
serverInfo.Checked = true; //rules != null;
onServerDataReceived(serverInfo);
});
}
private static void HandleUnresponsiveServer(Steamworks.Data.ServerInfo entry, Action<ServerInfo> onServerDataReceived)
{
//TODO: do we still want to list unresponsive servers?
if (!InfoFromListEntry(entry).TryUnwrap(out var serverInfo)) { return; }
onServerDataReceived(serverInfo);
}
private Steamworks.ServerList.Internet? serverQuery;
private CoroutineHandle? queryCoroutine;
protected override void RetrieveServersImpl(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted)
{
if (!SteamManager.IsInitialized)
{
onQueryCompleted();
return;
}
// All lambdas in here must only capture this call's
// query, not the provider's latest query
var selfServerQuery = new Steamworks.ServerList.Internet();
serverQuery = selfServerQuery;
ConcurrentQueue<Steamworks.Data.ServerInfo> responsiveServers =
new ConcurrentQueue<Steamworks.Data.ServerInfo>();
ConcurrentQueue<Steamworks.Data.ServerInfo> unresponsiveServers =
new ConcurrentQueue<Steamworks.Data.ServerInfo>();
selfServerQuery.OnResponsiveServer = responsiveServers.Enqueue;
selfServerQuery.OnUnresponsiveServer = unresponsiveServers.Enqueue;
void dequeue(int? limit = null)
{
for (int i = 0; (!limit.HasValue || i < limit) && responsiveServers.TryDequeue(out var serverInfo); i++)
{
HandleResponsiveServer(serverInfo, onServerDataReceived);
}
for (int i = 0; (!limit.HasValue || i < limit) && unresponsiveServers.TryDequeue(out var serverInfo); i++)
{
HandleUnresponsiveServer(serverInfo, onServerDataReceived);
}
}
IEnumerable<CoroutineStatus> dequeueCoroutine()
{
while (true)
{
dequeue(limit: 20);
yield return new WaitForSeconds(0.1f, ignorePause: true);
}
}
var selfQueryCoroutine = CoroutineManager.StartCoroutine(dequeueCoroutine(),
$"{nameof(SteamDedicatedServerProvider)}.{nameof(RetrieveServers)}.{nameof(dequeueCoroutine)}");
queryCoroutine = selfQueryCoroutine;
TaskPool.Add("RunServerQuery", selfServerQuery.RunQueryAsync(timeoutSeconds: 30f),
t =>
{
try
{
// Clear the callbacks because it's too late now, we want to get this over with
selfServerQuery.OnResponsiveServer = null;
selfServerQuery.OnUnresponsiveServer = null;
CoroutineManager.StopCoroutines(selfQueryCoroutine);
dequeue();
if (t.Status == TaskStatus.Faulted) { TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers"); }
selfServerQuery.Dispose();
}
finally
{
onQueryCompleted();
}
});
}
public override void Cancel()
{
if (queryCoroutine != null) { CoroutineManager.StopCoroutines(queryCoroutine); }
serverQuery?.Dispose();
serverQuery = null;
}
}
}

View File

@@ -0,0 +1,107 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Xml.Linq;
using Barotrauma.Networking;
using Barotrauma.Steam;
namespace Barotrauma
{
sealed class SteamP2PServerProvider : ServerProvider
{
public class DataSource : ServerInfo.DataSource
{
public readonly Steamworks.Data.Lobby Lobby;
public override void Write(XElement element) { /* do nothing */ }
public DataSource(Steamworks.Data.Lobby lobby)
{
Lobby = lobby;
}
}
private object? queryRef = null;
protected override void RetrieveServersImpl(Action<ServerInfo> onServerDataReceived, Action onQueryCompleted)
{
if (!SteamManager.IsInitialized)
{
onQueryCompleted();
return;
}
// All lambdas and local methods in here must only capture
// this call's query, not the provider's latest query
var selfQueryRef = new object();
queryRef = selfQueryRef;
Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery()
.FilterDistanceWorldwide()
.WithMaxResults(50);
// Steamworks is unable to retrieve more than 50 lobbies per request
// (see https://partner.steamgames.com/doc/features/multiplayer/matchmaking#3)
// To work around this, we'll make up to 10 requests, asking to ignore
// all previous results in each subsequent request.
#warning TODO: do something less horrible here?
int requestCount = 0;
HashSet<SteamId> retrieved = new HashSet<SteamId>();
void startQuery()
{
if (requestCount >= 10) { return; }
requestCount++;
TaskPool.Add($"LobbyQuery.RequestAsync ({requestCount})", lobbyQuery.RequestAsync(), onRequestComplete);
}
void onRequestComplete(Task t)
{
// If queryRef != selfQueryRef, this query was cancelled
if (!ReferenceEquals(selfQueryRef, queryRef)) { return; }
if (!t.TryGetResult(out Steamworks.Data.Lobby[] lobbies)
|| lobbies is null
|| lobbies.Length == 0)
{
onQueryCompleted();
return;
}
foreach (var lobby in lobbies)
{
string lobbyOwnerStr = lobby.GetData("lobbyowner");
lobbyQuery = lobbyQuery.WithoutKeyValue("lobbyowner", lobbyOwnerStr);
string serverName = lobby.GetData("name");
if (string.IsNullOrEmpty(serverName)) { continue; }
var ownerId = SteamId.Parse(lobbyOwnerStr);
if (!ownerId.TryUnwrap(out var lobbyOwnerId)) { continue; }
if (retrieved.Contains(lobbyOwnerId)) { continue; }
retrieved.Add(lobbyOwnerId);
var serverInfo = new ServerInfo(new SteamP2PEndpoint(lobbyOwnerId))
{
ServerName = serverName,
MetadataSource = Option<ServerInfo.DataSource>.Some(new DataSource(lobby))
};
serverInfo.UpdateInfo(key => lobby.GetData(key));
serverInfo.Checked = true;
onServerDataReceived(serverInfo);
}
startQuery();
}
startQuery();
}
public override void Cancel()
{
queryRef = null;
}
}
}

View File

@@ -125,8 +125,6 @@ namespace Barotrauma.Networking
public void ClientRead(IReadMessage incMsg)
{
cachedServerListInfo = null;
NetFlags requiredFlags = (NetFlags)incMsg.ReadByte();
if (requiredFlags.HasFlag(NetFlags.Name))
@@ -146,7 +144,6 @@ namespace Barotrauma.Networking
AllowFileTransfers = incMsg.ReadBoolean();
incMsg.ReadPadBits();
TickRate = incMsg.ReadRangedInteger(1, 60);
GameMain.NetworkMember.TickRate = TickRate;
if (requiredFlags.HasFlag(NetFlags.Properties))
{
@@ -241,7 +238,8 @@ namespace Barotrauma.Networking
outMsg.WriteSingle(levelDifficulty ?? -1000.0f);
outMsg.WriteBoolean(useRespawnShuttle ?? UseRespawnShuttle);
outMsg.WriteBoolean(useRespawnShuttle != null);
outMsg.WriteBoolean(useRespawnShuttle ?? false);
outMsg.WriteBoolean(autoRestart != null);
outMsg.WriteBoolean(autoRestart ?? false);
@@ -1057,15 +1055,7 @@ namespace Barotrauma.Networking
}
settingsFrame = null;
}
return false;
}
private ServerInfo cachedServerListInfo = null;
public ServerInfo GetServerListInfo()
{
cachedServerListInfo ??= GameMain.ServerListScreen.UpdateServerInfoWithServerSettings(GameMain.Client.ClientPeer.ServerConnection, this);
return cachedServerListInfo;
}
}
}

View File

@@ -3,6 +3,7 @@ using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
using Barotrauma.Extensions;
using System;
namespace Barotrauma
{
@@ -111,6 +112,23 @@ namespace Barotrauma
}
}
public void ResetVotes(IEnumerable<Client> connectedClients)
{
foreach (Client client in connectedClients)
{
client.ResetVotes();
}
foreach (VoteType voteType in Enum.GetValues(typeof(VoteType)))
{
SetVoteCountYes(voteType, 0);
SetVoteCountNo(voteType, 0);
SetVoteCountMax(voteType, 0);
}
UpdateVoteTexts(connectedClients, VoteType.Mode);
UpdateVoteTexts(connectedClients, VoteType.Sub);
}
public void ClientWrite(IWriteMessage msg, VoteType voteType, object data)
{
msg.WriteByte((byte)voteType);

View File

@@ -67,6 +67,8 @@ namespace Barotrauma
public static readonly Queue<ulong> WorkshopItemsToUpdate = new Queue<ulong>();
private readonly GUIListBox tutorialList;
#region Creation
public MainMenuScreen(GameMain game)
{
@@ -429,26 +431,19 @@ namespace Barotrauma
menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing });
//PLACEHOLDER
var tutorialList = new GUIListBox(
tutorialList = new GUIListBox(
new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) })
{
PlaySoundOnSelect = true,
};
foreach (var tutorialPrefab in TutorialPrefab.Prefabs.OrderBy(p => p.Order))
{
var tutorial = new Tutorial(tutorialPrefab);
var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.DisplayName, textAlignment: Alignment.Center, font: GUIStyle.LargeFont)
{
TextColor = GUIStyle.Green,
UserData = tutorial
};
}
tutorialList.OnSelected += (component, obj) =>
{
(obj as Tutorial)?.Start();
return true;
};
CreateTutorialButtons();
this.game = game;
menuTabs[Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null)
@@ -463,7 +458,28 @@ namespace Barotrauma
var creditsContainer = new GUIFrame(new RectTransform(new Vector2(0.75f, 1.5f), menuTabs[Tab.Credits].RectTransform, Anchor.CenterRight), style: "OuterGlow", color: Color.Black * 0.8f);
creditsPlayer = new CreditsPlayer(new RectTransform(Vector2.One, creditsContainer.RectTransform), "Content/Texts/Credits.xml");
}
#endregion
private void CreateTutorialButtons()
{
foreach (var tutorialPrefab in TutorialPrefab.Prefabs.OrderBy(p => p.Order))
{
var tutorial = new Tutorial(tutorialPrefab);
var tutorialText = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.15f), tutorialList.Content.RectTransform), tutorial.DisplayName, textAlignment: Alignment.Center, font: GUIStyle.LargeFont)
{
TextColor = GUIStyle.Green,
UserData = tutorial
};
}
}
public static void UpdateInstanceTutorialButtons()
{
if (GameMain.MainMenuScreen is not MainMenuScreen menuScreen) { return; }
menuScreen.tutorialList.ClearChildren();
menuScreen.CreateTutorialButtons();
}
#endregion
#region Selection
public override void Select()
@@ -1117,7 +1133,7 @@ namespace Barotrauma
var playstyleContainer = new GUIFrame(new RectTransform(new Vector2(1.35f, 0.1f), parent.RectTransform), style: null, color: Color.Black);
playstyleBanner = new GUIImage(new RectTransform(new Vector2(1.0f, 0.1f), playstyleContainer.RectTransform),
ServerListScreen.PlayStyleBanners[0], scaleToFit: true)
GUIStyle.GetComponentStyle($"PlayStyleBanner.{PlayStyle.Serious}").GetSprite(GUIComponent.ComponentState.None), scaleToFit: true)
{
UserData = PlayStyle.Serious
};
@@ -1336,12 +1352,15 @@ namespace Barotrauma
private void SetServerPlayStyle(PlayStyle playStyle)
{
playstyleBanner.Sprite = ServerListScreen.PlayStyleBanners[(int)playStyle];
playstyleBanner.Sprite = GUIStyle
.GetComponentStyle($"PlayStyleBanner.{playStyle}")
.GetSprite(GUIComponent.ComponentState.None);
playstyleBanner.UserData = playStyle;
var nameText = playstyleBanner.GetChild<GUITextBlock>();
nameText.Text = TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag." + playStyle));
nameText.Color = ServerListScreen.PlayStyleColors[(int)playStyle];
nameText.Color = playstyleBanner.Sprite
.SourceElement.GetAttributeColor("BannerColor") ?? Color.White;
nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint();
playstyleDescription.Text = TextManager.Get("servertagdescription." + playStyle);

View File

@@ -92,7 +92,7 @@ namespace Barotrauma
var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages
.Where(sp => sp.ContentPackage is null).ToArray();
if (!missingPackages.Any())
if (!missingPackages.Any(p => p.IsMandatory))
{
if (!GameMain.Client.IsServerOwner)
{
@@ -106,7 +106,7 @@ namespace Barotrauma
.Select(p => p.RegularPackage)
.OfType<RegularPackage>().ToList();
//keep enabled client-side-only mods enabled
regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent));
regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent && !regularPackages.Contains(p)));
ContentPackageManager.EnabledPackages.SetRegular(regularPackages);
}
GameMain.NetLobbyScreen.Select();
@@ -177,11 +177,11 @@ namespace Barotrauma
});
buttonContainerSpacing(0.1f);
var missingIds = missingPackages.Where(
mp => mp.WorkshopId != 0
&& ContentPackageManager.WorkshopPackages.All(wp
=> wp.SteamWorkshopId != mp.WorkshopId))
.Select(mp => mp.WorkshopId)
var missingIds = missingPackages
.Where(p => p.IsMandatory)
.Select(mp => ContentPackageId.Parse(mp.UgcId))
.NotNone()
.Where(id => ContentPackageManager.WorkshopPackages.All(wp => !wp.UgcId.Equals(id)))
.ToArray();
if (missingIds.Any() && SteamManager.IsInitialized)
{
@@ -191,7 +191,7 @@ namespace Barotrauma
{
if (GameMain.Client != null)
{
BulkDownloader.SubscribeToServerMods(missingIds,
BulkDownloader.SubscribeToServerMods(missingIds.OfType<SteamWorkshopId>().Select(id => id.Value),
new ConnectCommand(
serverName: GameMain.Client.ServerName,
endpoint: GameMain.Client.ClientPeer.ServerEndpoint));
@@ -202,7 +202,7 @@ namespace Barotrauma
buttonContainerSpacing(0.15f);
}
foreach (var p in missingPackages)
foreach (var p in missingPackages.Where(p => p.IsMandatory))
{
pendingDownloads.Enqueue(p);
@@ -294,26 +294,50 @@ namespace Barotrauma
?? serverPackages.FirstOrDefault(p => p.CorePackage != null)
?.CorePackage
?? throw new Exception($"Failed to find core package to enable");
List<RegularPackage> regularPackages
= serverPackages.Where(p => p.CorePackage is null)
.Select(p =>
p.RegularPackage
?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash))
?? throw new Exception($"Could not find regular package \"{p.Name}\""))
.Cast<RegularPackage>()
.ToList();
List<RegularPackage> regularPackages = new List<RegularPackage>();
foreach (var p in serverPackages)
{
if (p.CorePackage != null) { continue; }
RegularPackage? matchingPackage =
p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage;
if (matchingPackage is null)
{
if (!p.IsMandatory)
{
//we don't need to care about missing non-mandatory (= submarine) mods
continue;
}
else
{
throw new Exception($"Could not find regular package \"{p.Name}\"");
}
}
regularPackages.Add(matchingPackage);
}
foreach (var regularPackage in regularPackages)
{
DebugConsole.NewMessage($"Enabling \"{regularPackage.Name}\" ({regularPackage.Dir})", Color.Lime);
}
//keep enabled client-side-only mods enabled
regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent));
regularPackages.AddRange(ContentPackageManager.EnabledPackages.Regular.Where(p => !p.HasMultiplayerSyncedContent && !regularPackages.Contains(p)));
ContentPackageManager.EnabledPackages.BackUp();
ContentPackageManager.EnabledPackages.SetCore(corePackage);
ContentPackageManager.EnabledPackages.SetRegular(regularPackages);
//see if any of the packages we enabled contain subs that we were missing previously, and update their paths
foreach (var serverSub in GameMain.Client.ServerSubmarines)
{
if (File.Exists(serverSub.FilePath)) { continue; }
var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == serverSub.Name && s.MD5Hash == serverSub.MD5Hash);
if (matchingSub != null)
{
serverSub.FilePath = matchingSub.FilePath;
}
}
GameMain.NetLobbyScreen.UpdateSubList(GameMain.NetLobbyScreen.SubList, GameMain.Client.ServerSubmarines);
GameMain.NetLobbyScreen.Select();
}
}

View File

@@ -666,7 +666,7 @@ namespace Barotrauma
OnSelected = (tickbox) =>
{
if (GameMain.Client == null) { return true; }
ServerInfo info = GameMain.Client.ServerSettings.GetServerListInfo();
ServerInfo info = GameMain.Client.CreateServerInfoFromSettings();
if (tickbox.Selected)
{
GameMain.ServerListScreen.AddToFavoriteServers(info);
@@ -1432,10 +1432,6 @@ namespace Barotrauma
bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName;
changesPendingText = null;
if (isGameRunning)
{
infoContainer.RectTransform.AbsoluteOffset = new Point(0, (int)(parent.Rect.Height * 0.025f));
}
if (TabMenu.PendingChanges)
{
@@ -1454,7 +1450,6 @@ namespace Barotrauma
{
if (GameMain.Client == null) { return; }
string newName = Client.SanitizeName(tb.Text);
newName = newName.Replace(":", "").Replace(";", "");
if (newName == GameMain.Client.Name) return;
if (string.IsNullOrWhiteSpace(newName))
{
@@ -1782,6 +1777,10 @@ namespace Barotrauma
// Hide spectate tickbox if spectating is not allowed
spectateBox.Visible = allowSpectating;
if (infoContainer != null)
{
infoContainer.RectTransform.RelativeSize = new Vector2(infoContainer.RectTransform.RelativeSize.X, spectateBox.Visible ? 0.92f : 0.97f);
}
}
public void SetAutoRestart(bool enabled, float timer = 0.0f)
@@ -2753,25 +2752,24 @@ namespace Barotrauma
if (GameMain.NetworkMember?.ServerSettings == null) { return; }
PlayStyle playStyle = GameMain.NetworkMember.ServerSettings.PlayStyle;
if ((int)playStyle < 0 ||
(int)playStyle >= ServerListScreen.PlayStyleBanners.Length)
{
return;
}
Sprite sprite = ServerListScreen.PlayStyleBanners[(int)playStyle];
Sprite sprite = GUIStyle
.GetComponentStyle($"PlayStyleBanner.{playStyle}")?
.GetSprite(GUIComponent.ComponentState.None);
if (sprite is null) { return; }
float scale = component.Rect.Width / sprite.size.X;
sprite.Draw(spriteBatch, component.Center, scale: scale);
if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value)
{
var nameText = component.GetChild<GUITextBlock>();
nameText.Text = TextManager.Get("servertag." + playStyle);
nameText.Color = ServerListScreen.PlayStyleColors[(int)playStyle];
nameText.Text = TextManager.Get($"ServerTag.{playStyle}");
nameText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White;
nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint();
prevPlayStyle = playStyle;
component.ToolTip = TextManager.Get("servertagdescription." + playStyle);
component.ToolTip = TextManager.Get($"ServerTagDescription.{playStyle}");
}
publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint();

View File

@@ -0,0 +1,112 @@
using System;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Barotrauma
{
public class PanelAnimator
{
private readonly GUIScissorComponent container;
private readonly GUIFrame leftFrame;
private readonly GUIComponent middleFrame;
private readonly GUIFrame rightFrame;
private readonly GUIButton leftButton;
private readonly GUIButton rightButton;
private float leftAnimState = 1.0f;
private float rightAnimState = 0.0f;
public bool LeftEnabled
{
get => leftButton.Enabled;
set => leftButton.Enabled = value;
}
public bool RightEnabled
{
get => rightButton.Enabled;
set => rightButton.Enabled = value;
}
public bool LeftVisible = true;
public bool RightVisible = false;
public PanelAnimator(RectTransform rectTransform, GUIFrame leftFrame, GUIComponent middleFrame, GUIFrame rightFrame)
{
container = new GUIScissorComponent(rectTransform);
this.leftFrame = leftFrame;
this.middleFrame = middleFrame;
this.rightFrame = rightFrame;
void own(GUIComponent component)
{
component.RectTransform.Parent = container.Content.RectTransform;
component.RectTransform.Anchor = Anchor.TopLeft;
component.RectTransform.Pivot = Pivot.TopLeft;
component.GetAllChildren<GUIDropDown>().ForEach(dd => dd.RefreshListBoxParent());
}
GUIButton makeButton(Action action)
=> new GUIButton(new RectTransform(new Vector2(0.01f, 1.0f), container.Content.RectTransform)
{ MinSize = new Point(20, 0), MaxSize = new Point(int.MaxValue, (int)(150 * GUI.Scale)) },
style: "UIToggleButton")
{
OnClicked = (_, __) =>
{
action();
return false;
}
};
own(leftFrame);
this.leftButton = makeButton(() => LeftVisible = !LeftVisible);
own(middleFrame);
this.rightButton = makeButton(() => RightVisible = !RightVisible);
own(rightFrame);
}
public void Update()
{
if (!LeftEnabled) { LeftVisible = false; }
if (!RightEnabled) { RightVisible = false; }
static void updateState(ref float state, bool visible)
=> state = MathHelper.Lerp(state, visible ? 0.0f : 1.0f, 0.5f);
updateState(ref leftAnimState, LeftVisible);
updateState(ref rightAnimState, RightVisible);
static int width(GUIComponent c)
=> c.RectTransform.NonScaledSize.X;
int height = container.RectTransform.NonScaledSize.Y;
int buttonY = height/2 - leftButton.RectTransform.NonScaledSize.Y/2;
leftFrame.RectTransform.AbsoluteOffset = new Point((int)(-width(leftFrame) * leftAnimState), 0);
leftButton.RectTransform.AbsoluteOffset = leftFrame.RectTransform.AbsoluteOffset
+ new Point(width(leftFrame), buttonY);
leftButton.Children.ForEach(c => c.SpriteEffects = LeftVisible
? SpriteEffects.FlipHorizontally
: SpriteEffects.None);
rightFrame.RectTransform.AbsoluteOffset = new Point((int)(width(container) + width(rightFrame) * (rightAnimState-1f)), 0);
rightButton.RectTransform.AbsoluteOffset = rightFrame.RectTransform.AbsoluteOffset
+ new Point(-width(rightButton), buttonY);
rightButton.Children.ForEach(c => c.SpriteEffects = RightVisible
? SpriteEffects.None
: SpriteEffects.FlipHorizontally);
middleFrame.RectTransform.AbsoluteOffset = new Point(
leftButton.RectTransform.AbsoluteOffset.X + width(leftButton),
0);
middleFrame.RectTransform.NonScaledSize = new Point(
rightButton.RectTransform.AbsoluteOffset.X - middleFrame.RectTransform.AbsoluteOffset.X,
height);
}
}
}

View File

@@ -1547,6 +1547,8 @@ namespace Barotrauma
GUI.ForceMouseOn(null);
if (ImageManager.EditorMode) { GameSettings.SaveCurrentConfig(); }
MapEntityPrefab.Selected = null;
saveFrame = null;
@@ -1797,25 +1799,14 @@ namespace Barotrauma
{
Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player);
void addSubAndSaveModProject(ModProject modProject, string filePath, string packagePath)
static string getExistingFilePath(ContentPackage package, string fileName)
{
filePath = filePath.CleanUpPath();
packagePath = packagePath.CleanUpPath();
string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false);
if (filePath.StartsWith(packageDir))
if (Submarine.MainSub?.Info == null) { return null; }
if (package.Files.Any(f => f.Path == MainSub.Info.FilePath && Path.GetFileName(f.Path.Value) == fileName))
{
filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}";
return MainSub.Info.FilePath;
}
if (!modProject.Files.Any(f => f.Type == subFileType &&
f.Path == filePath))
{
var newFile = ModProject.File.FromPath(filePath, subFileType);
modProject.AddFile(newFile);
}
using var _ = Validation.SkipInDebugBuilds();
modProject.DiscardHashAndInstallTime();
modProject.Save(packagePath);
return null;
}
if (!GameMain.DebugDraw)
@@ -1861,101 +1852,139 @@ namespace Barotrauma
#if !DEBUG
throw new InvalidOperationException("Cannot save to Vanilla package");
#endif
savePath = string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch
{
SubmarineType.Player => "Content/Submarines/{0}",
SubmarineType.Outpost => "Content/Map/Outposts/{0}",
SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore...
SubmarineType.Wreck => "Content/Map/Wrecks/{0}",
SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}",
SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}",
SubmarineType.OutpostModule => "Content/Map/Outposts/{0}",
_ => throw new InvalidOperationException()
}, savePath);
savePath =
getExistingFilePath(packageToSaveTo, savePath) ??
string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch
{
SubmarineType.Player => "Content/Submarines/{0}",
SubmarineType.Outpost => "Content/Map/Outposts/{0}",
SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore...
SubmarineType.Wreck => "Content/Map/Wrecks/{0}",
SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}",
SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}",
SubmarineType.OutpostModule => "Content/Map/Outposts/{0}",
_ => throw new InvalidOperationException()
}, savePath);
modProject.ModVersion = "";
}
else
{
savePath = Path.Combine(packageToSaveTo.Dir, savePath);
string existingFilePath = getExistingFilePath(packageToSaveTo, savePath);
//if we're trying to save a sub that's already included in the package with the same name as before, save directly in the same path
if (existingFilePath != null)
{
savePath = existingFilePath;
}
//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 verification = new GUIMessageBox(TextManager.Get("warning"), TextManager.Get("subeditor.duplicatesubinpackage"),
new LocalizedString[] { TextManager.Get("yes"), TextManager.Get("no") });
verification.Buttons[0].OnClicked = (_, _) =>
{
addSubAndSave(modProject, savePath, fileListPath);
verification.Close();
return true;
};
verification.Buttons[1].OnClicked = verification.Close;
return false;
}
}
}
addSubAndSaveModProject(modProject, savePath, fileListPath);
}
else if (MainSub?.Info?.FilePath != null
&& MainSub.Info.Name != null
&& MainSub.Info.FilePath.StartsWith(ContentPackage.LocalModsDir)
&& MainSub.Info.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))
{
prevSavePath = MainSub.Info.FilePath.CleanUpPath();
ContentPackage contentPackage = GetLocalPackageThatOwnsSub(MainSub.Info);
if (contentPackage == null)
{
throw new InvalidOperationException($"Tried to overwrite a submarine ({name}) that's not in a local package!");
}
ModProject modProject = new ModProject(contentPackage);
packageToSaveTo = contentPackage;
savePath = prevSavePath;
addSubAndSaveModProject(modProject, savePath, contentPackage.Path);
addSubAndSave(modProject, savePath, fileListPath);
}
else
{
savePath = Path.Combine(newLocalModDir, savePath);
ModProject modProject = new ModProject { Name = name };
addSubAndSaveModProject(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName));
}
savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false);
if (MainSub != null)
{
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && MainSub.Info.Type != SubmarineType.OutpostModule)
if (File.Exists(savePath))
{
bool savePreviewImage = true;
using System.IO.MemoryStream imgStream = new System.IO.MemoryStream();
try
{
previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Saving the preview image of the submarine \"{MainSub.Info.Name}\" failed.", e);
savePreviewImage = false;
}
MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null);
new GUIMessageBox(TextManager.Get("warning"), TextManager.GetWithVariable("subeditor.packagealreadyexists", "[name]", name));
return false;
}
else
{
MainSub.TrySaveAs(savePath);
ModProject modProject = new ModProject { Name = name };
addSubAndSave(modProject, savePath, Path.Combine(Path.GetDirectoryName(savePath), ContentPackage.FileListFileName));
}
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
}
MainSub.CheckForErrors();
GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green);
if (savePath.StartsWith(newLocalModDir))
void addSubAndSave(ModProject modProject, string filePath, string packagePath)
{
filePath = filePath.CleanUpPath();
packagePath = packagePath.CleanUpPath();
string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false);
if (filePath.StartsWith(packageDir))
{
ContentPackageManager.LocalPackages.Refresh();
var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir));
if (newPackage is RegularPackage regular)
filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}";
}
if (!modProject.Files.Any(f => f.Type == subFileType &&
f.Path == filePath))
{
var newFile = ModProject.File.FromPath(filePath, subFileType);
modProject.AddFile(newFile);
}
using var _ = Validation.SkipInDebugBuilds();
modProject.DiscardHashAndInstallTime();
modProject.Save(packagePath);
savePath = savePath.CleanUpPathCrossPlatform(correctFilenameCase: false);
if (MainSub != null)
{
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = true;
if (previewImage?.Sprite?.Texture != null && !previewImage.Sprite.Texture.IsDisposed && MainSub.Info.Type != SubmarineType.OutpostModule)
{
ContentPackageManager.EnabledPackages.EnableRegular(regular);
GameSettings.SaveCurrentConfig();
bool savePreviewImage = true;
using System.IO.MemoryStream imgStream = new System.IO.MemoryStream();
try
{
previewImage.Sprite.Texture.SaveAsPng(imgStream, previewImage.Sprite.Texture.Width, previewImage.Sprite.Texture.Height);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Saving the preview image of the submarine \"{MainSub.Info.Name}\" failed.", e);
savePreviewImage = false;
}
MainSub.TrySaveAs(savePath, savePreviewImage ? imgStream : null);
}
}
if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); }
SubmarineInfo.RefreshSavedSub(savePath);
if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); }
MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage;
else
{
MainSub.TrySaveAs(savePath);
}
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
linkedSubBox.ClearChildren();
foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines)
{
if (sub.Type != SubmarineType.Player) { continue; }
if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; }
linkedSubBox.AddItem(sub.Name, sub);
MainSub.CheckForErrors();
GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green);
if (savePath.StartsWith(newLocalModDir))
{
ContentPackageManager.LocalPackages.Refresh();
var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir));
if (newPackage is RegularPackage regular)
{
ContentPackageManager.EnabledPackages.EnableRegular(regular);
GameSettings.SaveCurrentConfig();
}
}
if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); }
SubmarineInfo.RefreshSavedSub(savePath);
if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); }
MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage;
string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder);
linkedSubBox.ClearChildren();
foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines)
{
if (sub.Type != SubmarineType.Player) { continue; }
if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; }
linkedSubBox.AddItem(sub.Name, sub);
}
subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width);
}
subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width);
}
return false;
@@ -2735,40 +2764,31 @@ namespace Barotrauma
new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform),
onUpdate: (f, component) =>
{
bool canCreateNewPackage = true;
foreach (GUIComponent contentChild in packageToSaveInList.Content.Children)
{
contentChild.Visible = !(contentChild.UserData is ContentPackage p)
|| !string.Equals(p.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase);
canCreateNewPackage &= contentChild.Visible;
contentChild.Visible &= !(contentChild.GetChild<GUILayoutGroup>()?.GetChild<GUITextBlock>() is GUITextBlock tb &&
!tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase));
}
if (newPackageListIcon.Style.Identifier != "NewContentPackageIcon" && canCreateNewPackage)
{
GUIStyle.Apply(newPackageListIcon, "NewContentPackageIcon");
newPackageListText.Text = TextManager.Get("CreateNewLocalPackage");
}
if (newPackageListIcon.Style.Identifier != "WorkshopMenu.EditButton" && !canCreateNewPackage)
{
GUIStyle.Apply(newPackageListIcon, "WorkshopMenu.EditButton");
newPackageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", nameBox.Text);
}
});
packageToSaveInList.Select(0);
ContentPackage ownerPkg = null;
if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); }
foreach (var p in ContentPackageManager.LocalPackages)
{
addItemToPackageToSaveList(p.Name, p);
var packageListItem = addItemToPackageToSaveList(p.Name, p);
if (p == ownerPkg)
{
var packageListIcon = packageListItem.GetChild<GUIFrame>();
var packageListText = packageListItem.GetChild<GUITextBlock>();
GUIStyle.Apply(packageListIcon, "WorkshopMenu.EditButton");
packageListText.Text = TextManager.GetWithVariable("UpdateExistingLocalPackage", "[mod]", p.Name);
}
}
if (ownerPkg != null && !string.Equals(ownerPkg.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase))
if (ownerPkg != null)
{
packageToSaveInList.Select(ownerPkg);
packageToSaveInList.ScrollToElement(packageToSaveInList.SelectedComponent);
var element = packageToSaveInList.Content.FindChild(ownerPkg);
element?.RectTransform.SetAsFirstChild();
}
packageToSaveInList.Select(0);
var requiredContentPackagesLayout = new GUILayoutGroup(new RectTransform(Vector2.One,
horizontalArea.RectTransform, Anchor.BottomRight))
@@ -3424,7 +3444,8 @@ namespace Barotrauma
{
if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage)
{
if (publishedWorkshopItemIds.Contains(workshopPackage.SteamWorkshopId))
if (workshopPackage.TryExtractSteamWorkshopId(out var workshopId)
&& publishedWorkshopItemIds.Contains(workshopId.Value))
{
AskLoadPublishedSub(selectedSubInfo, workshopPackage);
}

View File

@@ -380,6 +380,11 @@ namespace Barotrauma
}
LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description");
if (toolTip.IsNullOrEmpty() && entity.GetType() != property.PropertyInfo.DeclaringType)
{
Identifier propertyTagForDerivedClass = $"{entity.GetType().Name}.{property.PropertyInfo.Name}".ToIdentifier();
toolTip = TextManager.Get($"{propertyTagForDerivedClass}.description", $"sp.{propertyTagForDerivedClass}.description");
}
if (toolTip.IsNullOrEmpty())
{
toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description");

View File

@@ -127,15 +127,22 @@ namespace Barotrauma
}
}
public void ReloadTexture(bool updateAllSprites = false) => ReloadTexture(updateAllSprites ? LoadedSprites.Where(s => s.texture == texture).ToList() : new List<Sprite>() { this });
public void ReloadTexture(IEnumerable<Sprite> spritesToUpdate)
public void ReloadTexture()
{
var oldTexture = texture;
texture.Dispose();
texture = TextureLoader.FromFile(FilePath.Value, Compress);
foreach (Sprite sprite in spritesToUpdate)
Identifier pathKey = FullPath.ToIdentifier();
if (textureRefCounts.ContainsKey(pathKey))
{
sprite.texture = texture;
textureRefCounts[pathKey].Texture = texture;
}
foreach (Sprite sprite in LoadedSprites)
{
if (sprite.texture == oldTexture)
{
sprite.texture = texture;
}
}
}

View File

@@ -70,7 +70,9 @@ namespace Barotrauma.Steam
(ContentPackage Package, bool IsUpToDate)[] outOfDatePackages = await Task.WhenAll(determiningTasks);
return (await Task.WhenAll(outOfDatePackages.Where(p => !p.IsUpToDate)
.Select(async p => await SteamManager.Workshop.GetItem(p.Package.SteamWorkshopId))))
.Select(p => p.Package.UgcId)
.OfType<SteamWorkshopId>()
.Select(async id => await SteamManager.Workshop.GetItem(id.Value))))
.Where(p => p.HasValue).Select(p => p ?? default).ToArray();
}

View File

@@ -1,7 +1,5 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -102,7 +100,7 @@ namespace Barotrauma.Steam
currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation)));
currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId)));
currentLobby?.SetData("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.UgcId)));
currentLobby?.SetData("modeselectionmode", serverSettings.ModeSelectionMode.ToString());
currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString());
currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.ToString());
@@ -152,275 +150,5 @@ namespace Barotrauma.Steam
}
});
}
public static bool GetServers(Action<ServerInfo> addToServerList, Action serverQueryFinished)
{
if (!IsInitialized) { return false; }
int doneTasks = 0;
void taskDone()
{
doneTasks++;
if (doneTasks >= 2)
{
serverQueryFinished?.Invoke();
serverQueryFinished = null;
}
}
Steamworks.Dispatch.OnDebugCallback = (callbackType, contents, isServer) =>
{
DebugConsole.NewMessage($"{callbackType}: " + contents, Color.Yellow);
};
TaskPool.Add("LobbyQueryRequest", LobbyQueryRequest(),
(t) =>
{
Steamworks.Dispatch.OnDebugCallback = null;
if (t.Status == TaskStatus.Faulted)
{
TaskPool.PrintTaskExceptions(t, "Failed to retrieve SteamP2P lobbies");
taskDone();
return;
}
var lobbies = ((Task<List<Steamworks.Data.Lobby>>)t).Result;
if (lobbies != null)
{
foreach (var lobby in lobbies)
{
if (string.IsNullOrEmpty(lobby.GetData("name"))) { continue; }
ServerInfo serverInfo = new ServerInfo
{
ServerName = lobby.GetData("name"),
Endpoint = new SteamP2PEndpoint(SteamId.Parse(lobby.GetData("lobbyowner")).Fallback(default(SteamId))),
LobbyID = lobby.Id,
RespondedToSteamQuery = true
};
bool.TryParse(lobby.GetData("haspassword"), out serverInfo.HasPassword);
serverInfo.PlayerCount = int.TryParse(lobby.GetData("playercount"), out int playerCount) ? playerCount : 0;
serverInfo.MaxPlayers = int.TryParse(lobby.GetData("maxplayernum"), out int maxPlayers) ? maxPlayers : 1;
AssignLobbyDataToServerInfo(lobby, serverInfo);
addToServerList(serverInfo);
}
}
taskDone();
});
Steamworks.ServerList.Internet serverQuery = new Steamworks.ServerList.Internet();
void onServer(Steamworks.Data.ServerInfo info, bool responsive)
{
if (string.IsNullOrEmpty(info.Name)) { return; }
ServerInfo serverInfo = new ServerInfo
{
ServerName = info.Name,
HasPassword = info.Passworded,
Endpoint = new LidgrenEndpoint(info.Address, info.ConnectionPort),
PlayerCount = info.Players,
MaxPlayers = info.MaxPlayers,
RespondedToSteamQuery = responsive
};
if (responsive)
{
TaskPool.Add($"QueryServerRules (GetServers, {info.Name}, {info.Address})", info.QueryRulesAsync(),
(t) =>
{
if (t.Status == TaskStatus.Faulted)
{
TaskPool.PrintTaskExceptions(t, "Failed to retrieve rules for " + info.Name);
return;
}
var rules = ((Task<Dictionary<string, string>>)t).Result;
AssignServerRulesToServerInfo(rules, serverInfo);
CrossThread.RequestExecutionOnMainThread(() =>
{
addToServerList(serverInfo);
});
});
}
else
{
CrossThread.RequestExecutionOnMainThread(() =>
{
addToServerList(serverInfo);
});
}
}
serverQuery.OnResponsiveServer += (info) => onServer(info, true);
serverQuery.OnUnresponsiveServer += (info) => onServer(info, false);
TaskPool.Add("RunServerQuery", serverQuery.RunQueryAsync(),
(t) =>
{
serverQuery.Dispose();
taskDone();
if (t.Status == TaskStatus.Faulted)
{
TaskPool.PrintTaskExceptions(t, "Failed to retrieve servers");
return;
}
});
return true;
}
public static async Task<List<Steamworks.Data.Lobby>> LobbyQueryRequest()
{
List<Steamworks.Data.Lobby> allLobbies = new List<Steamworks.Data.Lobby>();
Steamworks.Data.LobbyQuery lobbyQuery = Steamworks.SteamMatchmaking.CreateLobbyQuery()
.FilterDistanceWorldwide()
.WithMaxResults(50);
//steamworks seems to unable to retrieve more than 50
//lobbies per request; to work around this, we'll make
//up to 10 requests, asking to ignore all previous results
//in each subsequent request
for (int i = 0; i < 10; i++)
{
Steamworks.Data.Lobby[] lobbies = await lobbyQuery.RequestAsync();
if (lobbies == null) { break; }
foreach (var l in lobbies)
{
lobbyQuery = lobbyQuery
.WithoutKeyValue("lobbyowner", l.GetData("lobbyowner"));
}
allLobbies.AddRange(lobbies);
}
//make sure all returned lobbies are distinct, don't want any duplicates here
return allLobbies.Select(l => l.Id).Distinct().Select(i => allLobbies.Find(l => l.Id == i)).ToList();
}
public static void AssignLobbyDataToServerInfo(Steamworks.Data.Lobby lobby, ServerInfo serverInfo)
{
serverInfo.OwnerVerified = true;
serverInfo.ServerMessage = lobby.GetData("message");
serverInfo.GameVersion = lobby.GetData("version");
serverInfo.ContentPackageNames.AddRange(lobby.GetData("contentpackage").Split(','));
serverInfo.ContentPackageHashes.AddRange(lobby.GetData("contentpackagehash").Split(','));
string workshopIdData = lobby.GetData("contentpackageid");
if (!string.IsNullOrEmpty(workshopIdData))
{
serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(workshopIdData));
}
else
{
string[] workshopUrls = lobby.GetData("contentpackageurl").Split(',');
serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls));
}
if (Enum.TryParse(lobby.GetData("modeselectionmode"), out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; }
if (Enum.TryParse(lobby.GetData("subselectionmode"), out selectionMode)) { serverInfo.SubSelectionMode = selectionMode; }
serverInfo.AllowSpectating = getLobbyBool("allowspectating");
serverInfo.AllowRespawn = getLobbyBool("allowrespawn");
serverInfo.VoipEnabled = getLobbyBool("voicechatenabled");
serverInfo.KarmaEnabled = getLobbyBool("karmaenabled");
serverInfo.FriendlyFireEnabled = getLobbyBool("friendlyfireenabled");
if (Enum.TryParse(lobby.GetData("traitors"), out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; }
serverInfo.GameStarted = lobby.GetData("gamestarted") == "True";
serverInfo.GameMode = (lobby.GetData("gamemode") ?? "").ToIdentifier();
if (Enum.TryParse(lobby.GetData("playstyle"), out PlayStyle playStyle)) serverInfo.PlayStyle = playStyle;
if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count ||
serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count)
{
//invalid contentpackage info
serverInfo.ContentPackageNames.Clear();
serverInfo.ContentPackageHashes.Clear();
serverInfo.ContentPackageWorkshopIds.Clear();
}
string pingLocation = lobby.GetData("pinglocation");
if (!string.IsNullOrEmpty(pingLocation))
{
serverInfo.PingLocation = Steamworks.Data.NetPingLocation.TryParseFromString(pingLocation);
}
bool? getLobbyBool(string key)
{
string data = lobby.GetData(key);
if (string.IsNullOrEmpty(data)) { return null; }
return data == "True" || data == "true";
}
}
public static void AssignServerRulesToServerInfo(Dictionary<string, string> rules, ServerInfo serverInfo)
{
serverInfo.OwnerVerified = true;
if (rules == null) { return; }
if (rules.ContainsKey("message")) { serverInfo.ServerMessage = rules["message"]; }
if (rules.ContainsKey("version")) { serverInfo.GameVersion = rules["version"]; }
if (rules.ContainsKey("playercount"))
{
if (int.TryParse(rules["playercount"], out int playerCount)) { serverInfo.PlayerCount = playerCount; }
}
serverInfo.ContentPackageNames.Clear();
serverInfo.ContentPackageHashes.Clear();
serverInfo.ContentPackageWorkshopIds.Clear();
if (rules.ContainsKey("contentpackage")) { serverInfo.ContentPackageNames.AddRange(rules["contentpackage"].Split(',')); }
if (rules.ContainsKey("contentpackagehash")) { serverInfo.ContentPackageHashes.AddRange(rules["contentpackagehash"].Split(',')); }
if (rules.ContainsKey("contentpackageid"))
{
serverInfo.ContentPackageWorkshopIds.AddRange(ParseWorkshopIds(rules["contentpackageid"]));
}
else if (rules.ContainsKey("contentpackageurl"))
{
string[] workshopUrls = rules["contentpackageurl"].Split(',');
serverInfo.ContentPackageWorkshopIds.AddRange(WorkshopUrlsToIds(workshopUrls));
}
if (rules.ContainsKey("modeselectionmode"))
{
if (Enum.TryParse(rules["modeselectionmode"], out SelectionMode selectionMode)) { serverInfo.ModeSelectionMode = selectionMode; }
}
if (rules.ContainsKey("subselectionmode"))
{
if (Enum.TryParse(rules["subselectionmode"], out SelectionMode selectionMode)) { serverInfo.SubSelectionMode = selectionMode; }
}
if (rules.ContainsKey("allowspectating")) { serverInfo.AllowSpectating = rules["allowspectating"] == "True"; }
if (rules.ContainsKey("allowrespawn")) { serverInfo.AllowRespawn = rules["allowrespawn"] == "True"; }
if (rules.ContainsKey("voicechatenabled")) { serverInfo.VoipEnabled = rules["voicechatenabled"] == "True"; }
if (rules.ContainsKey("friendlyfireenabled")) { serverInfo.FriendlyFireEnabled = rules["friendlyfireenabled"] == "True"; }
if (rules.ContainsKey("karmaenabled")) { serverInfo.KarmaEnabled = rules["karmaenabled"] == "True"; }
if (rules.ContainsKey("traitors"))
{
if (Enum.TryParse(rules["traitors"], out YesNoMaybe traitorsEnabled)) { serverInfo.TraitorsEnabled = traitorsEnabled; }
}
if (rules.ContainsKey("gamestarted")) { serverInfo.GameStarted = rules["gamestarted"] == "True"; }
if (rules.ContainsKey("gamemode"))
{
serverInfo.GameMode = rules["gamemode"].ToIdentifier();
}
if (rules.ContainsKey("playstyle") && Enum.TryParse(rules["playstyle"], out PlayStyle playStyle))
{
serverInfo.PlayStyle = playStyle;
}
if (serverInfo.ContentPackageNames.Count != serverInfo.ContentPackageHashes.Count ||
serverInfo.ContentPackageHashes.Count != serverInfo.ContentPackageWorkshopIds.Count)
{
//invalid contentpackage info
serverInfo.ContentPackageNames.Clear();
serverInfo.ContentPackageHashes.Clear();
serverInfo.ContentPackageWorkshopIds.Clear();
}
}
}
}

View File

@@ -196,7 +196,7 @@ namespace Barotrauma.Steam
throw new Exception("Expected Workshop package");
}
if (contentPackage.SteamWorkshopId == 0)
if (!contentPackage.UgcId.TryUnwrap(out var ugcId) || !(ugcId is SteamWorkshopId workshopId))
{
throw new Exception($"Steam Workshop ID not set for {contentPackage.Name}");
}
@@ -210,7 +210,7 @@ namespace Barotrauma.Steam
string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}";
if (File.Exists(newPath) || Directory.Exists(newPath))
{
newPath += $"_{contentPackage.SteamWorkshopId}";
newPath += $"_{workshopId.Value}";
}
if (File.Exists(newPath) || Directory.Exists(newPath))
@@ -226,7 +226,7 @@ namespace Barotrauma.Steam
RefreshLocalMods();
return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == contentPackage.SteamWorkshopId);
return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.UgcId == contentPackage.UgcId);
}
private struct InstallWaiter
@@ -266,7 +266,10 @@ namespace Barotrauma.Steam
{
NukeDownload(workshopItem);
var toUninstall
= ContentPackageManager.WorkshopPackages.Where(p => p.SteamWorkshopId == workshopItem.Id)
= ContentPackageManager.WorkshopPackages.Where(p =>
p.UgcId.TryUnwrap(out var ugcId)
&& ugcId is SteamWorkshopId workshopId
&& workshopId.Value == workshopItem.Id)
.ToHashSet();
toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d));
CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.WorkshopPackages.Refresh());
@@ -296,7 +299,10 @@ namespace Barotrauma.Steam
return;
}
else if (CanBeInstalled(id)
&& !ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == id)
&& !ContentPackageManager.WorkshopPackages.Any(p =>
p.UgcId.TryUnwrap(out var ugcId)
&& ugcId is SteamWorkshopId workshopId
&& workshopId.Value == id)
&& !InstallTaskCounter.IsInstalling(id))
{
TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id));

View File

@@ -35,7 +35,12 @@ namespace Barotrauma.Steam
memSubscribedModCount = numSubscribedMods;
var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet();
var installedIds = ContentPackageManager.WorkshopPackages.Select(p => p.SteamWorkshopId).ToHashSet();
var installedIds = ContentPackageManager.WorkshopPackages
.Select(p => p.UgcId)
.NotNone()
.OfType<SteamWorkshopId>()
.Select(id => id.Value)
.ToHashSet();
foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2)))
{
Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id);
@@ -514,7 +519,9 @@ namespace Barotrauma.Steam
private void PrepareToShowModInfo(ContentPackage mod)
{
TaskPool.Add($"PrepareToShow{mod.SteamWorkshopId}Info", SteamManager.Workshop.GetItem(mod.SteamWorkshopId),
if (!mod.UgcId.TryUnwrap(out var ugcId)
|| !(ugcId is SteamWorkshopId workshopId)) { return; }
TaskPool.Add($"PrepareToShow{mod.UgcId}Info", SteamManager.Workshop.GetItem(workshopId.Value),
t =>
{
if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; }
@@ -592,7 +599,12 @@ namespace Barotrauma.Steam
isEnabled: true,
onSelected: () =>
{
TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(selectedMods.Select(m => SteamManager.Workshop.GetItem(m.SteamWorkshopId))),
var workshopIds = selectedMods
.Select(m => m.UgcId)
.NotNone()
.OfType<SteamWorkshopId>()
.Select(id => id.Value);
TaskPool.AddIfNotFound($"UnsubFromSelected", Task.WhenAll(workshopIds.Select(SteamManager.Workshop.GetItem)),
t =>
{
if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; }
@@ -672,7 +684,7 @@ namespace Barotrauma.Steam
infoButton.Enabled = false;
}
TaskPool.AddIfNotFound(
$"DetermineUpdateRequired{mod.SteamWorkshopId}",
$"DetermineUpdateRequired{mod.UgcId}",
mod.IsUpToDate(),
t =>
{
@@ -725,6 +737,8 @@ namespace Barotrauma.Steam
{
var mod = child.UserData as RegularPackage;
if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; }
if (!mod.UgcId.TryUnwrap(out var ugcId)) { continue; }
if (!(ugcId is SteamWorkshopId workshopId)) { continue; }
var btn = child.GetChild<GUILayoutGroup>()?.GetAllChildren<GUIButton>().Last();
if (btn is null) { continue; }
@@ -732,11 +746,11 @@ namespace Barotrauma.Steam
btn.ApplyStyle(
GUIStyle.GetComponentStyle(
ids.Contains(mod.SteamWorkshopId)
ids.Contains(workshopId.Value)
? "WorkshopMenu.PublishedIcon"
: "WorkshopMenu.DownloadedIcon"));
btn.ToolTip = TextManager.Get(
ids.Contains(mod.SteamWorkshopId)
ids.Contains(workshopId.Value)
? "PublishedWorkshopMod"
: "DownloadedWorkshopMod");
btn.HoverCursor = CursorState.Default;

View File

@@ -226,7 +226,7 @@ namespace Barotrauma.Steam
(Steamworks.Ugc.Item WorkshopItem, ContentPackage? LocalPackage)[] publishedItems = workshopItems
.Select(item => (item,
(ContentPackage?)ContentPackageManager.LocalPackages.FirstOrDefault(p
=> p.SteamWorkshopId != 0 && p.SteamWorkshopId == item.Id)))
=> p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == item.Id)))
//Sort the pairs by last local edit time if available
.OrderBy(t => t.Item2 == null)
.ThenByDescending(t => t.Item2 is { } p ? getEditTime(p) : t.Item1.LatestUpdateTime)
@@ -241,7 +241,9 @@ namespace Barotrauma.Steam
//Get mods that haven't been published and add them to the list
var unpublishedMods = ContentPackageManager.LocalPackages
.Where(p => p.SteamWorkshopId == 0 || !publishedItems.Any(item => item.WorkshopItem.Id == p.SteamWorkshopId))
.Where(p =>
!p.TryExtractSteamWorkshopId(out var workshopId)
|| !publishedItems.Any(item => item.WorkshopItem.Id == workshopId.Value))
.OrderByDescending(getEditTime).ToArray();
if (unpublishedMods.Any())
@@ -556,7 +558,9 @@ namespace Barotrauma.Steam
taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc;
var contentPackage
= ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id);
= ContentPackageManager.WorkshopPackages.FirstOrDefault(p =>
p.TryExtractSteamWorkshopId(out var workshopId)
&& workshopId.Value == workshopItem.Id);
var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform));
@@ -619,7 +623,7 @@ namespace Barotrauma.Steam
if (contentPackage != null)
{
TaskPool.AddIfNotFound(
$"DetermineUpdateRequired{contentPackage.SteamWorkshopId}",
$"DetermineUpdateRequired{contentPackage.UgcId}",
contentPackage.IsUpToDate(),
t =>
{
@@ -652,7 +656,9 @@ namespace Barotrauma.Steam
if (contentPackage != null
&& !ContentPackageManager.WorkshopPackages.Contains(contentPackage)
&& ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id))
&& ContentPackageManager.WorkshopPackages.Any(p =>
p.TryExtractSteamWorkshopId(out var workshopId)
&& workshopId.Value == workshopItem.Id))
{
updateButton.Visible = false;
}

View File

@@ -6,6 +6,7 @@ using System.Linq;
using System.Xml.Linq;
using Barotrauma.IO;
using Barotrauma.Extensions;
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
namespace Barotrauma
@@ -49,21 +50,18 @@ namespace Barotrauma
case ModType.Workshop:
{
var id = element.GetAttributeUInt64("id", 0);
var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == id);
if (id != 0 && pkg != null)
{
addPkg(pkg);
}
if (id == 0) { continue; }
var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p =>
p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == id);
if (pkg != null) { addPkg(pkg); }
}
break;
case ModType.Local:
{
var name = element.GetAttributeString("name", "");
if (name.IsNullOrEmpty()) { continue; }
var pkg = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.NameMatches(name));
if (!name.IsNullOrEmpty() && pkg != null)
{
addPkg(pkg);
}
if (pkg != null) { addPkg(pkg); }
}
break;
}
@@ -115,7 +113,7 @@ namespace Barotrauma
{
case ModType.Workshop:
pkgElem.SetAttributeValue("name", pkg.Name);
pkgElem.SetAttributeValue("id", pkg.SteamWorkshopId.ToString());
pkgElem.SetAttributeValue("id", pkg.UgcId.ToString());
break;
case ModType.Local:
pkgElem.SetAttributeValue("name", pkg.Name);

View File

@@ -95,6 +95,9 @@ namespace Barotrauma.Steam
SelectTab(Tab.Publish);
}
private static bool PackageMatchesItem(ContentPackage p, Steamworks.Ugc.Item workshopItem)
=> p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == workshopItem.Id;
private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame)
{
ContentPackageManager.LocalPackages.Refresh();
@@ -105,18 +108,19 @@ namespace Barotrauma.Steam
childAnchor: Anchor.TopCenter);
Steamworks.Ugc.Item workshopItem = itemOrPackage.TryGet(out Steamworks.Ugc.Item item) ? item : default;
ContentPackage? localPackage = itemOrPackage.TryGet(out ContentPackage package)
? package
: ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id);
: ContentPackageManager.LocalPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem));
ContentPackage? workshopPackage
= ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id);
= ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem));
if (localPackage is null)
{
new GUIFrame(new RectTransform((1.0f, 0.15f), mainLayout.RectTransform), style: null);
//Local copy does not exist; check for Workshop copy
bool workshopCopyExists =
ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id);
ContentPackageManager.WorkshopPackages.Any(p => PackageMatchesItem(p, workshopItem));
new GUITextBlock(new RectTransform((0.7f, 0.4f), mainLayout.RectTransform),
TextManager.Get(workshopCopyExists ? "LocalCopyRequired" : "ItemInstallRequired"),
@@ -403,7 +407,7 @@ namespace Barotrauma.Steam
private IEnumerable<CoroutineStatus> CreateLocalCopy(GUITextBlock currentStepText, Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame)
{
ContentPackage? workshopCopy =
ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id);
ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem));
if (workshopCopy is null)
{
if (!SteamManager.Workshop.CanBeInstalled(workshopItem))
@@ -417,7 +421,7 @@ namespace Barotrauma.Steam
{
ContentPackageManager.WorkshopPackages.Refresh();
});
while (!ContentPackageManager.WorkshopPackages.Any(p => p.SteamWorkshopId == workshopItem.Id))
while (!ContentPackageManager.WorkshopPackages.Any(p => PackageMatchesItem(p, workshopItem)))
{
currentStepText.Text = SteamManager.Workshop.CanBeInstalled(workshopItem)
? TextManager.Get("PublishPopupInstall")
@@ -426,7 +430,7 @@ namespace Barotrauma.Steam
}
workshopCopy =
ContentPackageManager.WorkshopPackages.First(p => p.SteamWorkshopId == workshopItem.Id);
ContentPackageManager.WorkshopPackages.First(p => PackageMatchesItem(p, workshopItem));
}
bool localCopyMade = false;
@@ -480,7 +484,7 @@ namespace Barotrauma.Steam
messageBox.Buttons[0].Enabled = false;
Steamworks.Ugc.PublishResult? result = null;
Exception? resultException = null;
TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.SteamWorkshopId})",
TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.UgcId})",
editor.SubmitAsync(),
t =>
{
@@ -496,6 +500,8 @@ namespace Barotrauma.Steam
if (result is { Success: true })
{
var resultId = result.Value.FileId;
bool packageMatchesResult(ContentPackage p)
=> p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == resultId;
Steamworks.Ugc.Item resultItem = new Steamworks.Ugc.Item(resultId);
Task downloadTask = SteamManager.Workshop.ForceRedownload(resultItem);
while (!resultItem.IsInstalled && !downloadTask.IsCompleted)
@@ -511,7 +517,7 @@ namespace Barotrauma.Steam
}
ContentPackage? pkgToNuke
= ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == resultId);
= ContentPackageManager.WorkshopPackages.FirstOrDefault(packageMatchesResult);
if (pkgToNuke != null)
{
Directory.Delete(pkgToNuke.Dir, recursive: true);
@@ -537,7 +543,7 @@ namespace Barotrauma.Steam
var localModProject = new ModProject(localPackage)
{
SteamWorkshopId = resultId
UgcId = Option<ContentPackageId>.Some(new SteamWorkshopId(resultId))
};
localModProject.DiscardHashAndInstallTime();
localModProject.Save(localPackage.Path);

View File

@@ -19,7 +19,7 @@ namespace Barotrauma
public override bool Loaded => nestedStr.Loaded;
public override void RetrieveValue()
{
cachedValue = ToolBox.WrapText(nestedStr.Value, lineLength, font.Value, textScale);
cachedValue = ToolBox.WrapText(nestedStr.Value, lineLength, font.GetFontForStr(nestedStr.Value), textScale);
UpdateLanguage();
}
}

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.19.3.0</Version>
<Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.19.3.0</Version>
<Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>0.19.3.0</Version>
<Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.19.3.0</Version>
<Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.19.3.0</Version>
<Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -61,8 +61,9 @@ namespace Barotrauma
msg.WriteColorR8G8B8(Head.SkinColor);
msg.WriteColorR8G8B8(Head.HairColor);
msg.WriteColorR8G8B8(Head.FacialHairColor);
msg.WriteString(ragdollFileName);
msg.WriteString(ragdollFileName);
msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier);
if (Job != null)
{
msg.WriteUInt32(Job.Prefab.UintIdentifier);

View File

@@ -243,6 +243,7 @@ namespace Barotrauma
maxPlayers,
ownerKey,
steamId);
Server.StartServer();
for (int i = 0; i < CommandLineArgs.Length; i++)
{

View File

@@ -1,4 +1,5 @@
using Barotrauma.Networking;
using System;
using System.Xml.Linq;
namespace Barotrauma
@@ -20,19 +21,22 @@ namespace Barotrauma
CharacterInfo = client.CharacterInfo;
healthData = new XElement("health");
client.Character?.CharacterHealth?.Save(healthData);
if (client.Character?.Inventory != null)
//the character may not be controlled by the client atm, but still exist
Character character = client.Character ?? CharacterInfo?.Character;
character?.CharacterHealth?.Save(healthData);
if (character?.Inventory != null)
{
itemData = new XElement("inventory");
Character.SaveInventory(client.Character.Inventory, itemData);
Character.SaveInventory(character.Inventory, itemData);
}
OrderData = new XElement("orders");
if (client.CharacterInfo != null)
if (CharacterInfo != null)
{
CharacterInfo.SaveOrderData(client.CharacterInfo, OrderData);
CharacterInfo.SaveOrderData(CharacterInfo, OrderData);
}
if (client.Character?.Wallet.Save() is { } walletSave)
if (character?.Wallet.Save() is { } walletSave)
{
WalletData = walletSave;
}
@@ -118,9 +122,9 @@ namespace Barotrauma
character.SpawnInventoryItems(inventory, itemData.FromPackage(null));
}
public void ApplyHealthData(Character character)
public void ApplyHealthData(Character character, Func<AfflictionPrefab, bool> afflictionPredicate = null)
{
CharacterInfo.ApplyHealthData(character, healthData);
CharacterInfo.ApplyHealthData(character, healthData, afflictionPredicate);
}
public void ApplyOrderData(Character character)

View File

@@ -917,15 +917,14 @@ namespace Barotrauma
foreach (var kvp in purchasedItems)
{
foreach (var purchasedItemList in purchasedItems.Values)
var storeId = kvp.Key;
var purchasedItemList = kvp.Value;
foreach (var purchasedItem in purchasedItemList)
{
foreach (var purchasedItem in purchasedItemList)
{
int availableQuantity = map.CurrentLocation.Stores[kvp.Key].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0;
purchasedItem.Quantity = Math.Min(purchasedItem.Quantity, availableQuantity);
}
}
CargoManager.PurchaseItems(kvp.Key, kvp.Value, false, sender);
int availableQuantity = map.CurrentLocation.Stores[storeId].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0;
purchasedItem.Quantity = Math.Min(purchasedItem.Quantity, availableQuantity);
}
CargoManager.PurchaseItems(storeId, purchasedItemList, false, sender);
}
foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems)

View File

@@ -71,6 +71,12 @@ namespace Barotrauma.Networking
{
expirationTime = parsedTime;
}
else
{
string error = $"Failed to parse the ban duration of \"{name}\" ({separatedLine[2]}) from the legacy ban list file (text file which has now been changed to XML). Considering the ban permanent.";
DebugConsole.ThrowError(error);
GameServer.AddPendingMessageToOwner(error, ChatMessageType.Error);
}
}
string reason = separatedLine.Length > 3 ? string.Join(",", separatedLine.Skip(3)) : "";
@@ -156,6 +162,8 @@ namespace Barotrauma.Networking
public void BanPlayer(string name, Either<Address, AccountId> addressOrAccountId, string reason, TimeSpan? duration)
{
if (addressOrAccountId.TryGet(out Address address) && address.IsLocalHost) { return; }
var existingBan = bannedPlayers.Find(bp => bp.AddressOrAccountId == addressOrAccountId);
if (existingBan != null) { bannedPlayers.Remove(existingBan); }

View File

@@ -224,27 +224,55 @@ namespace Barotrauma.Networking
}
}
/// <summary>
/// Reset what this client has voted for and the kick votes given to this client
/// </summary>
public void ResetVotes(bool resetKickVotes)
{
for (int i = 0; i < votes.Length; i++)
{
votes[i] = null;
}
if (resetKickVotes)
{
kickVoters.Clear();
}
}
public void SetPermissions(ClientPermissions permissions, IEnumerable<DebugConsole.Command> permittedConsoleCommands)
{
this.Permissions = permissions;
this.PermittedConsoleCommands.Clear();
this.PermittedConsoleCommands.UnionWith(permittedConsoleCommands);
Permissions = permissions;
PermittedConsoleCommands.Clear();
PermittedConsoleCommands.UnionWith(permittedConsoleCommands);
if (Permissions.HasFlag(ClientPermissions.ManageSettings))
{
//ensure the client has the up-to-date server settings
GameMain.Server?.ServerSettings?.ForcePropertyUpdate();
}
}
public void GivePermission(ClientPermissions permission)
{
if (!this.Permissions.HasFlag(permission)) this.Permissions |= permission;
if (!Permissions.HasFlag(permission))
{
Permissions |= permission;
if (permission.HasFlag(ClientPermissions.ManageSettings))
{
//ensure the client has the up-to-date server settings
GameMain.Server?.ServerSettings?.ForcePropertyUpdate();
}
}
}
public void RemovePermission(ClientPermissions permission)
{
this.Permissions &= ~permission;
Permissions &= ~permission;
}
public bool HasPermission(ClientPermissions permission)
{
return this.Permissions.HasFlag(permission);
return Permissions.HasFlag(permission);
}
}
}

View File

@@ -39,7 +39,7 @@ namespace Barotrauma.Networking
string resultFileName
= dir.StartsWith(ContentPackage.LocalModsDir)
? $"Local_{mod.Name}"
: $"Workshop_{mod.Name}_{mod.SteamWorkshopId}";
: $"Workshop_{mod.Name}_{(mod.UgcId.TryUnwrap(out var ugcId) ? ugcId.ToString() : "NULL")}";
resultFileName = ToolBox.RemoveInvalidFileNameChars(resultFileName.Replace('\\', '_').Replace('/', '_'));
resultFileName = $"{resultFileName}{Extension}";
return Path.Combine(UploadFolder, resultFileName);

View File

@@ -246,7 +246,7 @@ namespace Barotrauma.Networking
" (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " +
(lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" +
" Events queued: " + events.Count + ", last sent to all: " + lastSentToAll, ServerLog.MessageType.Error);
server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncOldEvent + "/ServerMessage.ExcessiveDesyncOldEvent");
server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncOldEvent));
}
);
}
@@ -260,7 +260,7 @@ namespace Barotrauma.Networking
{
DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", Color.Red);
GameServer.Log(GameServer.ClientLogName(c) + " was kicked because they were expecting a removed network event (" + (c.LastRecvEntityEventID + 1).ToString() + ", last available is " + events[0].ID.ToString() + ")", ServerLog.MessageType.Error);
server.DisconnectClient(c, "", DisconnectReason.ExcessiveDesyncRemovedEvent + "/ServerMessage.ExcessiveDesyncRemovedEvent");
server.DisconnectClient(c, PeerDisconnectPacket.WithReason(DisconnectReason.ExcessiveDesyncRemovedEvent));
});
}
}
@@ -269,7 +269,7 @@ namespace Barotrauma.Networking
foreach (Client timedOutClient in timedOutClients)
{
GameServer.Log("Disconnecting client " + GameServer.ClientLogName(timedOutClient) + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error);
GameMain.Server.DisconnectClient(timedOutClient, "", DisconnectReason.SyncTimeout + "/ServerMessage.SyncTimeout");
GameMain.Server.DisconnectClient(timedOutClient, PeerDisconnectPacket.WithReason(DisconnectReason.SyncTimeout));
}
bufferedEvents.RemoveAll(b => b.IsProcessed);

View File

@@ -20,7 +20,7 @@ namespace Barotrauma.Networking
if (Sender != null && c.InGame)
{
msg.WriteUInt16(Sender.ID);
}
}
msg.WriteBoolean(false); //text color (no custom text colors for order messages)
msg.WritePadBits();
WriteOrder(msg);

View File

@@ -15,7 +15,7 @@ namespace Barotrauma.Networking
private readonly List<NetIncomingMessage> incomingLidgrenMessages;
public LidgrenServerPeer(Option<int> ownKey, ServerSettings settings)
public LidgrenServerPeer(Option<int> ownKey, ServerSettings settings, Callbacks callbacks) : base(callbacks)
{
serverSettings = settings;
@@ -68,21 +68,21 @@ namespace Barotrauma.Networking
}
}
public override void Close(string? msg = null)
public override void Close()
{
if (netServer == null) { return; }
for (int i = pendingClients.Count - 1; i >= 0; i--)
{
RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg);
RemovePendingClient(pendingClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown));
}
for (int i = connectedClients.Count - 1; i >= 0; i--)
{
Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString());
Disconnect(connectedClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown));
}
netServer.Shutdown(msg ?? DisconnectReason.ServerShutdown.ToString());
netServer.Shutdown(PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown).ToLidgrenStringRepresentation());
pendingClients.Clear();
connectedClients.Clear();
@@ -91,7 +91,7 @@ namespace Barotrauma.Networking
Steamworks.SteamServer.OnValidateAuthTicketResponse -= OnAuthChange;
OnShutdown?.Invoke();
callbacks.OnShutdown.Invoke();
}
public override void Update(float deltaTime)
@@ -100,12 +100,6 @@ namespace Barotrauma.Networking
ToolBox.ThrowIfNull(incomingLidgrenMessages);
if (OnOwnerDetermined != null && OwnerConnection != null)
{
OnOwnerDetermined?.Invoke(OwnerConnection);
OnOwnerDetermined = null;
}
netServer.ReadMessages(incomingLidgrenMessages);
//process incoming connections first
@@ -193,14 +187,14 @@ namespace Barotrauma.Networking
if (connectedClients.Count >= serverSettings.MaxPlayers)
{
inc.SenderConnection.Deny(DisconnectReason.ServerFull.ToString());
inc.SenderConnection.Deny(PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull).ToLidgrenStringRepresentation());
return;
}
if (serverSettings.BanList.IsBanned(new LidgrenEndpoint(inc.SenderConnection.RemoteEndPoint), out string banReason))
{
//IP banned: deny immediately
inc.SenderConnection.Deny($"{DisconnectReason.Banned}/ {banReason}");
inc.SenderConnection.Deny(PeerDisconnectPacket.Banned(banReason).ToLidgrenStringRepresentation());
return;
}
@@ -235,12 +229,12 @@ namespace Barotrauma.Networking
{
if (pendingClient != null)
{
RemovePendingClient(pendingClient, DisconnectReason.AuthenticationRequired, "Received data message from unauthenticated client");
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationRequired));
}
else if (lidgrenMsg.SenderConnection.Status != NetConnectionStatus.Disconnected &&
lidgrenMsg.SenderConnection.Status != NetConnectionStatus.Disconnecting)
{
lidgrenMsg.SenderConnection.Disconnect($"{DisconnectReason.AuthenticationRequired}/ Received data message from unauthenticated client");
lidgrenMsg.SenderConnection.Disconnect(PeerDisconnectPacket.WithReason(DisconnectReason.AuthenticationRequired).ToLidgrenStringRepresentation());
}
return;
@@ -252,12 +246,12 @@ namespace Barotrauma.Networking
|| (conn.AccountInfo.AccountId.TryUnwrap(out var accountId) && serverSettings.BanList.IsBanned(accountId, out banReason))
|| conn.AccountInfo.OtherMatchingIds.Any(id => serverSettings.BanList.IsBanned(id, out banReason)))
{
Disconnect(conn, $"{DisconnectReason.Banned}/ {banReason}");
Disconnect(conn, PeerDisconnectPacket.Banned(banReason));
return;
}
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
OnMessageReceived?.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn));
callbacks.OnMessageReceived.Invoke(conn, packet.GetReadMessage(packetHeader.IsCompressed(), conn));
}
}
@@ -275,12 +269,11 @@ namespace Barotrauma.Networking
{
DebugConsole.NewMessage("Owner disconnected: closing the server...");
GameServer.Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage);
Close($"{DisconnectReason.ServerShutdown}/ Owner disconnected");
Close();
}
else
{
#warning TODO: kill off disconnect in layer 1
Disconnect(conn, $"ServerMessage.HasDisconnected~[client]={GameMain.Server.ConnectedClients.First(c => c.Connection == conn).Name}");
Disconnect(conn, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
}
}
else
@@ -288,7 +281,7 @@ namespace Barotrauma.Networking
PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection);
if (pendingClient != null)
{
RemovePendingClient(pendingClient, DisconnectReason.Unknown, $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}");
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
}
}
@@ -312,9 +305,11 @@ namespace Barotrauma.Networking
{
if (status == Steamworks.AuthResponse.OK) { return; }
if (connectedClients.Find(c => c.AccountInfo.AccountId is Some<AccountId> { Value: SteamId id } && id.Value == steamId) is LidgrenConnection connection)
if (connectedClients.Find(c
=> c.AccountInfo.AccountId is Some<AccountId> { Value: SteamId id } && id.Value == steamId)
is LidgrenConnection connection)
{
Disconnect(connection, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam authentication status changed: {status}");
Disconnect(connection, PeerDisconnectPacket.SteamAuthError(status));
}
return;
@@ -325,7 +320,7 @@ namespace Barotrauma.Networking
|| serverSettings.BanList.IsBanned(new SteamId(steamId), out banReason)
|| serverSettings.BanList.IsBanned(new SteamId(ownerId), out banReason))
{
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason);
RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason));
return;
}
@@ -337,7 +332,7 @@ namespace Barotrauma.Networking
}
else
{
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, $"Steam authentication failed: {status}");
RemovePendingClient(pendingClient, PeerDisconnectPacket.SteamAuthError(status));
}
}
@@ -374,7 +369,7 @@ namespace Barotrauma.Networking
SendMsgInternal(conn, headers, body);
}
public override void Disconnect(NetworkConnection conn, string? msg = null)
public override void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket)
{
if (netServer == null) { return; }
@@ -384,11 +379,11 @@ namespace Barotrauma.Networking
{
lidgrenConn.Status = NetworkConnectionStatus.Disconnected;
connectedClients.Remove(lidgrenConn);
OnDisconnect?.Invoke(conn, msg);
callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket);
if (conn.AccountInfo.AccountId is Some<AccountId> { Value: SteamId steamId }) { SteamManager.StopAuthSession(steamId); }
}
lidgrenConn.NetConnection.Disconnect(msg ?? "Disconnected");
lidgrenConn.NetConnection.Disconnect(peerDisconnectPacket.ToLidgrenStringRepresentation());
}
protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body)
@@ -413,6 +408,7 @@ namespace Barotrauma.Networking
{
ownerKey = Option<int>.None();
OwnerConnection = pendingClient.Connection;
callbacks.OnOwnerDetermined.Invoke(OwnerConnection);
}
}
@@ -440,7 +436,7 @@ namespace Barotrauma.Networking
{
if (requireSteamAuth)
{
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: Steam ID not provided");
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed));
return;
}
}
@@ -451,7 +447,7 @@ namespace Barotrauma.Networking
{
if (requireSteamAuth)
{
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, $"Steam auth session failed to start: {authSessionStartState}");
RemovePendingClient(pendingClient, PeerDisconnectPacket.SteamAuthError(authSessionStartState));
}
else
{
@@ -471,7 +467,7 @@ namespace Barotrauma.Networking
{
if (pendingClient.AccountInfo.AccountId != packet.SteamId.Select(uid => (AccountId)uid))
{
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "SteamID mismatch");
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed));
}
}
}

View File

@@ -9,26 +9,31 @@ namespace Barotrauma.Networking
{
internal abstract class ServerPeer
{
public delegate void MessageCallback(NetworkConnection connection, IReadMessage message);
public readonly record struct Callbacks(
Callbacks.MessageCallback OnMessageReceived,
Callbacks.DisconnectCallback OnDisconnect,
Callbacks.InitializationCompleteCallback OnInitializationComplete,
Callbacks.ShutdownCallback OnShutdown,
Callbacks.OwnerDeterminedCallback OnOwnerDetermined)
{
public delegate void MessageCallback(NetworkConnection connection, IReadMessage message);
public delegate void DisconnectCallback(NetworkConnection connection, PeerDisconnectPacket peerDisconnectPacket);
public delegate void InitializationCompleteCallback(NetworkConnection connection, string? clientName);
public delegate void ShutdownCallback();
public delegate void OwnerDeterminedCallback(NetworkConnection connection);
}
public delegate void DisconnectCallback(NetworkConnection connection, string? reason);
public delegate void InitializationCompleteCallback(NetworkConnection connection, string? clientName);
public delegate void ShutdownCallback();
public delegate void OwnerDeterminedCallback(NetworkConnection connection);
public MessageCallback? OnMessageReceived;
public DisconnectCallback? OnDisconnect;
public InitializationCompleteCallback? OnInitializationComplete;
public ShutdownCallback? OnShutdown;
public OwnerDeterminedCallback? OnOwnerDetermined;
protected readonly Callbacks callbacks;
protected ServerPeer(Callbacks callbacks)
{
this.callbacks = callbacks;
}
public abstract void InitializeSteamServerCallbacks();
public abstract void Start();
public abstract void Close(string? msg = null);
public abstract void Close();
public abstract void Update(float deltaTime);
protected sealed class PendingClient
@@ -84,15 +89,16 @@ namespace Barotrauma.Networking
if (!Client.IsValidName(authPacket.Name, serverSettings))
{
RemovePendingClient(pendingClient, DisconnectReason.InvalidName, "");
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.InvalidName));
return;
}
bool isCompatibleVersion = NetworkMember.IsCompatible(authPacket.GameVersion, GameMain.Version.ToString()) ?? false;
bool isCompatibleVersion =
Version.TryParse(authPacket.GameVersion, out var remoteVersion)
&& NetworkMember.IsCompatible(remoteVersion, GameMain.Version);
if (!isCompatibleVersion)
{
RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion,
$"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={authPacket.GameVersion}");
RemovePendingClient(pendingClient, PeerDisconnectPacket.InvalidVersion());
GameServer.Log($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (incompatible game version)", ServerLog.MessageType.Error);
DebugConsole.NewMessage($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (incompatible game version)", Microsoft.Xna.Framework.Color.Red);
@@ -104,7 +110,7 @@ namespace Barotrauma.Networking
Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), authPacket.Name.ToLower()));
if (nameTaken != null)
{
RemovePendingClient(pendingClient, DisconnectReason.NameTaken, "");
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.NameTaken));
GameServer.Log($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (name too similar to the name of the client \"" + nameTaken.Name + "\").", ServerLog.MessageType.Error);
return;
}
@@ -135,7 +141,7 @@ namespace Barotrauma.Networking
{
const string banMsg = "Failed to enter correct password too many times";
BanPendingClient(pendingClient, banMsg, null);
RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg);
RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banMsg));
return;
}
}
@@ -190,13 +196,13 @@ namespace Barotrauma.Networking
{
if (IsPendingClientBanned(pendingClient, out string? banReason))
{
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason);
RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason));
return;
}
if (connectedClients.Count >= serverSettings.MaxPlayers)
{
RemovePendingClient(pendingClient, DisconnectReason.ServerFull, "");
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull));
}
if (pendingClient.InitializationStep == ConnectionInitialization.Success)
@@ -205,15 +211,15 @@ namespace Barotrauma.Networking
connectedClients.Add(newConnection);
pendingClients.Remove(pendingClient);
CheckOwnership(pendingClient);
callbacks.OnInitializationComplete.Invoke(newConnection, pendingClient.Name);
OnInitializationComplete?.Invoke(newConnection, pendingClient.Name);
CheckOwnership(pendingClient);
}
pendingClient.TimeOut -= Timing.Step;
if (pendingClient.TimeOut < 0.0)
{
RemovePendingClient(pendingClient, DisconnectReason.Unknown, Lidgren.Network.NetConnection.NoResponseMessage);
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
}
if (Timing.TotalTime < pendingClient.UpdateTime) { return; }
@@ -237,7 +243,7 @@ namespace Barotrauma.Networking
structToSend = new ServerPeerContentPackageOrderPacket
{
ServerName = GameMain.Server.ServerName,
ContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent)
ContentPackages = ContentPackageManager.EnabledPackages.All.Where(cp => cp.HasMultiplayerSyncedContent || cp.Files.All(f => f is SubmarineFile))
.Select(contentPackage => new ServerContentPackage(contentPackage, timeNow))
.ToImmutableArray()
};
@@ -267,11 +273,11 @@ namespace Barotrauma.Networking
protected virtual void CheckOwnership(PendingClient pendingClient) { }
protected void RemovePendingClient(PendingClient pendingClient, DisconnectReason reason, string? msg)
protected void RemovePendingClient(PendingClient pendingClient, PeerDisconnectPacket peerDisconnectPacket)
{
if (pendingClients.Contains(pendingClient))
{
Disconnect(pendingClient.Connection, $"{reason}/{msg}");
Disconnect(pendingClient.Connection, peerDisconnectPacket);
pendingClients.Remove(pendingClient);
@@ -285,6 +291,6 @@ namespace Barotrauma.Networking
}
public abstract void Send(IWriteMessage msg, NetworkConnection conn, DeliveryMethod deliveryMethod, bool compressPastThreshold = true);
public abstract void Disconnect(NetworkConnection conn, string? msg = null);
public abstract void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket);
}
}

View File

@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Networking
{
@@ -16,7 +17,7 @@ namespace Barotrauma.Networking
private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64);
private void WriteSteamId(IWriteMessage msg, SteamId val) => msg.WriteUInt64(val.Value ^ ownerKey64);
public SteamP2PServerPeer(SteamId steamId, int ownerKey, ServerSettings settings)
public SteamP2PServerPeer(SteamId steamId, int ownerKey, ServerSettings settings, Callbacks callbacks) : base(callbacks)
{
serverSettings = settings;
@@ -43,7 +44,7 @@ namespace Barotrauma.Networking
started = true;
}
public override void Close(string? msg = null)
public override void Close()
{
if (!started) { return; }
@@ -51,12 +52,12 @@ namespace Barotrauma.Networking
for (int i = pendingClients.Count - 1; i >= 0; i--)
{
RemovePendingClient(pendingClients[i], DisconnectReason.ServerShutdown, msg);
RemovePendingClient(pendingClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown));
}
for (int i = connectedClients.Count - 1; i >= 0; i--)
{
Disconnect(connectedClients[i], msg ?? DisconnectReason.ServerShutdown.ToString());
Disconnect(connectedClients[i], PeerDisconnectPacket.WithReason(DisconnectReason.ServerShutdown));
}
pendingClients.Clear();
@@ -64,19 +65,13 @@ namespace Barotrauma.Networking
ChildServerRelay.ShutDown();
OnShutdown?.Invoke();
callbacks.OnShutdown.Invoke();
}
public override void Update(float deltaTime)
{
if (!started) { return; }
if (OnOwnerDetermined != null && OwnerConnection != null)
{
OnOwnerDetermined?.Invoke(OwnerConnection);
OnOwnerDetermined = null;
}
//backwards for loop so we can remove elements while iterating
for (int i = connectedClients.Count - 1; i >= 0; i--)
{
@@ -84,7 +79,7 @@ namespace Barotrauma.Networking
conn.Decay(deltaTime);
if (conn.Timeout < 0.0)
{
Disconnect(conn, "Timed out");
Disconnect(conn, PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
}
}
@@ -149,24 +144,22 @@ namespace Barotrauma.Networking
{
if (pendingClient != null)
{
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason);
RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason));
}
else if (connectedClient != null)
{
Disconnect(connectedClient, $"{DisconnectReason.Banned}/ {banReason}");
Disconnect(connectedClient, PeerDisconnectPacket.Banned(banReason));
}
}
else if (packetHeader.IsDisconnectMessage())
{
if (pendingClient != null)
{
string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}";
RemovePendingClient(pendingClient, DisconnectReason.Unknown, disconnectMsg);
RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
}
else if (connectedClient != null)
{
string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={GameMain.Server.ConnectedClients.First(c => c.Connection == connectedClient).Name}";
Disconnect(connectedClient, disconnectMsg, false);
Disconnect(connectedClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
}
}
else if (packetHeader.IsHeartbeatMessage())
@@ -176,28 +169,27 @@ namespace Barotrauma.Networking
}
else if (packetHeader.IsConnectionInitializationStep())
{
if (!initialization.HasValue) { return; }
ConnectionInitialization initializationStep = initialization.Value;
if (pendingClient != null)
{
pendingClient.Connection.SetAccountInfo(new AccountInfo(senderSteamId, sentOwnerSteamId));
ReadConnectionInitializationStep(
pendingClient,
new ReadWriteMessage(inc.Buffer, inc.BitPosition, inc.LengthBits, false),
initialization ?? throw new Exception("Initialization step missing"));
initializationStep);
}
else if (initialization.HasValue)
else if (initializationStep == ConnectionInitialization.ConnectionStarted)
{
ConnectionInitialization initializationStep = initialization.Value;
if (initializationStep == ConnectionInitialization.ConnectionStarted)
{
pendingClients.Add(new PendingClient(new SteamP2PConnection(senderSteamId)));
}
pendingClients.Add(new PendingClient(new SteamP2PConnection(senderSteamId)));
}
}
else if (connectedClient != null)
{
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, connectedClient);
OnMessageReceived?.Invoke(connectedClient, msg);
callbacks.OnMessageReceived.Invoke(connectedClient, msg);
}
}
else //sender is owner
@@ -227,7 +219,8 @@ namespace Barotrauma.Networking
};
OwnerConnection.SetAccountInfo(new AccountInfo(ownerSteamId, ownerSteamId));
OnInitializationComplete?.Invoke(OwnerConnection, packet.OwnerName);
callbacks.OnInitializationComplete.Invoke(OwnerConnection, packet.OwnerName);
callbacks.OnOwnerDetermined.Invoke(OwnerConnection);
}
return;
@@ -241,7 +234,7 @@ namespace Barotrauma.Networking
{
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, OwnerConnection);
OnMessageReceived?.Invoke(OwnerConnection!, msg);
callbacks.OnMessageReceived.Invoke(OwnerConnection!, msg);
}
}
}
@@ -281,27 +274,21 @@ namespace Barotrauma.Networking
SendMsgInternal(steamP2PConn, headers, body);
}
private void SendDisconnectMessage(SteamId steamId, string? msg)
private void SendDisconnectMessage(SteamId steamId, PeerDisconnectPacket peerDisconnectPacket)
{
if (!started) { return; }
if (string.IsNullOrWhiteSpace(msg)) { return; }
var headers = new PeerPacketHeaders
{
DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage,
Initialization = null
};
var packet = new PeerDisconnectPacket
{
Message = msg
};
SendMsgInternal(steamId, headers, packet);
SendMsgInternal(steamId, headers, peerDisconnectPacket);
}
private void Disconnect(NetworkConnection conn, string? msg, bool sendDisconnectMessage)
public override void Disconnect(NetworkConnection conn, PeerDisconnectPacket peerDisconnectPacket)
{
if (!started) { return; }
@@ -309,26 +296,21 @@ namespace Barotrauma.Networking
if (!conn.AccountInfo.AccountId.TryUnwrap(out var connAccountId) || !(connAccountId is SteamId connSteamId)) { return; }
if (sendDisconnectMessage) { SendDisconnectMessage(connSteamId, msg); }
SendDisconnectMessage(connSteamId, peerDisconnectPacket);
if (connectedClients.Contains(steamp2pConn))
{
steamp2pConn.Status = NetworkConnectionStatus.Disconnected;
connectedClients.Remove(steamp2pConn);
OnDisconnect?.Invoke(conn, msg);
callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket);
Steam.SteamManager.StopAuthSession(connSteamId);
}
else if (steamp2pConn == OwnerConnection)
{
//TODO: fix?
throw new InvalidOperationException("Cannot disconnect owner peer");
}
}
public override void Disconnect(NetworkConnection conn, string? msg = null)
{
Disconnect(conn, msg, true);
}
protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body)
{
var connSteamId = conn is SteamP2PConnection { Endpoint: SteamP2PEndpoint { SteamId: var id } } ? id : null;

View File

@@ -505,9 +505,10 @@ namespace Barotrauma.Networking
var characterData = campaign?.GetClientCharacterData(clients[i]);
if (characterData != null && Level.Loaded?.Type != LevelData.LevelType.Outpost && characterData.HasSpawned)
{
//we need to reapply the previous respawn penalty affliction or successive deaths won't make it stack
characterData.ApplyHealthData(character, (AfflictionPrefab ap) => ap == GetRespawnPenaltyAfflictionPrefab());
GiveRespawnPenaltyAffliction(character);
}
if (characterData == null || characterData.HasSpawned)
{
//give the character the items they would've gotten if they had spawned in the main sub
@@ -555,7 +556,7 @@ namespace Barotrauma.Networking
foreach (Skill skill in characterInfo.Job.GetSkills())
{
var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier);
if (skillPrefab == null) { continue; }
if (skillPrefab == null || skill.Level < skillPrefab.LevelRange.End) { continue; }
skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillReductionOnDeath);
}
}

View File

@@ -18,10 +18,14 @@ namespace Barotrauma.Networking
{
if (!PropEquals(lastSyncedValue, Value))
{
LastUpdateID = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID);
LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID;
lastSyncedValue = Value;
}
}
public void ForceUpdate()
{
LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID++;
}
}
public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml";
@@ -54,6 +58,15 @@ namespace Barotrauma.Networking
LoadClientPermissions();
}
public void ForcePropertyUpdate()
{
UpdateFlag(NetFlags.Properties);
foreach (NetPropertyData property in netProperties.Values)
{
property.ForceUpdate();
}
}
private void WriteNetProperties(IWriteMessage outMsg, Client c)
{
foreach (UInt32 key in netProperties.Keys)
@@ -197,27 +210,32 @@ namespace Barotrauma.Networking
{
int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All;
int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All;
GameMain.NetLobbyScreen.MissionType = (Barotrauma.MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits);
GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits);
int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1;
if (traitorSetting < 0) traitorSetting = 2;
if (traitorSetting > 2) traitorSetting = 0;
if (traitorSetting < 0) { traitorSetting = 2; }
if (traitorSetting > 2) { traitorSetting = 0; }
TraitorsEnabled = (YesNoMaybe)traitorSetting;
int botCount = BotCount + incMsg.ReadByte() - 1;
if (botCount < 0) botCount = MaxBotCount;
if (botCount > MaxBotCount) botCount = 0;
if (botCount < 0) { botCount = MaxBotCount; }
if (botCount > MaxBotCount) { botCount = 0; }
BotCount = botCount;
int botSpawnMode = (int)BotSpawnMode + incMsg.ReadByte() - 1;
if (botSpawnMode < 0) botSpawnMode = 1;
if (botSpawnMode > 1) botSpawnMode = 0;
if (botSpawnMode < 0) { botSpawnMode = 1; }
if (botSpawnMode > 1) { botSpawnMode = 0; }
BotSpawnMode = (BotSpawnMode)botSpawnMode;
float levelDifficulty = incMsg.ReadSingle();
if (levelDifficulty >= 0.0f) SelectedLevelDifficulty = levelDifficulty;
if (levelDifficulty >= 0.0f) { SelectedLevelDifficulty = levelDifficulty; }
UseRespawnShuttle = incMsg.ReadBoolean();
bool changedUseRespawnShuttle = incMsg.ReadBoolean();
bool useRespawnShuttle = incMsg.ReadBoolean();
if (changedUseRespawnShuttle)
{
UseRespawnShuttle = useRespawnShuttle;
}
bool changedAutoRestart = incMsg.ReadBoolean();
bool autoRestart = incMsg.ReadBoolean();

View File

@@ -216,6 +216,14 @@ namespace Barotrauma
}
}
public void ResetVotes(IEnumerable<Client> connectedClients, bool resetKickVotes)
{
foreach (Client client in connectedClients)
{
client.ResetVotes(resetKickVotes);
}
}
public void ServerRead(IReadMessage inc, Client sender)
{
if (GameMain.Server == null || sender == null) { return; }

View File

@@ -192,7 +192,7 @@ namespace Barotrauma
public override void Select()
{
base.Select();
GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients);
GameMain.Server.Voting.ResetVotes(GameMain.Server.ConnectedClients, resetKickVotes: false);
if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this)
{
GameMain.GameSession = null;

View File

@@ -53,12 +53,13 @@ namespace Barotrauma.Steam
Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword;
Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName?.Value ?? "";
Steamworks.SteamServer.SetKey("haspassword", server.ServerSettings.HasPassword.ToString());
Steamworks.SteamServer.SetKey("message", GameMain.Server.ServerSettings.ServerMessageText);
Steamworks.SteamServer.SetKey("message", server.ServerSettings.ServerMessageText);
Steamworks.SteamServer.SetKey("version", GameMain.Version.ToString());
Steamworks.SteamServer.SetKey("playercount", GameMain.Server.ConnectedClients.Count.ToString());
Steamworks.SteamServer.SetKey("playercount", server.ConnectedClients.Count.ToString());
Steamworks.SteamServer.SetKey("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation)));
Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp => cp.SteamWorkshopId)));
Steamworks.SteamServer.SetKey("contentpackageid", string.Join(",", contentPackages.Select(cp
=> cp.UgcId.TryUnwrap(out var ugcId) ? ugcId.StringRepresentation : "")));
Steamworks.SteamServer.SetKey("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString());
Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString());
Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString());

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>0.19.3.0</Version>
<Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -527,7 +527,7 @@ namespace Barotrauma
{
// We'll want this to run each time, because the delegate is used to find a valid button component.
bool canAccessButtons = false;
foreach (var button in door.Item.GetConnectedComponents<Controller>(true))
foreach (var button in door.Item.GetConnectedComponents<Controller>(true, connectionFilter: c => c.Name == "toggle" || c.Name == "set_state"))
{
if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
{
@@ -675,6 +675,8 @@ namespace Barotrauma
}
}
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition);
//heavily prefer buttons linked to the door, so sub builders can help the bots figure out which button to use by linking them
if (door.Item.linkedTo.Contains(button.Item)) { distance *= 0.1f; }
if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item))
{
closestButton = button;

View File

@@ -1634,9 +1634,9 @@ namespace Barotrauma
return id;
}
public static void ApplyHealthData(Character character, XElement healthData)
public static void ApplyHealthData(Character character, XElement healthData, Func<AfflictionPrefab, bool> afflictionPredicate = null)
{
if (healthData != null) { character?.CharacterHealth.Load(healthData); }
if (healthData != null) { character?.CharacterHealth.Load(healthData, afflictionPredicate); }
}
/// <summary>

View File

@@ -12,12 +12,8 @@ namespace Barotrauma
{
public readonly static PrefabCollection<CharacterPrefab> Prefabs = new PrefabCollection<CharacterPrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
Character.RemoveByPrefab(this);
}

View File

@@ -11,13 +11,7 @@ namespace Barotrauma
{
public static readonly PrefabCollection<CorpsePrefab> Prefabs = new PrefabCollection<CorpsePrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public override void Dispose() { }
public static CorpsePrefab Get(Identifier identifier)
{

View File

@@ -1227,7 +1227,7 @@ namespace Barotrauma
}
}
public void Load(XElement element)
public void Load(XElement element, Func<AfflictionPrefab, bool> afflictionPredicate = null)
{
foreach (var subElement in element.Elements())
{
@@ -1260,6 +1260,7 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found.");
return;
}
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null)

View File

@@ -65,13 +65,7 @@ namespace Barotrauma
{
public static readonly PrefabCollection<JobPrefab> Prefabs = new PrefabCollection<JobPrefab>();
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public override void Dispose() { }
private static readonly Dictionary<Identifier, float> _itemRepairPriorities = new Dictionary<Identifier, float>();
/// <summary>

View File

@@ -56,11 +56,6 @@ namespace Barotrauma
}
}
private bool disposed = false;
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
}
public override void Dispose() { }
}
}

View File

@@ -40,7 +40,9 @@ namespace Barotrauma
}
catch
{
prefab.Dispose(); //clean up before rethrowing, since some prefab types might lock resources
//clean up before rethrowing, since some prefab types might lock resources
prefab.Dispose();
Prefabs.Remove(prefab);
throw;
}
}

View File

@@ -29,25 +29,25 @@ namespace Barotrauma
public readonly ImmutableArray<string> AltNames;
public readonly string Path;
public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? "";
public readonly UInt64 SteamWorkshopId;
public readonly Option<ContentPackageId> UgcId;
public readonly Version GameVersion;
public readonly string ModVersion;
public Md5Hash Hash { get; private set; }
public readonly DateTime? InstallTime;
public readonly Option<DateTime> InstallTime;
public ImmutableArray<ContentFile> Files { get; private set; }
public ImmutableArray<ContentFile.LoadError> Errors { get; private set; }
public async Task<bool> IsUpToDate()
{
if (SteamWorkshopId != 0 && InstallTime.HasValue)
{
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(SteamWorkshopId);
if (item is null) { return true; }
return item.Value.LatestUpdateTime <= InstallTime;
}
return true;
if (!UgcId.TryUnwrap(out var ugcId)) { return true; }
if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return true; }
if (!InstallTime.TryUnwrap(out var installTime)) { return true; }
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
if (item is null) { return true; }
return item.Value.LatestUpdateTime <= installTime;
}
public int Index => ContentPackageManager.EnabledPackages.IndexOf(this);
@@ -66,18 +66,19 @@ namespace Barotrauma
AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>())
.Select(n => n.Trim()).ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty");
SteamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0);
UInt64 steamWorkshopId = rootElement.GetAttributeUInt64("steamworkshopid", 0);
UgcId = steamWorkshopId != 0
? Option<ContentPackageId>.Some(new SteamWorkshopId(steamWorkshopId))
: Option<ContentPackageId>.None();
GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version);
ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion);
if (rootElement.Attribute("installtime") != null)
{
InstallTime = ToolBox.Epoch.ToDateTime(rootElement.GetAttributeUInt("installtime", 0));
}
else
{
InstallTime = null;
}
UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0);
InstallTime = installTimeUnix != 0
? Option<DateTime>.Some(ToolBox.Epoch.ToDateTime(installTimeUnix))
: Option<DateTime>.None();
var fileResults = rootElement.Elements()
.Select(e => ContentFile.CreateFromXElement(this, e))

View File

@@ -0,0 +1,19 @@
#nullable enable
namespace Barotrauma
{
public abstract class ContentPackageId
{
public abstract string StringRepresentation { get; }
public override string ToString()
=> StringRepresentation;
public abstract override bool Equals(object? obj);
public abstract override int GetHashCode();
public static Option<ContentPackageId> Parse(string s)
=> ReflectionUtils.ParseDerived<ContentPackageId, string>(s);
}
}

View File

@@ -0,0 +1,32 @@
#nullable enable
using System;
using System.Globalization;
namespace Barotrauma
{
sealed class SteamWorkshopId : ContentPackageId
{
public readonly UInt64 Value;
public SteamWorkshopId(UInt64 value)
{
Value = value;
}
private const string Prefix = "STEAM_WORKSHOP_";
public override string StringRepresentation => Value.ToString(CultureInfo.InvariantCulture);
public override bool Equals(object? obj)
=> obj is SteamWorkshopId otherWorkshopId && otherWorkshopId.Value == Value;
public override int GetHashCode() => Value.GetHashCode();
public new static Option<SteamWorkshopId> Parse(string s)
{
if (s.StartsWith(Prefix)) { s = s[Prefix.Length..]; }
if (!UInt64.TryParse(s, out var id) || id == 0) { return Option<SteamWorkshopId>.None(); }
return Option<SteamWorkshopId>.Some(new SteamWorkshopId(id));
}
}
}

View File

@@ -181,7 +181,7 @@ namespace Barotrauma
{
if (Core != null && !ContentPackageManager.CorePackages.Contains(Core))
{
SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.SteamWorkshopId == Core.SteamWorkshopId) ??
SetCore(ContentPackageManager.WorkshopPackages.Core.FirstOrDefault(p => p.UgcId == Core.UgcId) ??
ContentPackageManager.CorePackages.First());
}
@@ -193,7 +193,7 @@ namespace Barotrauma
newRegular.Add(p);
}
else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2
=> p2.SteamWorkshopId == p.SteamWorkshopId) is { } newP)
=> p2.UgcId == p.UgcId) is { } newP)
{
newRegular.Add(newP);
}

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