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
@@ -1,5 +1,6 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Barotrauma.Tutorials;
using FarseerPhysics; using FarseerPhysics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
@@ -398,36 +399,58 @@ namespace Barotrauma
progressBar.Draw(spriteBatch, cam); 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); Hull currentHull = entity switch
if (iconStyle == null) { continue; } {
Range<float> visibleRange = new Range<float>(npc.CurrentHull == Character.Controlled.CurrentHull ? 500.0f : 100.0f, float.PositiveInfinity); Character character => character.CurrentHull,
if (npc.CampaignInteractionType == CampaignMode.InteractionType.Examine) 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 //TODO: we could probably do better than just hardcoding
//a check for InteractionType.Examine here. //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); var body = Submarine.CheckVisibility(character.SimPosition, entity.SimPosition, ignoreLevel: true);
if (body != null && body.UserData as Character != npc) { continue; } if (body != null && body.UserData != entity) { return; }
visibleRange = new Range<float>(-100f, 500f); 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 distFactor = 1.0f - MathUtils.InverseLerp(1000.0f, 3000.0f, dist);
float alpha = MathHelper.Lerp(0.3f, 1.0f, distFactor); float alpha = MathHelper.Lerp(0.3f, 1.0f, distFactor);
GUI.DrawIndicator( GUI.DrawIndicator(
spriteBatch, spriteBatch,
npc.WorldPosition, entity.WorldPosition,
cam, cam,
visibleRange, visibleRange,
iconStyle.GetDefaultSprite(), style.GetDefaultSprite(),
iconStyle.Color * alpha, style.Color * alpha,
label: npc.Info?.Title); 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) foreach (Item item in Item.ItemList)
@@ -521,8 +521,9 @@ namespace Barotrauma
Color skinColor = inc.ReadColorR8G8B8(); Color skinColor = inc.ReadColorR8G8B8();
Color hairColor = inc.ReadColorR8G8B8(); Color hairColor = inc.ReadColorR8G8B8();
Color facialHairColor = inc.ReadColorR8G8B8(); Color facialHairColor = inc.ReadColorR8G8B8();
string ragdollFile = inc.ReadString();
string ragdollFile = inc.ReadString();
Identifier npcId = inc.ReadIdentifier();
uint jobIdentifier = inc.ReadUInt32(); uint jobIdentifier = inc.ReadUInt32();
int variant = inc.ReadByte(); int variant = inc.ReadByte();
@@ -539,9 +540,9 @@ namespace Barotrauma
} }
// TODO: animations // 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.RecreateHead(tagSet.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
ch.Head.SkinColor = skinColor; ch.Head.SkinColor = skinColor;
@@ -63,7 +63,7 @@ namespace Barotrauma
files = contentPackage.Files.Select(File.FromContentFile).ToList(); files = contentPackage.Files.Select(File.FromContentFile).ToList();
ModVersion = IncrementModVersion(contentPackage.ModVersion); ModVersion = IncrementModVersion(contentPackage.ModVersion);
IsCore = contentPackage is CorePackage; IsCore = contentPackage is CorePackage;
SteamWorkshopId = contentPackage.SteamWorkshopId; UgcId = contentPackage.UgcId;
ExpectedHash = contentPackage.Hash; ExpectedHash = contentPackage.Hash;
InstallTime = contentPackage.InstallTime; InstallTime = contentPackage.InstallTime;
} }
@@ -90,9 +90,9 @@ namespace Barotrauma
public bool IsCore = false; 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) public bool HasFile(File file)
=> Files.Any(f => => Files.Any(f =>
@@ -120,7 +120,7 @@ namespace Barotrauma
public void DiscardHashAndInstallTime() public void DiscardHashAndInstallTime()
{ {
ExpectedHash = null; ExpectedHash = null;
InstallTime = null; InstallTime = Option<DateTime>.None();
} }
public static string IncrementModVersion(string modVersion) public static string IncrementModVersion(string modVersion)
@@ -155,11 +155,11 @@ namespace Barotrauma
addRootAttribute("name", Name); addRootAttribute("name", Name);
if (!ModVersion.IsNullOrEmpty()) { addRootAttribute("modversion", ModVersion); } if (!ModVersion.IsNullOrEmpty()) { addRootAttribute("modversion", ModVersion); }
addRootAttribute("corepackage", IsCore); 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); addRootAttribute("gameversion", GameMain.Version);
if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); } if (AltNames.Any()) { addRootAttribute("altnames", string.Join(",", AltNames)); }
if (ExpectedHash != null) { addRootAttribute("expectedhash", ExpectedHash.StringRepresentation); } 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())); files.ForEach(f => rootElement.Add(f.ToXElement()));
@@ -45,9 +45,11 @@ namespace Barotrauma
var needInstalling = subscribedItems.Where(item var needInstalling = subscribedItems.Where(item
=> !WorkshopPackages.Any(p => !WorkshopPackages.Any(p
=> item.Id == p.SteamWorkshopId => p.UgcId.TryUnwrap(out var ugcId)
&& p.InstallTime.HasValue && ugcId is SteamWorkshopId workshopId
&& item.LatestUpdateTime <= p.InstallTime)) && item.Id == workshopId.Value
&& p.InstallTime.TryUnwrap(out var installTime)
&& item.LatestUpdateTime <= installTime))
.ToArray(); .ToArray();
if (needInstalling.Any()) if (needInstalling.Any())
{ {
@@ -16,7 +16,7 @@ namespace Barotrauma.Transition
/// Class dedicated to transitioning away from the old, shitty /// Class dedicated to transitioning away from the old, shitty
/// Mods + Submarines folders to the new LocalMods folder /// Mods + Submarines folders to the new LocalMods folder
/// </summary> /// </summary>
public static class UgcTransition public static class LegacySteamUgcTransition
{ {
private const string readmeName = "LOCALMODS_README.txt"; private const string readmeName = "LOCALMODS_README.txt";
@@ -168,7 +168,11 @@ namespace Barotrauma.Transition
addHeader(TextManager.Get("SubscribedMods")); addHeader(TextManager.Get("SubscribedMods"));
foreach (var mod in mods.Mods) 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)));
} }
} }
@@ -2489,26 +2489,8 @@ namespace Barotrauma
Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false; Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
ToolBox.OpenFileWithShell(Path.GetFullPath(filePath)); ToolBox.OpenFileWithShell(Path.GetFullPath(filePath));
})); }));
#if DEBUG #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) => commands.Add(new Command("checkduplicates", "Checks the given language for duplicate translation keys and writes to file.", (string[] args) =>
{ {
if (args.Length != 1) { return; } 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 => 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); var character = (args.Length == 0) ? Character.Controlled : FindMatchingCharacter(args, true);
if (character == null) if (character == null)
{ {
@@ -3072,11 +3049,6 @@ namespace Barotrauma
commands.Add(new Command("loadwearable", "Force select certain variant for the selected character.", args => 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; var character = Character.Controlled;
if (character == null) if (character == null)
{ {
@@ -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;
}
}
@@ -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;
}
}
@@ -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!");
}
}
}
@@ -6,23 +6,26 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Threading;
using Barotrauma.Threading;
namespace Barotrauma namespace Barotrauma
{ {
public class ScalableFont : IDisposable 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 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 readonly string filename;
private Face face; private readonly Face face;
private uint size; private uint size;
private int baseHeight; private int baseHeight;
private Dictionary<uint, GlyphData> texCoords; private readonly Dictionary<uint, GlyphData> texCoords;
private List<Texture2D> textures; private readonly List<Texture2D> textures;
private GraphicsDevice graphicsDevice; private readonly GraphicsDevice graphicsDevice;
private Vector2 currentDynamicAtlasCoords; private Vector2 currentDynamicAtlasCoords;
private int currentDynamicAtlasNextY; private int currentDynamicAtlasNextY;
@@ -49,7 +52,7 @@ namespace Barotrauma
set set
{ {
size = value; 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) 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) foreach (ScalableFont font in FontList)
{ {
if (font.filename == filename) if (font.filename == filename)
@@ -106,19 +113,23 @@ namespace Barotrauma
break; 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) this.face ??= new Face(Lib, filename);
{ this.size = size;
RenderAtlas(gd); 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); FontList.Add(this);
} }
} }
@@ -162,7 +173,7 @@ namespace Barotrauma
Vector2 currentCoords = Vector2.Zero; Vector2 currentCoords = Vector2.Zero;
int nextY = 0; int nextY = 0;
lock (mutex) using (new WriteLock(rwl))
{ {
face.SetPixelSizes(0, size); face.SetPixelSizes(0, size);
face.LoadGlyph(face.GetCharIndex(baseChar), LoadFlags.Default, LoadTarget.Normal); face.LoadGlyph(face.GetCharIndex(baseChar), LoadFlags.Default, LoadTarget.Normal);
@@ -175,19 +186,22 @@ namespace Barotrauma
for (uint j = start; j <= end; j++) for (uint j = start; j <= end; j++)
{ {
uint glyphIndex = face.GetCharIndex(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); face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal);
if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0)
{ {
if (face.Glyph.Metrics.HorizontalAdvance > 0) //glyph is empty, but char might still apply advance
{ GlyphData blankData = new GlyphData(
//glyph is empty, but char still applies advance advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f),
GlyphData blankData = new GlyphData( texIndex: -1); //indicates no texture because the glyph is empty
advance: (float)face.Glyph.Metrics.HorizontalAdvance,
texIndex: -1); //indicates no texture because the glyph is empty
texCoords.Add(j, blankData); texCoords.Add(j, blankData);
}
continue; continue;
} }
//stacktrace doesn't really work that well when RenderGlyph throws an exception //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) private void DynamicRenderAtlas(GraphicsDevice gd, uint character, int texDims = 1024, uint baseChar = 0x54)
{ {
bool missingCharacterFound = false; bool missingCharacterFound = false;
lock (mutex) using (new ReadLock(rwl))
{ {
missingCharacterFound = !texCoords.ContainsKey(character); missingCharacterFound = !texCoords.ContainsKey(character);
} }
@@ -268,10 +282,9 @@ namespace Barotrauma
private void DynamicRenderAtlas(GraphicsDevice gd, string str, int texDims = 1024, uint baseChar = 0x54) private void DynamicRenderAtlas(GraphicsDevice gd, string str, int texDims = 1024, uint baseChar = 0x54)
{ {
bool missingCharacterFound = false; bool missingCharacterFound = false;
var distinctChrs = str.Distinct().Select(c => (uint)c).ToArray(); using (new ReadLock(rwl))
lock (mutex)
{ {
foreach (var character in distinctChrs) foreach (var character in str)
{ {
if (texCoords.ContainsKey(character)) { continue; } if (texCoords.ContainsKey(character)) { continue; }
@@ -280,7 +293,7 @@ namespace Barotrauma
} }
} }
if (!missingCharacterFound) { return; } 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) private void DynamicRenderAtlas(GraphicsDevice gd, IEnumerable<uint> characters, int texDims = 1024, uint baseChar = 0x54)
@@ -299,7 +312,7 @@ namespace Barotrauma
Fixed26Dot6 horizontalAdvance; Fixed26Dot6 horizontalAdvance;
Vector2 drawOffset; Vector2 drawOffset;
lock (mutex) using (new WriteLock(rwl))
{ {
if (textures.Count == 0) if (textures.Count == 0)
{ {
@@ -318,20 +331,23 @@ namespace Barotrauma
if (texCoords.ContainsKey(character)) { continue; } if (texCoords.ContainsKey(character)) { continue; }
uint glyphIndex = face.GetCharIndex(character); 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.SetPixelSizes(0, size);
face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal); face.LoadGlyph(glyphIndex, LoadFlags.Default, LoadTarget.Normal);
if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0) if (face.Glyph.Metrics.Width == 0 || face.Glyph.Metrics.Height == 0)
{ {
if (face.Glyph.Metrics.HorizontalAdvance > 0) //glyph is empty, but char might still apply advance
{ GlyphData blankData = new GlyphData(
//glyph is empty, but char still applies advance advance: Math.Max((float)face.Glyph.Metrics.HorizontalAdvance, 0f),
GlyphData blankData = new GlyphData( texIndex: -1); //indicates no texture because the glyph is empty
advance: (float)face.Glyph.Metrics.HorizontalAdvance, texCoords.Add(character, blankData);
texIndex: -1); //indicates no texture because the glyph is empty
texCoords.Add(character, blankData);
}
continue; continue;
} }
@@ -730,7 +730,7 @@ namespace Barotrauma
public void DrawToolTip(SpriteBatch spriteBatch) public void DrawToolTip(SpriteBatch spriteBatch)
{ {
if (!Visible) { return; } if (!Visible) { return; }
DrawToolTip(spriteBatch, ToolTip, GUI.MouseOn.Rect); DrawToolTip(spriteBatch, ToolTip, Rect);
} }
public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos) public static void DrawToolTip(SpriteBatch spriteBatch, RichString toolTip, Vector2 pos)
@@ -781,7 +781,7 @@ namespace Barotrauma
if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10) if (toolTipBlock.Rect.Bottom > GameMain.GraphicsHeight - 10)
{ {
toolTipBlock.RectTransform.AbsoluteOffset -= new Point( toolTipBlock.RectTransform.AbsoluteOffset -= new Point(
(targetElement.Width / 2) * Math.Sign(targetElement.Center.X - toolTipBlock.Center.X), 0,
toolTipBlock.Rect.Bottom - (GameMain.GraphicsHeight - 10)); toolTipBlock.Rect.Bottom - (GameMain.GraphicsHeight - 10));
} }
toolTipBlock.SetTextPos(); toolTipBlock.SetTextPos();
@@ -204,15 +204,7 @@ namespace Barotrauma
currentHighestParent = FindHighestParent(); currentHighestParent = FindHighestParent();
currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList; currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList;
rectT.ParentChanged += (RectTransform newParent) => rectT.ParentChanged += _ => RefreshListBoxParent();
{
currentHighestParent.GUIComponent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList;
if (newParent != null)
{
currentHighestParent = FindHighestParent();
currentHighestParent.GUIComponent.OnAddedToGUIUpdateList += AddListBoxToGUIUpdateList;
}
};
} }
@@ -396,6 +388,15 @@ namespace Barotrauma
return true; 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) private void AddListBoxToGUIUpdateList(GUIComponent parent)
{ {
//the parent is not our parent anymore :( //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 //and somewhere between this component and the higher parent a component was removed
for (int i = 1; i < parentHierarchy.Count; i++) 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; continue;
return;
} }
parent.OnAddedToGUIUpdateList -= AddListBoxToGUIUpdateList;
return;
} }
if (Dropped) if (Dropped)
@@ -34,7 +34,7 @@ namespace Barotrauma
public GUIFrame InnerFrame { get; private set; } public GUIFrame InnerFrame { get; private set; }
public GUITextBlock Header { get; private set; } public GUITextBlock Header { get; private set; }
public GUITextBlock Text { 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 Closed { get; private set; }
public bool DisplayInLoadingScreens; public bool DisplayInLoadingScreens;
@@ -78,6 +78,8 @@ namespace Barotrauma
/// </summary> /// </summary>
private readonly Func<bool> autoCloseCondition; private readonly Func<bool> autoCloseCondition;
public bool FlashOnAutoCloseCondition { get; set; }
public Type MessageBoxType => type; public Type MessageBoxType => type;
public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault(); public static GUIComponent VisibleBox => MessageBoxes.LastOrDefault();
@@ -142,7 +144,7 @@ namespace Barotrauma
} }
GUIStyle.Apply(InnerFrame, "", this); GUIStyle.Apply(InnerFrame, "", this);
this.type = type; this.type = type;
Tag = tag; Tag = tag.ToIdentifier();
#warning TODO: These should be broken into separate methods at least #warning TODO: These should be broken into separate methods at least
if (type == Type.Default || type == Type.Vote) 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]); var button = new GUIButton(new RectTransform(new Vector2(0.6f, 1.0f / buttons.Length), buttonContainer.RectTransform), buttons[i]);
Buttons.Add(button); Buttons.Add(button);
} }
GUITextBlock.AutoScaleAndNormalize(Buttons.Select(btn => btn.TextBlock));
} }
else if (type == Type.InGame || type == Type.Tutorial) else if (type == Type.InGame || type == Type.Tutorial)
{ {
@@ -540,9 +543,7 @@ namespace Barotrauma
if (type == Type.InGame || type == Type.Tutorial) if (type == Type.InGame || type == Type.Tutorial)
{ {
initialPos = new Vector2(0.0f, GameMain.GraphicsHeight); initialPos = new Vector2(0.0f, GameMain.GraphicsHeight);
defaultPos = type == Type.InGame ? defaultPos = new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale);
new Vector2(0.0f, HUDLayoutSettings.InventoryAreaLower.Y - InnerFrame.Rect.Height - 20 * GUI.Scale) :
new Vector2(0.0f, GameMain.GraphicsHeight / 2);
endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y); endPos = new Vector2(GameMain.GraphicsWidth, defaultPos.Y);
} }
else else
@@ -574,10 +575,18 @@ namespace Barotrauma
inGameCloseTimer += deltaTime; inGameCloseTimer += deltaTime;
} }
if (inGameCloseTimer >= inGameCloseTime || (autoCloseCondition != null && autoCloseCondition())) if (inGameCloseTimer >= inGameCloseTime)
{ {
Close(); Close();
} }
else if (autoCloseCondition != null && autoCloseCondition())
{
Close();
if (FlashOnAutoCloseCondition)
{
InnerFrame.Flash(GUIStyle.Green);
}
}
} }
else else
{ {
@@ -634,7 +643,6 @@ namespace Barotrauma
} }
} }
public void Close() public void Close()
{ {
if (IsAnimated) if (IsAnimated)
@@ -662,6 +670,19 @@ namespace Barotrauma
MessageBoxes.Clear(); 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> /// <summary>
/// Parent does not matter. It's overridden. /// Parent does not matter. It's overridden.
/// </summary> /// </summary>
@@ -178,19 +178,19 @@ namespace Barotrauma
{ {
public GUIFont(string identifier) : base(identifier) { } public GUIFont(string identifier) : base(identifier) { }
public bool HasValue => Prefabs.Any(); public bool HasValue => !Prefabs.IsEmpty;
public ScalableFont Value => Prefabs.ActivePrefab.Font; public ScalableFont Value => Prefabs.ActivePrefab.Font;
public static implicit operator ScalableFont(GUIFont reference) => reference.Value; 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; public uint Size => HasValue ? Value.Size : 0;
private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value); 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; 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) public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth)
@@ -161,7 +161,7 @@ namespace Barotrauma
public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale); public static Point ItemFrameOffset => new Point(0, 3).Multiply(GUI.SlicedSpriteScale);
public static GUIComponentStyle GetComponentStyle(string name) 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) public static void Apply(GUIComponent targetComponent, string styleName = "", GUIComponent parent = null)
{ {
@@ -177,7 +177,7 @@ namespace Barotrauma
radioButtonGroup = rbg; 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); 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; text.RectTransform.RelativeSize = textBlockScale;
@@ -233,7 +233,9 @@ namespace Barotrauma
get get
{ {
Point absoluteOffset = ConvertOffsetRelativeToAnchor(AbsoluteOffset, Anchor); 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); relativeOffset = ConvertOffsetRelativeToAnchor(relativeOffset, Anchor);
return AnchorPoint + PivotOffset + absoluteOffset + relativeOffset + ScreenSpaceOffset; return AnchorPoint + PivotOffset + absoluteOffset + relativeOffset + ScreenSpaceOffset;
} }
@@ -256,6 +258,7 @@ namespace Barotrauma
public Rectangle ParentRect => Parent != null ? Parent.Rect : UIRect; public Rectangle ParentRect => Parent != null ? Parent.Rect : UIRect;
protected Rectangle NonScaledRect => new Rectangle(NonScaledTopLeft, NonScaledSize); protected Rectangle NonScaledRect => new Rectangle(NonScaledTopLeft, NonScaledSize);
protected virtual Rectangle NonScaledUIRect => NonScaledRect; 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 NonScaledParentRect => parent != null ? Parent.NonScaledRect : UIRect;
protected Rectangle NonScaledParentUIRect => parent != null ? Parent.NonScaledUIRect : UIRect; protected Rectangle NonScaledParentUIRect => parent != null ? Parent.NonScaledUIRect : UIRect;
protected Rectangle UIRect => new Rectangle(0, 0, GUI.UIWidth, GameMain.GraphicsHeight); 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<RectTransform> ChildrenChanged;
public event Action ScaleChanged; public event Action ScaleChanged;
public event Action SizeChanged; public event Action SizeChanged;
public void ResetSizeChanged()
{
SizeChanged = null;
}
#endregion #endregion
#region Initialization #region Initialization
@@ -730,17 +738,17 @@ namespace Barotrauma
get { return animTargetPos ?? AbsoluteOffset; } get { return animTargetPos ?? AbsoluteOffset; }
} }
public void MoveOverTime(Point targetPos, float duration) public void MoveOverTime(Point targetPos, float duration, Action onDoneMoving = null)
{ {
animTargetPos = targetPos; animTargetPos = targetPos;
CoroutineManager.StartCoroutine(DoMoveAnimation(targetPos, duration)); CoroutineManager.StartCoroutine(DoMoveAnimation(targetPos, duration, onDoneMoving));
} }
public void ScaleOverTime(Point targetSize, float duration) public void ScaleOverTime(Point targetSize, float duration)
{ {
CoroutineManager.StartCoroutine(DoScaleAnimation(targetSize, 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(); Vector2 startPos = AbsoluteOffset.ToVector2();
float t = 0.0f; float t = 0.0f;
@@ -752,6 +760,7 @@ namespace Barotrauma
} }
AbsoluteOffset = targetPos; AbsoluteOffset = targetPos;
animTargetPos = null; animTargetPos = null;
onDoneMoving?.Invoke();
yield return CoroutineStatus.Success; yield return CoroutineStatus.Success;
} }
private IEnumerable<CoroutineStatus> DoScaleAnimation(Point targetSize, float duration) private IEnumerable<CoroutineStatus> DoScaleAnimation(Point targetSize, float duration)
@@ -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.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.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.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.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.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.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.selectSubmarineButton = new GUIButton(new RectTransform(Vector2.One, submarineDisplayElement.background.RectTransform), style: null); 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, Color = Color.White,
HoverColor = Color.White, HoverColor = Color.White,
PressedColor = 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; submarineDisplays[i] = submarineDisplayElement;
} }
@@ -395,9 +396,13 @@ namespace Barotrauma
return true; 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.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.Text = TextManager.Get($"submarinetier.{subToDisplay.Tier}");
submarineDisplays[i].submarineTier.ToolTip = TextManager.Get("submarinetier.description");
if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay)) if (!GameMain.GameSession.IsSubmarineOwned(subToDisplay))
{ {
@@ -1722,7 +1722,11 @@ namespace Barotrauma
var subInfoTextLayout = new GUILayoutGroup(new RectTransform(Vector2.One, paddedFrame.RectTransform)); 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 nameHeight = (int)GUIStyle.LargeFont.MeasureString(sub.Info.DisplayName, true).Y;
int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y;
@@ -2140,18 +2144,17 @@ namespace Barotrauma
skillNames.Add(skillName); skillNames.Add(skillName);
skillName.RectTransform.MinSize = new Point(0, skillName.Rect.Height); skillName.RectTransform.MinSize = new Point(0, skillName.Rect.Height);
skillContainer.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); 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; float modifiedSkillLevel = character?.GetSkillLevel(skill.Identifier) ?? skill.Level;
if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level))) if (!MathUtils.NearlyEqual(MathF.Floor(modifiedSkillLevel), MathF.Floor(skill.Level)))
{ {
int skillChange = (int)MathF.Floor(modifiedSkillLevel - 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 = skillChange switch
string stringColor = true switch
{ {
true when skillChange > 0 => XMLExtensions.ToStringHex(GUIStyle.Green), > 0 => XMLExtensions.ToStringHex(GUIStyle.Green),
true when skillChange < 0 => XMLExtensions.ToStringHex(GUIStyle.Red), < 0 => XMLExtensions.ToStringHex(GUIStyle.Red),
_ => XMLExtensions.ToStringHex(GUIStyle.TextColorNormal) _ => XMLExtensions.ToStringHex(GUIStyle.TextColorNormal)
}; };
@@ -1257,12 +1257,17 @@ namespace Barotrauma
List<Upgrade> upgrades = entity.GetUpgrades(); List<Upgrade> upgrades = entity.GetUpgrades();
int upgradesCount = upgrades.Count; int upgradesCount = upgrades.Count;
const int maxUpgrades = 4; 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) if (slotIndex > -1)
{ {
itemName.Text = TextManager.GetWithVariables("weaponslotwithname", ("[number]", slotIndex.ToString()), ("[weaponname]", itemName.Text)); 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(); upgradeList.Content.ClearChildren();
for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++) for (var i = 0; i < upgrades.Count && i < maxUpgrades; i++)
{ {
@@ -1275,7 +1280,7 @@ namespace Barotrauma
// include pending upgrades into the tooltip // include pending upgrades into the tooltip
foreach (var (prefab, category, level) in upgradeManager.PendingUpgrades) 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; bool found = false;
foreach (GUITextBlock textBlock in upgradeList.Content.Children.Where(c => c is GUITextBlock).Cast<GUITextBlock>()) 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); } if (!itemElement.Selected) { itemElement.OnClicked(itemElement, itemElement.UserData); }
(itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement); (itemElement.Parent?.Parent?.Parent as GUIListBox)?.ScrollToElement(itemElement);
} }
else
{
ScrollToCategory(data => data.Category.CanBeApplied(item, null));
}
} }
} }
else else
@@ -1463,23 +1472,16 @@ namespace Barotrauma
// submarine name // submarine name
new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.DisplayName, textAlignment: Alignment.Right, font: GUIStyle.LargeFont); 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}"))}"; LocalizedString classText = $"{TextManager.GetWithVariable("submarineclass.classsuffixformat", "[type]", TextManager.Get($"submarineclass.{submarine.Info.SubmarineClass}"))}";
// submarine class + tier // 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; new GUITextBlock(rectT(1.0f, 0.15f, submarineInfoFrame), TextManager.Get($"submarinetier.{submarine.Info.Tier}"), textAlignment: Alignment.Right, font: GUIStyle.Font)
string tierStyle = $"SubmarineTier.{tier}";
if (GUIStyle.GetComponentStyle(tierStyle) != null)
{ {
LocalizedString tooltip = TextManager.Get("submarinetier.description").Fallback(string.Empty); ToolTip = TextManager.Get("submarinetier.description")
new GUIImage(rectT(0.15f, 1, classLayout), style: tierStyle, scaleToFit: false) };
{
ToolTip = tooltip
};
}
var description = new GUITextBlock(rectT(1, 0, submarineInfoFrame), submarine.Info.Description, textAlignment: Alignment.Right, wrap: true); 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)); submarineInfoFrame.RectTransform.ScreenSpaceOffset = new Point(0, (int)(16 * GUI.Scale));
@@ -1750,14 +1752,26 @@ namespace Barotrauma
{ {
if (currentStoreLayout == null) { return; } if (currentStoreLayout == null) { return; }
CategoryData? mostAppropriateCategory = null;
GUIComponent? mostAppropriateChild = null;
foreach (GUIComponent child in currentStoreLayout.Content.Children) foreach (GUIComponent child in currentStoreLayout.Content.Children)
{ {
if (child.UserData is CategoryData data && predicate(data)) if (child.UserData is CategoryData data && predicate(data))
{ {
currentStoreLayout.ScrollToElement(child, playSelectSound); //choose the category with least items in it as the "most appropriate"
break; //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> /// <summary>
@@ -461,7 +461,7 @@ namespace Barotrauma
yield return CoroutineStatus.Running; yield return CoroutineStatus.Running;
UgcTransition.Prepare(); LegacySteamUgcTransition.Prepare();
var contentPackageLoadRoutine = ContentPackageManager.Init(); var contentPackageLoadRoutine = ContentPackageManager.Init();
foreach (var progress in contentPackageLoadRoutine) foreach (var progress in contentPackageLoadRoutine)
{ {
@@ -715,7 +715,7 @@ namespace Barotrauma
} }
#endif #endif
NetworkMember?.Update((float)Timing.Step); Client?.Update((float)Timing.Step);
if (!HasLoaded && !CoroutineManager.IsCoroutineRunning(loadingCoroutine)) 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); GUI.Update((float)Timing.Step);
@@ -1181,7 +1181,7 @@ namespace Barotrauma
{ {
exiting = true; exiting = true;
DebugConsole.NewMessage("Exiting..."); DebugConsole.NewMessage("Exiting...");
NetworkMember?.Quit(); Client?.Quit();
SteamManager.ShutDown(); SteamManager.ShutDown();
try try
@@ -3516,9 +3516,9 @@ namespace Barotrauma
if (node == null || characterContext != null) { return false; } if (node == null || characterContext != null) { return false; }
if (node.UserData is Order nodeOrder) if (node.UserData is Order nodeOrder)
{ {
return !nodeOrder.TargetAllCharacters && !nodeOrder.Prefab.HasOptions && return !nodeOrder.TargetAllCharacters &&
(!nodeOrder.MustSetTarget || itemContext != null || (!nodeOrder.Prefab.HasOptions || !nodeOrder.Option.IsEmpty) &&
nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2); (!nodeOrder.MustSetTarget || itemContext != null || nodeOrder.GetMatchingItems(GetTargetSubmarine(), true, interactableFor: Character.Controlled).Count < 2);
} }
return false; return false;
} }
@@ -1,22 +1,21 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq; using System.Linq;
namespace Barotrauma.Tutorials namespace Barotrauma.Tutorials
{ {
enum AutoPlayVideo { Yes, No }; enum AutoPlayVideo { Yes, No };
enum TutorialSegmentType { MessageBox, InfoBox }; enum TutorialSegmentType { MessageBox, InfoBox, Objective };
class Tutorial sealed class Tutorial
{ {
#region Constants #region Constants
private const string PlayableContentPath = "Content/Tutorials/TutorialVideos/";
private const SpawnType SpawnPointType = SpawnType.Human; private const SpawnType SpawnPointType = SpawnType.Human;
private const float FadeOutTime = 3f; private const float FadeOutTime = 3f;
private const float WaitBeforeFade = 4f; private const float WaitBeforeFade = 4f;
@@ -25,19 +24,11 @@ namespace Barotrauma.Tutorials
#region Tutorial variables #region Tutorial variables
public static ImmutableHashSet<Type> Types;
static Tutorial()
{
Types = ReflectionUtils.GetDerivedNonAbstract<Tutorial>()
.ToImmutableHashSet();
}
public readonly Identifier Identifier; public readonly Identifier Identifier;
public LocalizedString DisplayName { get; } public LocalizedString DisplayName { get; }
public bool ContentRunning { get; protected set; } public bool ContentRunning { get; private set; }
private GUIComponent infoBox; private GUIComponent infoBox;
private Action infoBoxClosedCallback; private Action infoBoxClosedCallback;
@@ -51,47 +42,25 @@ namespace Barotrauma.Tutorials
private readonly LocalizedString objectiveTextTranslated; private readonly LocalizedString objectiveTextTranslated;
private readonly List<Segment> ActiveObjectives = new List<Segment>(); 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; } private Segment ActiveContentSegment { get; set; }
public class Segment 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 string FileName => Path.GetFileName(FullPath.CleanUpPath());
public string ContentPath => Path.GetDirectoryName(FullPath.CleanUpPath());
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) { }
} }
private const int DefaultWidth = 450; private const int DefaultWidth = 450;
@@ -103,15 +72,30 @@ namespace Barotrauma.Tutorials
public LocalizedString ObjectiveText; public LocalizedString ObjectiveText;
public readonly Identifier Id; public readonly Identifier Id;
public readonly Text? TextContent; public readonly Text TextContent;
public readonly Video? VideoContent; public readonly Video VideoContent;
public readonly AutoPlayVideo AutoPlayVideo; public readonly AutoPlayVideo AutoPlayVideo;
public Action OnClickToDisplayMessage; public Action OnClickObjective;
public readonly TutorialSegmentType SegmentType; 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; Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag)); ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
@@ -121,15 +105,21 @@ namespace Barotrauma.Tutorials
SegmentType = TutorialSegmentType.InfoBox; SegmentType = TutorialSegmentType.InfoBox;
} }
public Segment(Identifier id, Action onClickToDisplayMessage) private Segment(Identifier id, Identifier objectiveTextTag, Action onClickObjective)
{ {
Id = id; Id = id;
var objetiveTextTag = $"{id}.objective".ToIdentifier(); ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objetiveTextTag)); OnClickObjective = onClickObjective;
OnClickToDisplayMessage = onClickToDisplayMessage;
SegmentType = TutorialSegmentType.MessageBox; SegmentType = TutorialSegmentType.MessageBox;
} }
}
private Segment(Identifier id, Identifier objectiveTextTag)
{
Id = id;
ObjectiveText = TextManager.ParseInputTypes(TextManager.Get(objectiveTextTag));
SegmentType = TutorialSegmentType.Objective;
}
}
private bool completed; private bool completed;
public bool Completed public bool Completed
@@ -157,60 +147,57 @@ namespace Barotrauma.Tutorials
private Character character; private Character character;
private readonly string submarinePath = "Content/Tutorials/Dugong_Tutorial.sub"; private string SubmarinePath => TutorialPrefab.SubmarinePath.Value;
private readonly string startOutpostPath = "Content/Tutorials/TutorialOutpost.sub"; private string StartOutpostPath => TutorialPrefab.OutpostPath.Value;
private string LevelSeed => TutorialPrefab.LevelSeed;
private readonly string levelSeed = "nLoZLLtza"; private string LevelParams => TutorialPrefab.LevelParams;
private readonly string levelParams = "ColdCavernsTutorial";
private SubmarineInfo startOutpost = null; private SubmarineInfo startOutpost = null;
public readonly List<(Entity entity, string iconStyle)> Icons = new List<(Entity entity, string iconStyle)>();
#endregion #endregion
#region Tutorial Controls #region Tutorial Controls
public Tutorial(TutorialPrefab prefab) public Tutorial(TutorialPrefab prefab)
{ {
Identifier = $"tutorial.{prefab?.Identifier ?? Identifier.Empty}".ToIdentifier(); Identifier = $"tutorial.{prefab.Identifier}".ToIdentifier();
DisplayName = TextManager.Get(Identifier); DisplayName = TextManager.Get(Identifier);
objectiveTextTranslated = TextManager.Get("Tutorial.Objective"); objectiveTextTranslated = TextManager.Get("Tutorial.Objective");
TutorialPrefab = prefab; TutorialPrefab = prefab;
submarinePath = prefab.SubmarinePath.Value;
startOutpostPath = prefab.OutpostPath.Value;
levelSeed = prefab.LevelSeed;
levelParams = prefab.LevelParams;
eventPrefab = EventSet.GetEventPrefab(prefab.EventIdentifier); eventPrefab = EventSet.GetEventPrefab(prefab.EventIdentifier);
} }
private IEnumerable<CoroutineStatus> Loading() 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; yield return CoroutineStatus.Running;
GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null); GameMain.GameSession = new GameSession(subInfo, GameModePreset.Tutorial, missionPrefabs: null);
(GameMain.GameSession.GameMode as TutorialMode).Tutorial = this; (GameMain.GameSession.GameMode as TutorialMode).Tutorial = this;
if (generationParams != null) if (generationParams is not null)
{ {
Biome biome = Biome biome =
Biome.Prefabs.FirstOrDefault(b => generationParams.AllowedBiomeIdentifiers.Contains(b.Identifier)) ?? Biome.Prefabs.FirstOrDefault(b => generationParams.AllowedBiomeIdentifiers.Contains(b.Identifier)) ??
Biome.Prefabs.First(); 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); GameMain.GameSession.StartRound(tutorialLevel, startOutpost: startOutpost);
} }
else else
{ {
GameMain.GameSession.StartRound(levelSeed); GameMain.GameSession.StartRound(LevelSeed);
} }
GameMain.GameSession.EventManager.ActiveEvents.Clear(); GameMain.GameSession.EventManager.ActiveEvents.Clear();
@@ -274,6 +261,7 @@ namespace Barotrauma.Tutorials
private void Initialize() private void Initialize()
{ {
GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch; GameMain.GameSession.CrewManager.AllowCharacterSwitch = TutorialPrefab.AllowCharacterSwitch;
GameMain.GameSession.CrewManager.AutoHideCrewList();
if (Character.Controlled is Character character) if (Character.Controlled is Character character)
{ {
@@ -450,7 +438,7 @@ namespace Barotrauma.Tutorials
public void TriggerTutorialSegment(Segment segment) public void TriggerTutorialSegment(Segment segment)
{ {
if (segment.SegmentType == TutorialSegmentType.MessageBox) if (segment.SegmentType != TutorialSegmentType.InfoBox)
{ {
ActiveObjectives.Add(segment); ActiveObjectives.Add(segment);
AddToObjectiveList(segment); AddToObjectiveList(segment);
@@ -462,7 +450,7 @@ namespace Barotrauma.Tutorials
ActiveContentSegment = segment; ActiveContentSegment = segment;
var title = TextManager.Get(segment.Id); 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); tutorialText = TextManager.ParseInputTypes(tutorialText);
switch (segment.AutoPlayVideo) switch (segment.AutoPlayVideo)
@@ -471,9 +459,9 @@ namespace Barotrauma.Tutorials
infoBox = CreateInfoFrame( infoBox = CreateInfoFrame(
title, title,
tutorialText, tutorialText,
segment.TextContent.Value.Width, segment.TextContent.Width,
segment.TextContent.Value.Height, segment.TextContent.Height,
segment.TextContent.Value.Anchor, segment.TextContent.Anchor,
hasButton: true, hasButton: true,
onInfoBoxClosed: LoadActiveContentVideo); onInfoBoxClosed: LoadActiveContentVideo);
break; break;
@@ -481,9 +469,9 @@ namespace Barotrauma.Tutorials
infoBox = CreateInfoFrame( infoBox = CreateInfoFrame(
title, title,
tutorialText, tutorialText,
segment.TextContent.Value.Width, segment.TextContent.Width,
segment.TextContent.Value.Height, segment.TextContent.Height,
segment.TextContent.Value.Anchor, segment.TextContent.Anchor,
hasButton: true, hasButton: true,
onInfoBoxClosed: StopCurrentContentSegment, onInfoBoxClosed: StopCurrentContentSegment,
onVideoButtonClicked: LoadActiveContentVideo); onVideoButtonClicked: LoadActiveContentVideo);
@@ -493,7 +481,7 @@ namespace Barotrauma.Tutorials
public void CompleteTutorialSegment(Identifier segmentId) 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!"); DebugConsole.AddWarning($"Warning: tried to complete the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!");
return; return;
@@ -501,29 +489,28 @@ namespace Barotrauma.Tutorials
if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style) if (GUIStyle.GetComponentStyle("ObjectiveIndicatorCompleted") is GUIComponentStyle style)
{ {
segment.ObjectiveStateIndicator.ApplyStyle(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.OnClicked = null;
segment.ObjectiveButton.CanBeFocused = false; segment.ObjectiveButton.CanBeFocused = false;
} }
public void RemoveTutorialSegment(Identifier segmentId) 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!"); DebugConsole.AddWarning($"Warning: tried to remove the tutorial segment \"{segmentId}\" in tutorial \"{Identifier}\" but it isn't active!");
return; return;
} }
segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentRemovalTime, false); segment.ObjectiveStateIndicator.FadeOut(ObjectiveComponentAnimationTime, false);
segment.LinkedTextBlock.FadeOut(ObjectiveComponentRemovalTime, false); segment.LinkedTextBlock.FadeOut(ObjectiveComponentAnimationTime, false);
var parent = segment.LinkedTextBlock.Parent; var parent = segment.LinkedTextBlock.Parent;
parent.FadeOut(ObjectiveComponentRemovalTime, true, onRemove: () => parent.FadeOut(ObjectiveComponentAnimationTime, true, onRemove: () =>
{ {
ActiveObjectives.Remove(segment); ActiveObjectives.Remove(segment);
objectiveGroup?.Recalculate(); objectiveGroup?.Recalculate();
}); });
var targetPos = new Point(GameMain.GraphicsWidth - parent.Rect.X, 0); parent.RectTransform.MoveOverTime(GetObjectiveHiddenPosition(parent.RectTransform), ObjectiveComponentAnimationTime);
parent.RectTransform.MoveOverTime(targetPos, ObjectiveComponentRemovalTime);
segment.ObjectiveButton.OnClicked = null; segment.ObjectiveButton.OnClicked = null;
segment.ObjectiveButton.CanBeFocused = false; segment.ObjectiveButton.CanBeFocused = false;
} }
@@ -585,12 +572,14 @@ namespace Barotrauma.Tutorials
{ {
var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform) var frameRt = new RectTransform(new Vector2(1.0f, 0.1f), objectiveGroup.RectTransform)
{ {
AbsoluteOffset = GetObjectiveHiddenPosition(),
MinSize = new Point(0, objectiveGroup.AbsoluteSpacing) MinSize = new Point(0, objectiveGroup.AbsoluteSpacing)
}; };
var frame = new GUIFrame(frameRt, style: null) var frame = new GUIFrame(frameRt, style: null)
{ {
CanBeFocused = true CanBeFocused = true
}; };
objectiveGroup.Recalculate();
segment.LinkedTextBlock = new GUITextBlock( segment.LinkedTextBlock = new GUITextBlock(
new RectTransform(new Point(frameRt.Rect.Width - objectiveGroup.AbsoluteSpacing, 0), frame.RectTransform, anchor: Anchor.TopRight), 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) segment.ObjectiveButton = new GUIButton(new RectTransform(Vector2.One, segment.LinkedTextBlock.RectTransform, Anchor.TopLeft, Pivot.TopLeft), style: null)
{ {
CanBeFocused = segment.SegmentType != TutorialSegmentType.Objective,
ToolTip = objectiveTextTranslated, ToolTip = objectiveTextTranslated,
OnClicked = (GUIButton btn, object userdata) => OnClicked = (GUIButton btn, object userdata) =>
{ {
@@ -628,13 +618,15 @@ namespace Barotrauma.Tutorials
} }
else if (segment.SegmentType == TutorialSegmentType.MessageBox) else if (segment.SegmentType == TutorialSegmentType.MessageBox)
{ {
segment.OnClickToDisplayMessage?.Invoke(); segment.OnClickObjective?.Invoke();
} }
return true; return true;
} }
}; };
SetTransparent(segment.ObjectiveButton); 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; static void SetTransparent(GUIComponent component) => component.Color = component.HoverColor = component.PressedColor = component.SelectedColor = Color.Transparent;
} }
@@ -654,15 +646,20 @@ namespace Barotrauma.Tutorials
ActiveContentSegment = segment; ActiveContentSegment = segment;
infoBox = CreateInfoFrame( infoBox = CreateInfoFrame(
TextManager.Get(segment.Id), TextManager.Get(segment.Id),
TextManager.Get(segment.TextContent.Value.Tag), TextManager.Get(segment.TextContent.Tag),
segment.TextContent.Value.Width, segment.TextContent.Width,
segment.TextContent.Value.Height, segment.TextContent.Height,
segment.TextContent.Value.Anchor, segment.TextContent.Anchor,
hasButton: true, hasButton: true,
onInfoBoxClosed: () => ContentRunning = false, onInfoBoxClosed: () => ContentRunning = false,
onVideoButtonClicked: () => LoadVideo(segment)); onVideoButtonClicked: () => LoadVideo(segment));
} }
private Point GetObjectiveHiddenPosition(RectTransform rt = null)
{
return new Point(GameMain.GraphicsWidth - objectiveGroup.Rect.X, rt?.AbsoluteOffset.Y ?? 0);
}
#endregion #endregion
#region InfoFrame #region InfoFrame
@@ -772,9 +769,9 @@ namespace Barotrauma.Tutorials
if (segment.AutoPlayVideo == AutoPlayVideo.Yes) if (segment.AutoPlayVideo == AutoPlayVideo.Yes)
{ {
videoPlayer.LoadContent( videoPlayer.LoadContent(
contentPath: PlayableContentPath, contentPath: segment.VideoContent.ContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
textSettings: new VideoPlayer.TextSettings(segment.VideoContent.Value.TextTag, segment.VideoContent.Value.Width), textSettings: new VideoPlayer.TextSettings(segment.VideoContent.TextTag, segment.VideoContent.Width),
contentId: segment.Id, contentId: segment.Id,
startPlayback: true, startPlayback: true,
objective: segment.ObjectiveText, objective: segment.ObjectiveText,
@@ -783,8 +780,8 @@ namespace Barotrauma.Tutorials
else else
{ {
videoPlayer.LoadContent( videoPlayer.LoadContent(
contentPath: PlayableContentPath, contentPath: segment.VideoContent.ContentPath,
videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.Value.File), videoSettings: new VideoPlayer.VideoSettings(segment.VideoContent.FileName),
textSettings: null, textSettings: null,
contentId: segment.Id, contentId: segment.Id,
startPlayback: true, startPlayback: true,
@@ -153,7 +153,7 @@ namespace Barotrauma.Items.Components
GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused; GUI.MouseOn == null && !Inventory.IsMouseOnInventory && !GameMain.Instance.Paused;
if (GUI.HideCursor) 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); crosshairPointerSprite?.Draw(spriteBatch, crosshairPointerPos, 0, currentCrossHairPointerScale);
} }
@@ -369,6 +369,7 @@ namespace Barotrauma.Items.Components
loopingSoundChannel = loopingSound.RoundSound.Sound.Play( loopingSoundChannel = loopingSound.RoundSound.Sound.Play(
new Vector3(position.X, position.Y, 0.0f), new Vector3(position.X, position.Y, 0.0f),
0.01f, 0.01f,
freqMult: itemSound.RoundSound.GetRandomFrequencyMultiplier(),
muffle: SoundPlayer.ShouldMuffleSound(Character.Controlled, position, loopingSound.Range, Character.Controlled?.CurrentHull)); muffle: SoundPlayer.ShouldMuffleSound(Character.Controlled, position, loopingSound.Range, Character.Controlled?.CurrentHull));
loopingSoundChannel.Looping = true; loopingSoundChannel.Looping = true;
//TODO: tweak //TODO: tweak
@@ -1013,6 +1013,19 @@ namespace Barotrauma.Items.Components
if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; } if (!CheckResourceMarkerVisibility(c.center, transducerCenter)) { continue; }
var i = unobtainedMinerals.FirstOrDefault(); var i = unobtainedMinerals.FirstOrDefault();
if (i == null) { continue; } 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, DrawMarker(spriteBatch,
i.Name, "mineral".ToIdentifier(), "mineralcluster" + i, i.Name, "mineral".ToIdentifier(), "mineralcluster" + i,
c.center, transducerCenter, c.center, transducerCenter,
@@ -22,6 +22,8 @@ namespace Barotrauma.Items.Components
private GUILayoutGroup extraButtonContainer; private GUILayoutGroup extraButtonContainer;
private GUIComponent skillTextContainer;
private readonly List<ParticleEmitter> particleEmitters = new List<ParticleEmitter>(); private readonly List<ParticleEmitter> particleEmitters = new List<ParticleEmitter>();
//the corresponding particle emitter is active when the condition is within this range //the corresponding particle emitter is active when the condition is within this range
private readonly List<Vector2> particleEmitterConditionRanges = new List<Vector2>(); 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), new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform),
TextManager.Get("RequiredRepairSkills"), font: GUIStyle.SubHeadingFont); TextManager.Get("RequiredRepairSkills"), font: GUIStyle.SubHeadingFont);
skillTextContainer = paddedFrame;
for (int i = 0; i < requiredSkills.Count; i++) 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()), " - " + TextManager.AddPunctuation(':', TextManager.Get("SkillName." + requiredSkills[i].Identifier), ((int) Math.Round(requiredSkills[i].Level * SkillRequirementMultiplier)).ToString()),
font: GUIStyle.SmallFont) font: GUIStyle.SmallFont)
{ {
@@ -359,19 +362,11 @@ namespace Barotrauma.Items.Components
extraButtonContainer.Visible = SabotageButton.Visible || TinkerButton.Visible; extraButtonContainer.Visible = SabotageButton.Visible || TinkerButton.Visible;
extraButtonContainer.IgnoreLayoutGroups = !extraButtonContainer.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; GUITextBlock textBlock = (GUITextBlock)c;
if (character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier)) textBlock.TextColor = character.GetSkillLevel(skill.Identifier) < (skill.Level * SkillRequirementMultiplier) ? GUIStyle.Red : GUIStyle.TextColorNormal;
{
textBlock.TextColor = GUIStyle.Red;
}
else
{
textBlock.TextColor = Color.White;
}
} }
} }
@@ -823,7 +823,7 @@ namespace Barotrauma
reloadTextureButton.OnClicked += (button, data) => reloadTextureButton.OnClicked += (button, data) =>
{ {
Sprite.ReloadXML(); Sprite.ReloadXML();
Sprite.ReloadTexture(updateAllSprites: true); Sprite.ReloadTexture();
return true; return true;
}; };
} }
@@ -163,7 +163,7 @@ namespace Barotrauma
OnClicked = (button, data) => OnClicked = (button, data) =>
{ {
Sprite.ReloadXML(); Sprite.ReloadXML();
Sprite.ReloadTexture(updateAllSprites: true); Sprite.ReloadTexture();
return true; return true;
} }
}; };
@@ -93,10 +93,11 @@ namespace Barotrauma
{ {
float leftPanelWidth = 0.6f; float leftPanelWidth = 0.6f;
float rightPanelWidth = 0.4f / leftPanelWidth; float rightPanelWidth = 0.4f / leftPanelWidth;
LocalizedString className = !HasTag(SubmarineTag.Shuttle) LocalizedString className = !HasTag(SubmarineTag.Shuttle) ?
? $"{TextManager.Get($"submarineclass.{SubmarineClass}")} ({TextManager.Get($"submarinetier.{Tier}")})" TextManager.GetWithVariables("submarine.classandtier",
: TextManager.Get("shuttle"); ("[class]", TextManager.Get($"submarineclass.{SubmarineClass}")),
("[tier]", TextManager.Get($"submarinetier.{Tier}"))) :
TextManager.Get("shuttle");
int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y; int classHeight = (int)GUIStyle.SubHeadingFont.MeasureString(className).Y;
int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth); int leftPanelWidthInt = (int)(parent.Rect.Width * leftPanelWidth);
@@ -134,6 +134,14 @@ namespace Barotrauma.Networking
return Permissions.HasFlag(permission); return Permissions.HasFlag(permission);
} }
public void ResetVotes()
{
for (int i = 0; i < votes.Length; i++)
{
votes[i] = null;
}
}
partial void DisposeProjSpecific() partial void DisposeProjSpecific()
{ {
if (VoipQueue != null) if (VoipQueue != null)
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,8 @@
using Barotrauma.Steam; using Barotrauma.Steam;
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Networking namespace Barotrauma.Networking
{ {
@@ -10,37 +12,14 @@ namespace Barotrauma.Networking
public ImmutableArray<ServerContentPackage> ServerContentPackages { get; set; } = public ImmutableArray<ServerContentPackage> ServerContentPackages { get; set; } =
ImmutableArray<ServerContentPackage>.Empty; ImmutableArray<ServerContentPackage>.Empty;
public delegate void MessageCallback(IReadMessage message); public readonly record struct Callbacks(
Callbacks.MessageCallback OnMessageReceived,
public delegate void DisconnectCallback(bool disableReconnect); Callbacks.DisconnectCallback OnDisconnect,
Callbacks.InitializationCompleteCallback OnInitializationComplete)
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 MessageCallback OnMessageReceived; public delegate void MessageCallback(IReadMessage message);
public readonly DisconnectCallback OnDisconnect; public delegate void DisconnectCallback(PeerDisconnectPacket disconnectPacket);
public readonly DisconnectMessageCallback OnDisconnectMessageReceived; public delegate void InitializationCompleteCallback();
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;
}
} }
protected readonly Callbacks callbacks; protected readonly Callbacks callbacks;
@@ -51,6 +30,8 @@ namespace Barotrauma.Networking
protected readonly bool isOwner; protected readonly bool isOwner;
protected readonly Option<int> ownerKey; protected readonly Option<int> ownerKey;
protected bool isActive;
public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option<int> ownerKey) public ClientPeer(Endpoint serverEndpoint, Callbacks callbacks, Option<int> ownerKey)
{ {
ServerEndpoint = serverEndpoint; ServerEndpoint = serverEndpoint;
@@ -60,7 +41,7 @@ namespace Barotrauma.Networking
} }
public abstract void Start(); 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 Update(float deltaTime);
public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true); public abstract void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true);
public abstract void SendPassword(string password); public abstract void SendPassword(string password);
@@ -71,6 +52,12 @@ namespace Barotrauma.Networking
public bool ContentPackageOrderReceived { get; protected set; } public bool ContentPackageOrderReceived { get; protected set; }
protected int passwordSalt; protected int passwordSalt;
protected Steamworks.AuthTicket? steamAuthTicket; protected Steamworks.AuthTicket? steamAuthTicket;
private GUIMessageBox? passwordMsgBox;
public bool WaitingForPassword
=> isActive && initializationStep == ConnectionInitialization.Password
&& passwordMsgBox != null
&& GUIMessageBox.MessageBoxes.Contains(passwordMsgBox);
public struct IncomingInitializationMessage public struct IncomingInitializationMessage
{ {
@@ -112,8 +99,9 @@ namespace Barotrauma.Networking
} }
case ConnectionInitialization.ContentPackageOrder: case ConnectionInitialization.ContentPackageOrder:
{ {
if (initializationStep == ConnectionInitialization.SteamTicketAndVersion || if (initializationStep
initializationStep == ConnectionInitialization.Password) is ConnectionInitialization.SteamTicketAndVersion
or ConnectionInitialization.Password)
{ {
initializationStep = ConnectionInitialization.ContentPackageOrder; initializationStep = ConnectionInitialization.ContentPackageOrder;
} }
@@ -155,10 +143,57 @@ namespace Barotrauma.Networking
var passwordPacket = INetSerializableStruct.Read<ServerPeerPasswordPacket>(inc.Message); var passwordPacket = INetSerializableStruct.Read<ServerPeerPasswordPacket>(inc.Message);
if (WaitingForPassword) { return; }
passwordPacket.Salt.TryUnwrap(out passwordSalt); passwordPacket.Salt.TryUnwrap(out passwordSalt);
passwordPacket.RetriesLeft.TryUnwrap(out var retries); 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; break;
} }
} }
@@ -9,7 +9,6 @@ namespace Barotrauma.Networking
{ {
internal sealed class LidgrenClientPeer : ClientPeer internal sealed class LidgrenClientPeer : ClientPeer
{ {
private bool isActive;
private NetClient? netClient; private NetClient? netClient;
private readonly NetPeerConfiguration netPeerConfiguration; private readonly NetPeerConfiguration netPeerConfiguration;
@@ -94,7 +93,7 @@ namespace Barotrauma.Networking
if (isOwner && !(ChildServerRelay.Process is { HasExited: false })) if (isOwner && !(ChildServerRelay.Process is { HasExited: false }))
{ {
Close(); Close(PeerDisconnectPacket.WithReason(DisconnectReason.ServerCrashed));
var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage); var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += (btn, obj) => msgBox.Buttons[0].OnClicked += (btn, obj) =>
{ {
@@ -140,8 +139,10 @@ namespace Barotrauma.Networking
var (_, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc); var (_, packetHeader, initialization) = INetSerializableStruct.Read<PeerPacketHeaders>(inc);
if (packetHeader.IsConnectionInitializationStep() && initializationStep != ConnectionInitialization.Success) if (packetHeader.IsConnectionInitializationStep())
{ {
if (initializationStep == ConnectionInitialization.Success) { return; }
ReadConnectionInitializationStep(new IncomingInitializationMessage ReadConnectionInitializationStep(new IncomingInitializationMessage
{ {
InitializationStep = initialization ?? throw new Exception("Initialization step missing"), InitializationStep = initialization ?? throw new Exception("Initialization step missing"),
@@ -170,8 +171,9 @@ namespace Barotrauma.Networking
{ {
case NetConnectionStatus.Disconnected: case NetConnectionStatus.Disconnected:
string disconnectMsg = inc.ReadString(); string disconnectMsg = inc.ReadString();
Close(disconnectMsg); var peerDisconnectPacket =
callbacks.OnDisconnectMessageReceived.Invoke(disconnectMsg); PeerDisconnectPacket.FromLidgrenStringRepresentation(disconnectMsg);
Close(peerDisconnectPacket.Fallback(PeerDisconnectPacket.WithReason(DisconnectReason.Unknown)));
break; break;
} }
} }
@@ -198,7 +200,7 @@ namespace Barotrauma.Networking
SendMsgInternal(headers, body); SendMsgInternal(headers, body);
} }
public override void Close(string? msg = null, bool disableReconnect = false) public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{ {
if (!isActive) { return; } if (!isActive) { return; }
@@ -206,13 +208,13 @@ namespace Barotrauma.Networking
isActive = false; isActive = false;
netClient.Shutdown(msg ?? TextManager.Get("Disconnecting").Value); netClient.Shutdown(peerDisconnectPacket.ToLidgrenStringRepresentation());
netClient = null; netClient = null;
steamAuthTicket?.Cancel(); steamAuthTicket?.Cancel();
steamAuthTicket = null; steamAuthTicket = null;
callbacks.OnDisconnect.Invoke(disableReconnect); callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
} }
public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true) public override void Send(IWriteMessage msg, DeliveryMethod deliveryMethod, bool compressPastThreshold = true)
@@ -10,7 +10,6 @@ namespace Barotrauma.Networking
{ {
internal sealed class SteamP2PClientPeer : ClientPeer internal sealed class SteamP2PClientPeer : ClientPeer
{ {
private bool isActive;
private readonly SteamId hostSteamId; private readonly SteamId hostSteamId;
private double timeout; private double timeout;
private double heartbeatTimer; private double heartbeatTimer;
@@ -97,8 +96,7 @@ namespace Barotrauma.Networking
if (steamId != hostSteamId.Value) { return; } if (steamId != hostSteamId.Value) { return; }
Close($"SteamP2P connection failed: {error}"); Close(PeerDisconnectPacket.SteamP2PError(error));
callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P connection failed: {error}");
} }
private void OnP2PData(ulong steamId, byte[] data, int dataLength) private void OnP2PData(ulong steamId, byte[] data, int dataLength)
@@ -117,8 +115,10 @@ namespace Barotrauma.Networking
if (!packetHeader.IsServerMessage()) { return; } if (!packetHeader.IsServerMessage()) { return; }
if (packetHeader.IsConnectionInitializationStep() && initialization.HasValue) if (packetHeader.IsConnectionInitializationStep())
{ {
if (!initialization.HasValue) { return; }
var relayPacket = INetSerializableStruct.Read<SteamP2PInitializationRelayPacket>(inc); var relayPacket = INetSerializableStruct.Read<SteamP2PInitializationRelayPacket>(inc);
SteamManager.JoinLobby(relayPacket.LobbyID, false); SteamManager.JoinLobby(relayPacket.LobbyID, false);
@@ -127,7 +127,7 @@ namespace Barotrauma.Networking
incomingInitializationMessages.Add(new IncomingInitializationMessage incomingInitializationMessages.Add(new IncomingInitializationMessage
{ {
InitializationStep = initialization.Value, InitializationStep = initialization.Value,
Message = relayPacket.Message.GetReadMessage() Message = relayPacket.Message.GetReadMessageUncompressed()
}); });
} }
} }
@@ -138,13 +138,12 @@ namespace Barotrauma.Networking
else if (packetHeader.IsDisconnectMessage()) else if (packetHeader.IsDisconnectMessage())
{ {
PeerDisconnectPacket packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc); PeerDisconnectPacket packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
Close(packet.Message); Close(packet);
callbacks.OnDisconnectMessageReceived.Invoke(packet.Message);
} }
else else
{ {
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc); 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) if (state.P2PSessionError != Steamworks.P2PSessionError.None)
{ {
Close($"SteamP2P error code: {state.P2PSessionError}"); Close(PeerDisconnectPacket.SteamP2PError(state.P2PSessionError));
callbacks.OnDisconnectMessageReceived.Invoke($"{DisconnectReason.SteamP2PError}/SteamP2P error code: {state.P2PSessionError}");
} }
} }
else else
{ {
Close("SteamP2P connection could not be established"); Close(PeerDisconnectPacket.WithReason(DisconnectReason.Timeout));
callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PError.ToString());
} }
connectionStatusTimer = 1.0f; connectionStatusTimer = 1.0f;
@@ -212,8 +209,7 @@ namespace Barotrauma.Networking
if (timeout < 0.0) if (timeout < 0.0)
{ {
Close("Timed out"); Close(PeerDisconnectPacket.WithReason(DisconnectReason.SteamP2PTimeOut));
callbacks.OnDisconnectMessageReceived.Invoke(DisconnectReason.SteamP2PTimeOut.ToString());
return; return;
} }
@@ -221,20 +217,26 @@ namespace Barotrauma.Networking
{ {
if (incomingDataMessages.Count > 0) if (incomingDataMessages.Count > 0)
{ {
var incomingMessage = incomingDataMessages.First(); void initializationError(string errorMsg, string analyticsTag)
byte incomingHeader = incomingMessage.LengthBytes > 0 ? incomingMessage.PeekByte() : (byte)0;
if (ContentPackageOrderReceived)
{ {
#warning: TODO: do not allow completing initialization until content package order has been received? GameAnalyticsManager.AddErrorEventOnce($"SteamP2PClientPeer.OnInitializationComplete:{analyticsTag}", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
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);
DebugConsole.ThrowError(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) 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}"; initializationError(
GameAnalyticsManager.AddErrorEventOnce("SteamP2PClientPeer.OnInitializationComplete:NoContentPackages", GameAnalyticsManager.ErrorSeverity.Error, errorMsg); errorMsg: "Error during connection initialization: list of content packages enabled on the server was empty when completing initialization.",
DebugConsole.ThrowError(errorMsg); analyticsTag: "NoContentPackages");
return;
} }
callbacks.OnInitializationComplete.Invoke(); callbacks.OnInitializationComplete.Invoke();
initializationStep = ConnectionInitialization.Success; initializationStep = ConnectionInitialization.Success;
@@ -320,7 +322,7 @@ namespace Barotrauma.Networking
SendMsgInternal(headers, body); SendMsgInternal(headers, body);
} }
public override void Close(string? msg = null, bool disableReconnect = false) public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{ {
if (!isActive) { return; } if (!isActive) { return; }
@@ -334,12 +336,7 @@ namespace Barotrauma.Networking
PacketHeader = PacketHeader.IsDisconnectMessage, PacketHeader = PacketHeader.IsDisconnectMessage,
Initialization = null Initialization = null
}; };
var body = new PeerDisconnectPacket SendMsgInternal(headers, peerDisconnectPacket);
{
Message = msg ?? "Disconnected"
};
SendMsgInternal(headers, body);
Thread.Sleep(100); Thread.Sleep(100);
@@ -349,7 +346,7 @@ namespace Barotrauma.Networking
steamAuthTicket?.Cancel(); steamAuthTicket?.Cancel();
steamAuthTicket = null; steamAuthTicket = null;
callbacks.OnDisconnect.Invoke(disableReconnect); callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
} }
protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body) protected override void SendMsgInternal(PeerPacketHeaders headers, INetSerializableStruct? body)
@@ -9,8 +9,6 @@ namespace Barotrauma.Networking
{ {
sealed class SteamP2POwnerPeer : ClientPeer sealed class SteamP2POwnerPeer : ClientPeer
{ {
private bool isActive;
private readonly SteamId selfSteamID; private readonly SteamId selfSteamID;
private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0)); private UInt64 ownerKey64 => unchecked((UInt64)ownerKey.Fallback(0));
@@ -68,6 +66,7 @@ namespace Barotrauma.Networking
: throw new InvalidOperationException("Steamworks not initialized"); : throw new InvalidOperationException("Steamworks not initialized");
} }
public override void Start() public override void Start()
{ {
if (isActive) { return; } if (isActive) { return; }
@@ -93,22 +92,13 @@ namespace Barotrauma.Networking
private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status) private void OnAuthChange(Steamworks.SteamId steamId, Steamworks.SteamId ownerId, Steamworks.AuthResponse status)
{ {
RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId); RemotePeer? remotePeer = remotePeers.Find(p => p.SteamId.Value == steamId);
DebugConsole.Log($"{steamId} validation: {status}, {remotePeer != null}");
if (remotePeer == null) { return; } 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 (status == Steamworks.AuthResponse.OK)
{ {
if (remotePeer.Authenticated) { return; }
SteamId ownerSteamId = new SteamId(ownerId); SteamId ownerSteamId = new SteamId(ownerId);
remotePeer.OwnerSteamId = Option<SteamId>.Some(ownerSteamId); remotePeer.OwnerSteamId = Option<SteamId>.Some(ownerSteamId);
remotePeer.Authenticated = true; remotePeer.Authenticated = true;
@@ -126,7 +116,7 @@ namespace Barotrauma.Networking
} }
else 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); Steamworks.BeginAuthResult authSessionStartState = SteamManager.StartAuthSession(ticket, steamId);
if (authSessionStartState != Steamworks.BeginAuthResult.OK) if (authSessionStartState != Steamworks.BeginAuthResult.OK)
{ {
DisconnectPeer(remotePeer, $"{DisconnectReason.SteamAuthenticationFailed}/ Steam auth session failed to start: {authSessionStartState}"); DisconnectPeer(remotePeer, PeerDisconnectPacket.SteamAuthError(authSessionStartState));
return; return;
} }
} }
@@ -197,9 +187,9 @@ namespace Barotrauma.Networking
{ {
if (!isActive) { return; } 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); var msgBox = new GUIMessageBox(TextManager.Get("ConnectionLost"), ChildServerRelay.CrashMessage);
msgBox.Buttons[0].OnClicked += (btn, obj) => msgBox.Buttons[0].OnClicked += (btn, obj) =>
{ {
@@ -279,7 +269,7 @@ namespace Barotrauma.Networking
if (packetHeader.IsDisconnectMessage()) if (packetHeader.IsDisconnectMessage())
{ {
var packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc); var packet = INetSerializableStruct.Read<PeerDisconnectPacket>(inc);
DisconnectPeer(peer, packet.Message); DisconnectPeer(peer, packet);
return; 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(); IWriteMessage outMsg = new WriteOnlyMessage();
outMsg.WriteNetSerializableStruct(new PeerPacketHeaders 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
{ {
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) private void ClosePeerSession(RemotePeer peer)
@@ -403,7 +383,7 @@ namespace Barotrauma.Networking
//owner doesn't send passwords //owner doesn't send passwords
} }
public override void Close(string? msg = null, bool disableReconnect = false) public override void Close(PeerDisconnectPacket peerDisconnectPacket)
{ {
if (!isActive) { return; } if (!isActive) { return; }
@@ -411,7 +391,7 @@ namespace Barotrauma.Networking
for (int i = remotePeers.Count - 1; i >= 0; i--) for (int i = remotePeers.Count - 1; i >= 0; i--)
{ {
DisconnectPeer(remotePeers[i], msg ?? DisconnectReason.ServerShutdown.ToString()); DisconnectPeer(remotePeers[i], peerDisconnectPacket);
} }
Thread.Sleep(100); Thread.Sleep(100);
@@ -423,7 +403,7 @@ namespace Barotrauma.Networking
ChildServerRelay.ClosePipes(); ChildServerRelay.ClosePipes();
callbacks.OnDisconnect.Invoke(disableReconnect); callbacks.OnDisconnect.Invoke(peerDisconnectPacket);
SteamManager.LeaveLobby(); SteamManager.LeaveLobby();
Steamworks.SteamNetworking.ResetActions(); Steamworks.SteamNetworking.ResetActions();
@@ -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;
}
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
}
}
@@ -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; }
}
}
@@ -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());
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -125,8 +125,6 @@ namespace Barotrauma.Networking
public void ClientRead(IReadMessage incMsg) public void ClientRead(IReadMessage incMsg)
{ {
cachedServerListInfo = null;
NetFlags requiredFlags = (NetFlags)incMsg.ReadByte(); NetFlags requiredFlags = (NetFlags)incMsg.ReadByte();
if (requiredFlags.HasFlag(NetFlags.Name)) if (requiredFlags.HasFlag(NetFlags.Name))
@@ -146,7 +144,6 @@ namespace Barotrauma.Networking
AllowFileTransfers = incMsg.ReadBoolean(); AllowFileTransfers = incMsg.ReadBoolean();
incMsg.ReadPadBits(); incMsg.ReadPadBits();
TickRate = incMsg.ReadRangedInteger(1, 60); TickRate = incMsg.ReadRangedInteger(1, 60);
GameMain.NetworkMember.TickRate = TickRate;
if (requiredFlags.HasFlag(NetFlags.Properties)) if (requiredFlags.HasFlag(NetFlags.Properties))
{ {
@@ -241,7 +238,8 @@ namespace Barotrauma.Networking
outMsg.WriteSingle(levelDifficulty ?? -1000.0f); outMsg.WriteSingle(levelDifficulty ?? -1000.0f);
outMsg.WriteBoolean(useRespawnShuttle ?? UseRespawnShuttle); outMsg.WriteBoolean(useRespawnShuttle != null);
outMsg.WriteBoolean(useRespawnShuttle ?? false);
outMsg.WriteBoolean(autoRestart != null); outMsg.WriteBoolean(autoRestart != null);
outMsg.WriteBoolean(autoRestart ?? false); outMsg.WriteBoolean(autoRestart ?? false);
@@ -1057,15 +1055,7 @@ namespace Barotrauma.Networking
} }
settingsFrame = null; settingsFrame = null;
} }
return false; return false;
} }
private ServerInfo cachedServerListInfo = null;
public ServerInfo GetServerListInfo()
{
cachedServerListInfo ??= GameMain.ServerListScreen.UpdateServerInfoWithServerSettings(GameMain.Client.ClientPeer.ServerConnection, this);
return cachedServerListInfo;
}
} }
} }
@@ -3,6 +3,7 @@ using Microsoft.Xna.Framework;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using System;
namespace Barotrauma 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) public void ClientWrite(IWriteMessage msg, VoteType voteType, object data)
{ {
msg.WriteByte((byte)voteType); msg.WriteByte((byte)voteType);
@@ -67,6 +67,8 @@ namespace Barotrauma
public static readonly Queue<ulong> WorkshopItemsToUpdate = new Queue<ulong>(); public static readonly Queue<ulong> WorkshopItemsToUpdate = new Queue<ulong>();
private readonly GUIListBox tutorialList;
#region Creation #region Creation
public MainMenuScreen(GameMain game) 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 }); menuTabs[Tab.Tutorials] = new GUIFrame(new RectTransform(relativeSize, GUI.Canvas, anchor, pivot, minSize, maxSize) { RelativeOffset = relativeSpacing });
//PLACEHOLDER //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) }) new RectTransform(new Vector2(0.95f, 0.85f), menuTabs[Tab.Tutorials].RectTransform, Anchor.TopCenter) { RelativeOffset = new Vector2(0.0f, 0.1f) })
{ {
PlaySoundOnSelect = true, 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) => tutorialList.OnSelected += (component, obj) =>
{ {
(obj as Tutorial)?.Start(); (obj as Tutorial)?.Start();
return true; return true;
}; };
CreateTutorialButtons();
this.game = game; this.game = game;
menuTabs[Tab.Credits] = new GUIFrame(new RectTransform(Vector2.One, GUI.Canvas, Anchor.Center), style: null) 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); 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"); 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 #region Selection
public override void Select() 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); 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), 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 UserData = PlayStyle.Serious
}; };
@@ -1336,12 +1352,15 @@ namespace Barotrauma
private void SetServerPlayStyle(PlayStyle playStyle) private void SetServerPlayStyle(PlayStyle playStyle)
{ {
playstyleBanner.Sprite = ServerListScreen.PlayStyleBanners[(int)playStyle]; playstyleBanner.Sprite = GUIStyle
.GetComponentStyle($"PlayStyleBanner.{playStyle}")
.GetSprite(GUIComponent.ComponentState.None);
playstyleBanner.UserData = playStyle; playstyleBanner.UserData = playStyle;
var nameText = playstyleBanner.GetChild<GUITextBlock>(); var nameText = playstyleBanner.GetChild<GUITextBlock>();
nameText.Text = TextManager.AddPunctuation(':', TextManager.Get("serverplaystyle"), TextManager.Get("servertag." + playStyle)); 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(); nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint();
playstyleDescription.Text = TextManager.Get("servertagdescription." + playStyle); playstyleDescription.Text = TextManager.Get("servertagdescription." + playStyle);
@@ -92,7 +92,7 @@ namespace Barotrauma
var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages var missingPackages = GameMain.Client.ClientPeer.ServerContentPackages
.Where(sp => sp.ContentPackage is null).ToArray(); .Where(sp => sp.ContentPackage is null).ToArray();
if (!missingPackages.Any()) if (!missingPackages.Any(p => p.IsMandatory))
{ {
if (!GameMain.Client.IsServerOwner) if (!GameMain.Client.IsServerOwner)
{ {
@@ -106,7 +106,7 @@ namespace Barotrauma
.Select(p => p.RegularPackage) .Select(p => p.RegularPackage)
.OfType<RegularPackage>().ToList(); .OfType<RegularPackage>().ToList();
//keep enabled client-side-only mods enabled //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); ContentPackageManager.EnabledPackages.SetRegular(regularPackages);
} }
GameMain.NetLobbyScreen.Select(); GameMain.NetLobbyScreen.Select();
@@ -177,11 +177,11 @@ namespace Barotrauma
}); });
buttonContainerSpacing(0.1f); buttonContainerSpacing(0.1f);
var missingIds = missingPackages.Where( var missingIds = missingPackages
mp => mp.WorkshopId != 0 .Where(p => p.IsMandatory)
&& ContentPackageManager.WorkshopPackages.All(wp .Select(mp => ContentPackageId.Parse(mp.UgcId))
=> wp.SteamWorkshopId != mp.WorkshopId)) .NotNone()
.Select(mp => mp.WorkshopId) .Where(id => ContentPackageManager.WorkshopPackages.All(wp => !wp.UgcId.Equals(id)))
.ToArray(); .ToArray();
if (missingIds.Any() && SteamManager.IsInitialized) if (missingIds.Any() && SteamManager.IsInitialized)
{ {
@@ -191,7 +191,7 @@ namespace Barotrauma
{ {
if (GameMain.Client != null) if (GameMain.Client != null)
{ {
BulkDownloader.SubscribeToServerMods(missingIds, BulkDownloader.SubscribeToServerMods(missingIds.OfType<SteamWorkshopId>().Select(id => id.Value),
new ConnectCommand( new ConnectCommand(
serverName: GameMain.Client.ServerName, serverName: GameMain.Client.ServerName,
endpoint: GameMain.Client.ClientPeer.ServerEndpoint)); endpoint: GameMain.Client.ClientPeer.ServerEndpoint));
@@ -202,7 +202,7 @@ namespace Barotrauma
buttonContainerSpacing(0.15f); buttonContainerSpacing(0.15f);
} }
foreach (var p in missingPackages) foreach (var p in missingPackages.Where(p => p.IsMandatory))
{ {
pendingDownloads.Enqueue(p); pendingDownloads.Enqueue(p);
@@ -294,26 +294,50 @@ namespace Barotrauma
?? serverPackages.FirstOrDefault(p => p.CorePackage != null) ?? serverPackages.FirstOrDefault(p => p.CorePackage != null)
?.CorePackage ?.CorePackage
?? throw new Exception($"Failed to find core package to enable"); ?? throw new Exception($"Failed to find core package to enable");
List<RegularPackage> regularPackages
= serverPackages.Where(p => p.CorePackage is null) List<RegularPackage> regularPackages = new List<RegularPackage>();
.Select(p => foreach (var p in serverPackages)
p.RegularPackage {
?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) if (p.CorePackage != null) { continue; }
?? throw new Exception($"Could not find regular package \"{p.Name}\"")) RegularPackage? matchingPackage =
.Cast<RegularPackage>() p.RegularPackage ?? downloadedPackages.FirstOrDefault(d => d is RegularPackage && d.Hash.Equals(p.Hash)) as RegularPackage;
.ToList(); 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) foreach (var regularPackage in regularPackages)
{ {
DebugConsole.NewMessage($"Enabling \"{regularPackage.Name}\" ({regularPackage.Dir})", Color.Lime); DebugConsole.NewMessage($"Enabling \"{regularPackage.Name}\" ({regularPackage.Dir})", Color.Lime);
} }
//keep enabled client-side-only mods enabled //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.BackUp();
ContentPackageManager.EnabledPackages.SetCore(corePackage); ContentPackageManager.EnabledPackages.SetCore(corePackage);
ContentPackageManager.EnabledPackages.SetRegular(regularPackages); 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(); GameMain.NetLobbyScreen.Select();
} }
} }
@@ -666,7 +666,7 @@ namespace Barotrauma
OnSelected = (tickbox) => OnSelected = (tickbox) =>
{ {
if (GameMain.Client == null) { return true; } if (GameMain.Client == null) { return true; }
ServerInfo info = GameMain.Client.ServerSettings.GetServerListInfo(); ServerInfo info = GameMain.Client.CreateServerInfoFromSettings();
if (tickbox.Selected) if (tickbox.Selected)
{ {
GameMain.ServerListScreen.AddToFavoriteServers(info); 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; bool nameChangePending = isGameRunning && GameMain.Client.PendingName != string.Empty && GameMain.Client?.Character?.Name != GameMain.Client.PendingName;
changesPendingText = null; changesPendingText = null;
if (isGameRunning)
{
infoContainer.RectTransform.AbsoluteOffset = new Point(0, (int)(parent.Rect.Height * 0.025f));
}
if (TabMenu.PendingChanges) if (TabMenu.PendingChanges)
{ {
@@ -1454,7 +1450,6 @@ namespace Barotrauma
{ {
if (GameMain.Client == null) { return; } if (GameMain.Client == null) { return; }
string newName = Client.SanitizeName(tb.Text); string newName = Client.SanitizeName(tb.Text);
newName = newName.Replace(":", "").Replace(";", "");
if (newName == GameMain.Client.Name) return; if (newName == GameMain.Client.Name) return;
if (string.IsNullOrWhiteSpace(newName)) if (string.IsNullOrWhiteSpace(newName))
{ {
@@ -1782,6 +1777,10 @@ namespace Barotrauma
// Hide spectate tickbox if spectating is not allowed // Hide spectate tickbox if spectating is not allowed
spectateBox.Visible = allowSpectating; 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) public void SetAutoRestart(bool enabled, float timer = 0.0f)
@@ -2753,25 +2752,24 @@ namespace Barotrauma
if (GameMain.NetworkMember?.ServerSettings == null) { return; } if (GameMain.NetworkMember?.ServerSettings == null) { return; }
PlayStyle playStyle = GameMain.NetworkMember.ServerSettings.PlayStyle; 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; float scale = component.Rect.Width / sprite.size.X;
sprite.Draw(spriteBatch, component.Center, scale: scale); sprite.Draw(spriteBatch, component.Center, scale: scale);
if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value) if (!prevPlayStyle.HasValue || playStyle != prevPlayStyle.Value)
{ {
var nameText = component.GetChild<GUITextBlock>(); var nameText = component.GetChild<GUITextBlock>();
nameText.Text = TextManager.Get("servertag." + playStyle); nameText.Text = TextManager.Get($"ServerTag.{playStyle}");
nameText.Color = ServerListScreen.PlayStyleColors[(int)playStyle]; nameText.Color = sprite.SourceElement.GetAttributeColor("BannerColor") ?? Color.White;
nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint(); nameText.RectTransform.NonScaledSize = (nameText.Font.MeasureString(nameText.Text) + new Vector2(25, 10) * GUI.Scale).ToPoint();
prevPlayStyle = playStyle; 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(); publicOrPrivate.RectTransform.NonScaledSize = (publicOrPrivate.Font.MeasureString(publicOrPrivate.Text) + new Vector2(25, 8) * GUI.Scale).ToPoint();
File diff suppressed because it is too large Load Diff
@@ -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);
}
}
}
@@ -1547,6 +1547,8 @@ namespace Barotrauma
GUI.ForceMouseOn(null); GUI.ForceMouseOn(null);
if (ImageManager.EditorMode) { GameSettings.SaveCurrentConfig(); }
MapEntityPrefab.Selected = null; MapEntityPrefab.Selected = null;
saveFrame = null; saveFrame = null;
@@ -1797,25 +1799,14 @@ namespace Barotrauma
{ {
Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player); 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(); if (Submarine.MainSub?.Info == null) { return null; }
packagePath = packagePath.CleanUpPath(); if (package.Files.Any(f => f.Path == MainSub.Info.FilePath && Path.GetFileName(f.Path.Value) == fileName))
string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false);
if (filePath.StartsWith(packageDir))
{ {
filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}"; return MainSub.Info.FilePath;
} }
if (!modProject.Files.Any(f => f.Type == subFileType && return null;
f.Path == filePath))
{
var newFile = ModProject.File.FromPath(filePath, subFileType);
modProject.AddFile(newFile);
}
using var _ = Validation.SkipInDebugBuilds();
modProject.DiscardHashAndInstallTime();
modProject.Save(packagePath);
} }
if (!GameMain.DebugDraw) if (!GameMain.DebugDraw)
@@ -1861,101 +1852,139 @@ namespace Barotrauma
#if !DEBUG #if !DEBUG
throw new InvalidOperationException("Cannot save to Vanilla package"); throw new InvalidOperationException("Cannot save to Vanilla package");
#endif #endif
savePath = string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch savePath =
{ getExistingFilePath(packageToSaveTo, savePath) ??
SubmarineType.Player => "Content/Submarines/{0}", string.Format((MainSub?.Info.Type ?? SubmarineType.Player) switch
SubmarineType.Outpost => "Content/Map/Outposts/{0}", {
SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore... SubmarineType.Player => "Content/Submarines/{0}",
SubmarineType.Wreck => "Content/Map/Wrecks/{0}", SubmarineType.Outpost => "Content/Map/Outposts/{0}",
SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}", SubmarineType.Ruin => "Content/Submarines/{0}", //we don't seem to use this anymore...
SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}", SubmarineType.Wreck => "Content/Map/Wrecks/{0}",
SubmarineType.OutpostModule => "Content/Map/Outposts/{0}", SubmarineType.BeaconStation => "Content/Map/BeaconStations/{0}",
_ => throw new InvalidOperationException() SubmarineType.EnemySubmarine => "Content/Map/EnemySubmarines/{0}",
}, savePath); SubmarineType.OutpostModule => "Content/Map/Outposts/{0}",
_ => throw new InvalidOperationException()
}, savePath);
modProject.ModVersion = ""; modProject.ModVersion = "";
} }
else 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); addSubAndSave(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);
} }
else else
{ {
savePath = Path.Combine(newLocalModDir, savePath); savePath = Path.Combine(newLocalModDir, savePath);
ModProject modProject = new ModProject { Name = name }; if (File.Exists(savePath))
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)
{ {
bool savePreviewImage = true; new GUIMessageBox(TextManager.Get("warning"), TextManager.GetWithVariable("subeditor.packagealreadyexists", "[name]", name));
using System.IO.MemoryStream imgStream = new System.IO.MemoryStream(); return false;
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);
} }
else 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(); void addSubAndSave(ModProject modProject, string filePath, string packagePath)
{
GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green); filePath = filePath.CleanUpPath();
packagePath = packagePath.CleanUpPath();
if (savePath.StartsWith(newLocalModDir)) string packageDir = Path.GetDirectoryName(packagePath).CleanUpPathCrossPlatform(correctFilenameCase: false);
if (filePath.StartsWith(packageDir))
{ {
ContentPackageManager.LocalPackages.Refresh(); filePath = $"{ContentPath.ModDirStr}/{filePath[packageDir.Length..]}";
var newPackage = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.Path.StartsWith(newLocalModDir)); }
if (newPackage is RegularPackage regular) 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); bool savePreviewImage = true;
GameSettings.SaveCurrentConfig(); 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);
} }
} else
if (packageToSaveTo != null) { ReloadModifiedPackage(packageToSaveTo); } {
SubmarineInfo.RefreshSavedSub(savePath); MainSub.TrySaveAs(savePath);
if (prevSavePath != null && prevSavePath != savePath) { SubmarineInfo.RefreshSavedSub(prevSavePath); } }
MainSub.Info.PreviewImage = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.FilePath == savePath)?.PreviewImage; Barotrauma.IO.Validation.SkipValidationInDebugBuilds = false;
string downloadFolder = Path.GetFullPath(SaveUtil.SubmarineDownloadFolder); MainSub.CheckForErrors();
linkedSubBox.ClearChildren();
foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) GUI.AddMessage(TextManager.GetWithVariable("SubSavedNotification", "[filepath]", savePath), GUIStyle.Green);
{
if (sub.Type != SubmarineType.Player) { continue; } if (savePath.StartsWith(newLocalModDir))
if (Path.GetDirectoryName(Path.GetFullPath(sub.FilePath)) == downloadFolder) { continue; } {
linkedSubBox.AddItem(sub.Name, sub); 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; return false;
@@ -2735,40 +2764,31 @@ namespace Barotrauma
new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform), new GUICustomComponent(new RectTransform(Vector2.Zero, saveInPackageLayout.RectTransform),
onUpdate: (f, component) => onUpdate: (f, component) =>
{ {
bool canCreateNewPackage = true;
foreach (GUIComponent contentChild in packageToSaveInList.Content.Children) 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 && contentChild.Visible &= !(contentChild.GetChild<GUILayoutGroup>()?.GetChild<GUITextBlock>() is GUITextBlock tb &&
!tb.Text.Contains(packToSaveInFilter.Text, StringComparison.OrdinalIgnoreCase)); !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; ContentPackage ownerPkg = null;
if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); } if (MainSub?.Info != null) { ownerPkg = GetLocalPackageThatOwnsSub(MainSub.Info); }
foreach (var p in ContentPackageManager.LocalPackages) 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)
if (ownerPkg != null && !string.Equals(ownerPkg.Name, nameBox.Text, StringComparison.OrdinalIgnoreCase))
{ {
packageToSaveInList.Select(ownerPkg); var element = packageToSaveInList.Content.FindChild(ownerPkg);
packageToSaveInList.ScrollToElement(packageToSaveInList.SelectedComponent); element?.RectTransform.SetAsFirstChild();
} }
packageToSaveInList.Select(0);
var requiredContentPackagesLayout = new GUILayoutGroup(new RectTransform(Vector2.One, var requiredContentPackagesLayout = new GUILayoutGroup(new RectTransform(Vector2.One,
horizontalArea.RectTransform, Anchor.BottomRight)) horizontalArea.RectTransform, Anchor.BottomRight))
@@ -3424,7 +3444,8 @@ namespace Barotrauma
{ {
if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage) if (GetWorkshopPackageThatOwnsSub(selectedSubInfo) is ContentPackage workshopPackage)
{ {
if (publishedWorkshopItemIds.Contains(workshopPackage.SteamWorkshopId)) if (workshopPackage.TryExtractSteamWorkshopId(out var workshopId)
&& publishedWorkshopItemIds.Contains(workshopId.Value))
{ {
AskLoadPublishedSub(selectedSubInfo, workshopPackage); AskLoadPublishedSub(selectedSubInfo, workshopPackage);
} }
@@ -380,6 +380,11 @@ namespace Barotrauma
} }
LocalizedString toolTip = TextManager.Get($"sp.{propertyTag}.description"); 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()) if (toolTip.IsNullOrEmpty())
{ {
toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description"); toolTip = TextManager.Get($"{propertyTag}.description", $"sp.{fallbackTag}.description");
@@ -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()
public void ReloadTexture(IEnumerable<Sprite> spritesToUpdate)
{ {
var oldTexture = texture;
texture.Dispose(); texture.Dispose();
texture = TextureLoader.FromFile(FilePath.Value, Compress); 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;
}
} }
} }
@@ -70,7 +70,9 @@ namespace Barotrauma.Steam
(ContentPackage Package, bool IsUpToDate)[] outOfDatePackages = await Task.WhenAll(determiningTasks); (ContentPackage Package, bool IsUpToDate)[] outOfDatePackages = await Task.WhenAll(determiningTasks);
return (await Task.WhenAll(outOfDatePackages.Where(p => !p.IsUpToDate) 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(); .Where(p => p.HasValue).Select(p => p ?? default).ToArray();
} }
@@ -1,7 +1,5 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -102,7 +100,7 @@ namespace Barotrauma.Steam
currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name))); currentLobby?.SetData("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
currentLobby?.SetData("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); 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("modeselectionmode", serverSettings.ModeSelectionMode.ToString());
currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString()); currentLobby?.SetData("subselectionmode", serverSettings.SubSelectionMode.ToString());
currentLobby?.SetData("voicechatenabled", serverSettings.VoiceChatEnabled.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();
}
}
} }
} }
@@ -196,7 +196,7 @@ namespace Barotrauma.Steam
throw new Exception("Expected Workshop package"); 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}"); throw new Exception($"Steam Workshop ID not set for {contentPackage.Name}");
} }
@@ -210,7 +210,7 @@ namespace Barotrauma.Steam
string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}"; string newPath = $"{ContentPackage.LocalModsDir}/{sanitizedName}";
if (File.Exists(newPath) || Directory.Exists(newPath)) if (File.Exists(newPath) || Directory.Exists(newPath))
{ {
newPath += $"_{contentPackage.SteamWorkshopId}"; newPath += $"_{workshopId.Value}";
} }
if (File.Exists(newPath) || Directory.Exists(newPath)) if (File.Exists(newPath) || Directory.Exists(newPath))
@@ -226,7 +226,7 @@ namespace Barotrauma.Steam
RefreshLocalMods(); RefreshLocalMods();
return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == contentPackage.SteamWorkshopId); return ContentPackageManager.LocalPackages.FirstOrDefault(p => p.UgcId == contentPackage.UgcId);
} }
private struct InstallWaiter private struct InstallWaiter
@@ -266,7 +266,10 @@ namespace Barotrauma.Steam
{ {
NukeDownload(workshopItem); NukeDownload(workshopItem);
var toUninstall 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(); .ToHashSet();
toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d)); toUninstall.Select(p => p.Dir).ForEach(d => Directory.Delete(d));
CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.WorkshopPackages.Refresh()); CrossThread.RequestExecutionOnMainThread(() => ContentPackageManager.WorkshopPackages.Refresh());
@@ -296,7 +299,10 @@ namespace Barotrauma.Steam
return; return;
} }
else if (CanBeInstalled(id) 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)) && !InstallTaskCounter.IsInstalling(id))
{ {
TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id)); TaskPool.Add($"InstallItem{id}", InstallMod(id), t => InstallWaiter.StopWaiting(id));
@@ -35,7 +35,12 @@ namespace Barotrauma.Steam
memSubscribedModCount = numSubscribedMods; memSubscribedModCount = numSubscribedMods;
var subscribedIds = SteamManager.GetSubscribedItems().ToHashSet(); 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))) foreach (var id in subscribedIds.Where(id2 => !installedIds.Contains(id2)))
{ {
Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id); Steamworks.Ugc.Item item = new Steamworks.Ugc.Item(id);
@@ -514,7 +519,9 @@ namespace Barotrauma.Steam
private void PrepareToShowModInfo(ContentPackage mod) 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 => t =>
{ {
if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; } if (!t.TryGetResult(out Steamworks.Ugc.Item? item)) { return; }
@@ -592,7 +599,12 @@ namespace Barotrauma.Steam
isEnabled: true, isEnabled: true,
onSelected: () => 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 => t =>
{ {
if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; } if (!t.TryGetResult(out Steamworks.Ugc.Item?[] items)) { return; }
@@ -672,7 +684,7 @@ namespace Barotrauma.Steam
infoButton.Enabled = false; infoButton.Enabled = false;
} }
TaskPool.AddIfNotFound( TaskPool.AddIfNotFound(
$"DetermineUpdateRequired{mod.SteamWorkshopId}", $"DetermineUpdateRequired{mod.UgcId}",
mod.IsUpToDate(), mod.IsUpToDate(),
t => t =>
{ {
@@ -725,6 +737,8 @@ namespace Barotrauma.Steam
{ {
var mod = child.UserData as RegularPackage; var mod = child.UserData as RegularPackage;
if (mod is null || !ContentPackageManager.WorkshopPackages.Contains(mod)) { continue; } 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(); var btn = child.GetChild<GUILayoutGroup>()?.GetAllChildren<GUIButton>().Last();
if (btn is null) { continue; } if (btn is null) { continue; }
@@ -732,11 +746,11 @@ namespace Barotrauma.Steam
btn.ApplyStyle( btn.ApplyStyle(
GUIStyle.GetComponentStyle( GUIStyle.GetComponentStyle(
ids.Contains(mod.SteamWorkshopId) ids.Contains(workshopId.Value)
? "WorkshopMenu.PublishedIcon" ? "WorkshopMenu.PublishedIcon"
: "WorkshopMenu.DownloadedIcon")); : "WorkshopMenu.DownloadedIcon"));
btn.ToolTip = TextManager.Get( btn.ToolTip = TextManager.Get(
ids.Contains(mod.SteamWorkshopId) ids.Contains(workshopId.Value)
? "PublishedWorkshopMod" ? "PublishedWorkshopMod"
: "DownloadedWorkshopMod"); : "DownloadedWorkshopMod");
btn.HoverCursor = CursorState.Default; btn.HoverCursor = CursorState.Default;
@@ -226,7 +226,7 @@ namespace Barotrauma.Steam
(Steamworks.Ugc.Item WorkshopItem, ContentPackage? LocalPackage)[] publishedItems = workshopItems (Steamworks.Ugc.Item WorkshopItem, ContentPackage? LocalPackage)[] publishedItems = workshopItems
.Select(item => (item, .Select(item => (item,
(ContentPackage?)ContentPackageManager.LocalPackages.FirstOrDefault(p (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 //Sort the pairs by last local edit time if available
.OrderBy(t => t.Item2 == null) .OrderBy(t => t.Item2 == null)
.ThenByDescending(t => t.Item2 is { } p ? getEditTime(p) : t.Item1.LatestUpdateTime) .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 //Get mods that haven't been published and add them to the list
var unpublishedMods = ContentPackageManager.LocalPackages 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(); .OrderByDescending(getEditTime).ToArray();
if (unpublishedMods.Any()) if (unpublishedMods.Any())
@@ -556,7 +558,9 @@ namespace Barotrauma.Steam
taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc; taskCancelSrc = taskCancelSrc.IsCancellationRequested ? new CancellationTokenSource() : taskCancelSrc;
var contentPackage 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)); var verticalLayout = new GUILayoutGroup(new RectTransform(Vector2.One, parentFrame.RectTransform));
@@ -619,7 +623,7 @@ namespace Barotrauma.Steam
if (contentPackage != null) if (contentPackage != null)
{ {
TaskPool.AddIfNotFound( TaskPool.AddIfNotFound(
$"DetermineUpdateRequired{contentPackage.SteamWorkshopId}", $"DetermineUpdateRequired{contentPackage.UgcId}",
contentPackage.IsUpToDate(), contentPackage.IsUpToDate(),
t => t =>
{ {
@@ -652,7 +656,9 @@ namespace Barotrauma.Steam
if (contentPackage != null if (contentPackage != null
&& !ContentPackageManager.WorkshopPackages.Contains(contentPackage) && !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; updateButton.Visible = false;
} }
@@ -6,6 +6,7 @@ using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.IO; using Barotrauma.IO;
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Barotrauma.Steam;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
namespace Barotrauma namespace Barotrauma
@@ -49,21 +50,18 @@ namespace Barotrauma
case ModType.Workshop: case ModType.Workshop:
{ {
var id = element.GetAttributeUInt64("id", 0); var id = element.GetAttributeUInt64("id", 0);
var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == id); if (id == 0) { continue; }
if (id != 0 && pkg != null) var pkg = ContentPackageManager.WorkshopPackages.FirstOrDefault(p =>
{ p.TryExtractSteamWorkshopId(out var workshopId) && workshopId.Value == id);
addPkg(pkg); if (pkg != null) { addPkg(pkg); }
}
} }
break; break;
case ModType.Local: case ModType.Local:
{ {
var name = element.GetAttributeString("name", ""); var name = element.GetAttributeString("name", "");
if (name.IsNullOrEmpty()) { continue; }
var pkg = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.NameMatches(name)); var pkg = ContentPackageManager.LocalPackages.FirstOrDefault(p => p.NameMatches(name));
if (!name.IsNullOrEmpty() && pkg != null) if (pkg != null) { addPkg(pkg); }
{
addPkg(pkg);
}
} }
break; break;
} }
@@ -115,7 +113,7 @@ namespace Barotrauma
{ {
case ModType.Workshop: case ModType.Workshop:
pkgElem.SetAttributeValue("name", pkg.Name); pkgElem.SetAttributeValue("name", pkg.Name);
pkgElem.SetAttributeValue("id", pkg.SteamWorkshopId.ToString()); pkgElem.SetAttributeValue("id", pkg.UgcId.ToString());
break; break;
case ModType.Local: case ModType.Local:
pkgElem.SetAttributeValue("name", pkg.Name); pkgElem.SetAttributeValue("name", pkg.Name);
@@ -95,6 +95,9 @@ namespace Barotrauma.Steam
SelectTab(Tab.Publish); 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) private void PopulatePublishTab(ItemOrPackage itemOrPackage, GUIFrame parentFrame)
{ {
ContentPackageManager.LocalPackages.Refresh(); ContentPackageManager.LocalPackages.Refresh();
@@ -105,18 +108,19 @@ namespace Barotrauma.Steam
childAnchor: Anchor.TopCenter); childAnchor: Anchor.TopCenter);
Steamworks.Ugc.Item workshopItem = itemOrPackage.TryGet(out Steamworks.Ugc.Item item) ? item : default; Steamworks.Ugc.Item workshopItem = itemOrPackage.TryGet(out Steamworks.Ugc.Item item) ? item : default;
ContentPackage? localPackage = itemOrPackage.TryGet(out ContentPackage package) ContentPackage? localPackage = itemOrPackage.TryGet(out ContentPackage package)
? package ? package
: ContentPackageManager.LocalPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); : ContentPackageManager.LocalPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem));
ContentPackage? workshopPackage ContentPackage? workshopPackage
= ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); = ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem));
if (localPackage is null) if (localPackage is null)
{ {
new GUIFrame(new RectTransform((1.0f, 0.15f), mainLayout.RectTransform), style: null); new GUIFrame(new RectTransform((1.0f, 0.15f), mainLayout.RectTransform), style: null);
//Local copy does not exist; check for Workshop copy //Local copy does not exist; check for Workshop copy
bool workshopCopyExists = 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), new GUITextBlock(new RectTransform((0.7f, 0.4f), mainLayout.RectTransform),
TextManager.Get(workshopCopyExists ? "LocalCopyRequired" : "ItemInstallRequired"), TextManager.Get(workshopCopyExists ? "LocalCopyRequired" : "ItemInstallRequired"),
@@ -403,7 +407,7 @@ namespace Barotrauma.Steam
private IEnumerable<CoroutineStatus> CreateLocalCopy(GUITextBlock currentStepText, Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame) private IEnumerable<CoroutineStatus> CreateLocalCopy(GUITextBlock currentStepText, Steamworks.Ugc.Item workshopItem, GUIFrame parentFrame)
{ {
ContentPackage? workshopCopy = ContentPackage? workshopCopy =
ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == workshopItem.Id); ContentPackageManager.WorkshopPackages.FirstOrDefault(p => PackageMatchesItem(p, workshopItem));
if (workshopCopy is null) if (workshopCopy is null)
{ {
if (!SteamManager.Workshop.CanBeInstalled(workshopItem)) if (!SteamManager.Workshop.CanBeInstalled(workshopItem))
@@ -417,7 +421,7 @@ namespace Barotrauma.Steam
{ {
ContentPackageManager.WorkshopPackages.Refresh(); 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) currentStepText.Text = SteamManager.Workshop.CanBeInstalled(workshopItem)
? TextManager.Get("PublishPopupInstall") ? TextManager.Get("PublishPopupInstall")
@@ -426,7 +430,7 @@ namespace Barotrauma.Steam
} }
workshopCopy = workshopCopy =
ContentPackageManager.WorkshopPackages.First(p => p.SteamWorkshopId == workshopItem.Id); ContentPackageManager.WorkshopPackages.First(p => PackageMatchesItem(p, workshopItem));
} }
bool localCopyMade = false; bool localCopyMade = false;
@@ -480,7 +484,7 @@ namespace Barotrauma.Steam
messageBox.Buttons[0].Enabled = false; messageBox.Buttons[0].Enabled = false;
Steamworks.Ugc.PublishResult? result = null; Steamworks.Ugc.PublishResult? result = null;
Exception? resultException = null; Exception? resultException = null;
TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.SteamWorkshopId})", TaskPool.Add($"Publishing {localPackage.Name} ({localPackage.UgcId})",
editor.SubmitAsync(), editor.SubmitAsync(),
t => t =>
{ {
@@ -496,6 +500,8 @@ namespace Barotrauma.Steam
if (result is { Success: true }) if (result is { Success: true })
{ {
var resultId = result.Value.FileId; 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); Steamworks.Ugc.Item resultItem = new Steamworks.Ugc.Item(resultId);
Task downloadTask = SteamManager.Workshop.ForceRedownload(resultItem); Task downloadTask = SteamManager.Workshop.ForceRedownload(resultItem);
while (!resultItem.IsInstalled && !downloadTask.IsCompleted) while (!resultItem.IsInstalled && !downloadTask.IsCompleted)
@@ -511,7 +517,7 @@ namespace Barotrauma.Steam
} }
ContentPackage? pkgToNuke ContentPackage? pkgToNuke
= ContentPackageManager.WorkshopPackages.FirstOrDefault(p => p.SteamWorkshopId == resultId); = ContentPackageManager.WorkshopPackages.FirstOrDefault(packageMatchesResult);
if (pkgToNuke != null) if (pkgToNuke != null)
{ {
Directory.Delete(pkgToNuke.Dir, recursive: true); Directory.Delete(pkgToNuke.Dir, recursive: true);
@@ -537,7 +543,7 @@ namespace Barotrauma.Steam
var localModProject = new ModProject(localPackage) var localModProject = new ModProject(localPackage)
{ {
SteamWorkshopId = resultId UgcId = Option<ContentPackageId>.Some(new SteamWorkshopId(resultId))
}; };
localModProject.DiscardHashAndInstallTime(); localModProject.DiscardHashAndInstallTime();
localModProject.Save(localPackage.Path); localModProject.Save(localPackage.Path);
@@ -19,7 +19,7 @@ namespace Barotrauma
public override bool Loaded => nestedStr.Loaded; public override bool Loaded => nestedStr.Loaded;
public override void RetrieveValue() 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(); UpdateLanguage();
} }
} }
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.19.3.0</Version> <Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.19.3.0</Version> <Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product> <Product>Barotrauma</Product>
<Version>0.19.3.0</Version> <Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName> <AssemblyName>Barotrauma</AssemblyName>
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.19.3.0</Version> <Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
+1 -1
View File
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.19.3.0</Version> <Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -61,8 +61,9 @@ namespace Barotrauma
msg.WriteColorR8G8B8(Head.SkinColor); msg.WriteColorR8G8B8(Head.SkinColor);
msg.WriteColorR8G8B8(Head.HairColor); msg.WriteColorR8G8B8(Head.HairColor);
msg.WriteColorR8G8B8(Head.FacialHairColor); msg.WriteColorR8G8B8(Head.FacialHairColor);
msg.WriteString(ragdollFileName);
msg.WriteString(ragdollFileName);
msg.WriteIdentifier(HumanPrefabIds.NpcIdentifier);
if (Job != null) if (Job != null)
{ {
msg.WriteUInt32(Job.Prefab.UintIdentifier); msg.WriteUInt32(Job.Prefab.UintIdentifier);
@@ -243,6 +243,7 @@ namespace Barotrauma
maxPlayers, maxPlayers,
ownerKey, ownerKey,
steamId); steamId);
Server.StartServer();
for (int i = 0; i < CommandLineArgs.Length; i++) for (int i = 0; i < CommandLineArgs.Length; i++)
{ {
@@ -1,4 +1,5 @@
using Barotrauma.Networking; using Barotrauma.Networking;
using System;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma namespace Barotrauma
@@ -20,19 +21,22 @@ namespace Barotrauma
CharacterInfo = client.CharacterInfo; CharacterInfo = client.CharacterInfo;
healthData = new XElement("health"); 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"); itemData = new XElement("inventory");
Character.SaveInventory(client.Character.Inventory, itemData); Character.SaveInventory(character.Inventory, itemData);
} }
OrderData = new XElement("orders"); OrderData = new XElement("orders");
if (client.CharacterInfo != null) if (CharacterInfo != null)
{ {
CharacterInfo.SaveOrderData(client.CharacterInfo, OrderData); CharacterInfo.SaveOrderData(CharacterInfo, OrderData);
} }
if (character?.Wallet.Save() is { } walletSave)
if (client.Character?.Wallet.Save() is { } walletSave)
{ {
WalletData = walletSave; WalletData = walletSave;
} }
@@ -118,9 +122,9 @@ namespace Barotrauma
character.SpawnInventoryItems(inventory, itemData.FromPackage(null)); 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) public void ApplyOrderData(Character character)
@@ -917,15 +917,14 @@ namespace Barotrauma
foreach (var kvp in purchasedItems) 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[storeId].Stock.Find(s => s.ItemPrefab == purchasedItem.ItemPrefab)?.Quantity ?? 0;
{ purchasedItem.Quantity = Math.Min(purchasedItem.Quantity, availableQuantity);
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(storeId, purchasedItemList, false, sender);
}
}
CargoManager.PurchaseItems(kvp.Key, kvp.Value, false, sender);
} }
foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems) foreach (var (storeIdentifier, items) in CargoManager.PurchasedItems)
@@ -71,6 +71,12 @@ namespace Barotrauma.Networking
{ {
expirationTime = parsedTime; 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)) : ""; 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) 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); var existingBan = bannedPlayers.Find(bp => bp.AddressOrAccountId == addressOrAccountId);
if (existingBan != null) { bannedPlayers.Remove(existingBan); } if (existingBan != null) { bannedPlayers.Remove(existingBan); }
@@ -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) public void SetPermissions(ClientPermissions permissions, IEnumerable<DebugConsole.Command> permittedConsoleCommands)
{ {
this.Permissions = permissions; Permissions = permissions;
this.PermittedConsoleCommands.Clear(); PermittedConsoleCommands.Clear();
this.PermittedConsoleCommands.UnionWith(permittedConsoleCommands); 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) 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) public void RemovePermission(ClientPermissions permission)
{ {
this.Permissions &= ~permission; Permissions &= ~permission;
} }
public bool HasPermission(ClientPermissions permission) public bool HasPermission(ClientPermissions permission)
{ {
return this.Permissions.HasFlag(permission); return Permissions.HasFlag(permission);
} }
} }
} }
@@ -39,7 +39,7 @@ namespace Barotrauma.Networking
string resultFileName string resultFileName
= dir.StartsWith(ContentPackage.LocalModsDir) = dir.StartsWith(ContentPackage.LocalModsDir)
? $"Local_{mod.Name}" ? $"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 = ToolBox.RemoveInvalidFileNameChars(resultFileName.Replace('\\', '_').Replace('/', '_'));
resultFileName = $"{resultFileName}{Extension}"; resultFileName = $"{resultFileName}{Extension}";
return Path.Combine(UploadFolder, resultFileName); return Path.Combine(UploadFolder, resultFileName);
File diff suppressed because it is too large Load Diff
@@ -246,7 +246,7 @@ namespace Barotrauma.Networking
" (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " + " (created " + (Timing.TotalTime - firstEventToResend.CreateTime).ToString("0.##") + " s ago, " +
(lastSentToAnyoneTime - firstEventToResend.CreateTime).ToString("0.##") + " s older than last event sent to anyone)" + (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); " 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); 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); 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) foreach (Client timedOutClient in timedOutClients)
{ {
GameServer.Log("Disconnecting client " + GameServer.ClientLogName(timedOutClient) + ". Syncing the client with the server took too long.", ServerLog.MessageType.Error); 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); bufferedEvents.RemoveAll(b => b.IsProcessed);
@@ -20,7 +20,7 @@ namespace Barotrauma.Networking
if (Sender != null && c.InGame) if (Sender != null && c.InGame)
{ {
msg.WriteUInt16(Sender.ID); msg.WriteUInt16(Sender.ID);
} }
msg.WriteBoolean(false); //text color (no custom text colors for order messages) msg.WriteBoolean(false); //text color (no custom text colors for order messages)
msg.WritePadBits(); msg.WritePadBits();
WriteOrder(msg); WriteOrder(msg);
@@ -15,7 +15,7 @@ namespace Barotrauma.Networking
private readonly List<NetIncomingMessage> incomingLidgrenMessages; 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; serverSettings = settings;
@@ -68,21 +68,21 @@ namespace Barotrauma.Networking
} }
} }
public override void Close(string? msg = null) public override void Close()
{ {
if (netServer == null) { return; } if (netServer == null) { return; }
for (int i = pendingClients.Count - 1; i >= 0; i--) 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--) 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(); pendingClients.Clear();
connectedClients.Clear(); connectedClients.Clear();
@@ -91,7 +91,7 @@ namespace Barotrauma.Networking
Steamworks.SteamServer.OnValidateAuthTicketResponse -= OnAuthChange; Steamworks.SteamServer.OnValidateAuthTicketResponse -= OnAuthChange;
OnShutdown?.Invoke(); callbacks.OnShutdown.Invoke();
} }
public override void Update(float deltaTime) public override void Update(float deltaTime)
@@ -100,12 +100,6 @@ namespace Barotrauma.Networking
ToolBox.ThrowIfNull(incomingLidgrenMessages); ToolBox.ThrowIfNull(incomingLidgrenMessages);
if (OnOwnerDetermined != null && OwnerConnection != null)
{
OnOwnerDetermined?.Invoke(OwnerConnection);
OnOwnerDetermined = null;
}
netServer.ReadMessages(incomingLidgrenMessages); netServer.ReadMessages(incomingLidgrenMessages);
//process incoming connections first //process incoming connections first
@@ -193,14 +187,14 @@ namespace Barotrauma.Networking
if (connectedClients.Count >= serverSettings.MaxPlayers) if (connectedClients.Count >= serverSettings.MaxPlayers)
{ {
inc.SenderConnection.Deny(DisconnectReason.ServerFull.ToString()); inc.SenderConnection.Deny(PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull).ToLidgrenStringRepresentation());
return; return;
} }
if (serverSettings.BanList.IsBanned(new LidgrenEndpoint(inc.SenderConnection.RemoteEndPoint), out string banReason)) if (serverSettings.BanList.IsBanned(new LidgrenEndpoint(inc.SenderConnection.RemoteEndPoint), out string banReason))
{ {
//IP banned: deny immediately //IP banned: deny immediately
inc.SenderConnection.Deny($"{DisconnectReason.Banned}/ {banReason}"); inc.SenderConnection.Deny(PeerDisconnectPacket.Banned(banReason).ToLidgrenStringRepresentation());
return; return;
} }
@@ -235,12 +229,12 @@ namespace Barotrauma.Networking
{ {
if (pendingClient != null) 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 && else if (lidgrenMsg.SenderConnection.Status != NetConnectionStatus.Disconnected &&
lidgrenMsg.SenderConnection.Status != NetConnectionStatus.Disconnecting) 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; return;
@@ -252,12 +246,12 @@ namespace Barotrauma.Networking
|| (conn.AccountInfo.AccountId.TryUnwrap(out var accountId) && serverSettings.BanList.IsBanned(accountId, out banReason)) || (conn.AccountInfo.AccountId.TryUnwrap(out var accountId) && serverSettings.BanList.IsBanned(accountId, out banReason))
|| conn.AccountInfo.OtherMatchingIds.Any(id => serverSettings.BanList.IsBanned(id, out banReason))) || conn.AccountInfo.OtherMatchingIds.Any(id => serverSettings.BanList.IsBanned(id, out banReason)))
{ {
Disconnect(conn, $"{DisconnectReason.Banned}/ {banReason}"); Disconnect(conn, PeerDisconnectPacket.Banned(banReason));
return; return;
} }
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc); 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..."); DebugConsole.NewMessage("Owner disconnected: closing the server...");
GameServer.Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage); GameServer.Log("Owner disconnected: closing the server...", ServerLog.MessageType.ServerMessage);
Close($"{DisconnectReason.ServerShutdown}/ Owner disconnected"); Close();
} }
else else
{ {
#warning TODO: kill off disconnect in layer 1 Disconnect(conn, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
Disconnect(conn, $"ServerMessage.HasDisconnected~[client]={GameMain.Server.ConnectedClients.First(c => c.Connection == conn).Name}");
} }
} }
else else
@@ -288,7 +281,7 @@ namespace Barotrauma.Networking
PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection); PendingClient? pendingClient = pendingClients.Find(c => c.Connection is LidgrenConnection l && l.NetConnection == inc.SenderConnection);
if (pendingClient != null) 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 (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; return;
@@ -325,7 +320,7 @@ namespace Barotrauma.Networking
|| serverSettings.BanList.IsBanned(new SteamId(steamId), out banReason) || serverSettings.BanList.IsBanned(new SteamId(steamId), out banReason)
|| serverSettings.BanList.IsBanned(new SteamId(ownerId), out banReason)) || serverSettings.BanList.IsBanned(new SteamId(ownerId), out banReason))
{ {
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason));
return; return;
} }
@@ -337,7 +332,7 @@ namespace Barotrauma.Networking
} }
else 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); 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; } if (netServer == null) { return; }
@@ -384,11 +379,11 @@ namespace Barotrauma.Networking
{ {
lidgrenConn.Status = NetworkConnectionStatus.Disconnected; lidgrenConn.Status = NetworkConnectionStatus.Disconnected;
connectedClients.Remove(lidgrenConn); 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); } 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) protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body)
@@ -413,6 +408,7 @@ namespace Barotrauma.Networking
{ {
ownerKey = Option<int>.None(); ownerKey = Option<int>.None();
OwnerConnection = pendingClient.Connection; OwnerConnection = pendingClient.Connection;
callbacks.OnOwnerDetermined.Invoke(OwnerConnection);
} }
} }
@@ -440,7 +436,7 @@ namespace Barotrauma.Networking
{ {
if (requireSteamAuth) if (requireSteamAuth)
{ {
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "Steam auth session failed to start: Steam ID not provided"); RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed));
return; return;
} }
} }
@@ -451,7 +447,7 @@ namespace Barotrauma.Networking
{ {
if (requireSteamAuth) if (requireSteamAuth)
{ {
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, $"Steam auth session failed to start: {authSessionStartState}"); RemovePendingClient(pendingClient, PeerDisconnectPacket.SteamAuthError(authSessionStartState));
} }
else else
{ {
@@ -471,7 +467,7 @@ namespace Barotrauma.Networking
{ {
if (pendingClient.AccountInfo.AccountId != packet.SteamId.Select(uid => (AccountId)uid)) if (pendingClient.AccountInfo.AccountId != packet.SteamId.Select(uid => (AccountId)uid))
{ {
RemovePendingClient(pendingClient, DisconnectReason.SteamAuthenticationFailed, "SteamID mismatch"); RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.SteamAuthenticationFailed));
} }
} }
} }
@@ -9,26 +9,31 @@ namespace Barotrauma.Networking
{ {
internal abstract class ServerPeer 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); protected readonly Callbacks callbacks;
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 ServerPeer(Callbacks callbacks)
{
this.callbacks = callbacks;
}
public abstract void InitializeSteamServerCallbacks(); public abstract void InitializeSteamServerCallbacks();
public abstract void Start(); public abstract void Start();
public abstract void Close(string? msg = null); public abstract void Close();
public abstract void Update(float deltaTime); public abstract void Update(float deltaTime);
protected sealed class PendingClient protected sealed class PendingClient
@@ -84,15 +89,16 @@ namespace Barotrauma.Networking
if (!Client.IsValidName(authPacket.Name, serverSettings)) if (!Client.IsValidName(authPacket.Name, serverSettings))
{ {
RemovePendingClient(pendingClient, DisconnectReason.InvalidName, ""); RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.InvalidName));
return; 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) if (!isCompatibleVersion)
{ {
RemovePendingClient(pendingClient, DisconnectReason.InvalidVersion, RemovePendingClient(pendingClient, PeerDisconnectPacket.InvalidVersion());
$"DisconnectMessage.InvalidVersion~[version]={GameMain.Version}~[clientversion]={authPacket.GameVersion}");
GameServer.Log($"{authPacket.Name} ({authPacket.SteamId}) couldn't join the server (incompatible game version)", ServerLog.MessageType.Error); 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); 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())); Client nameTaken = GameMain.Server.ConnectedClients.Find(c => Homoglyphs.Compare(c.Name.ToLower(), authPacket.Name.ToLower()));
if (nameTaken != null) 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); 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; return;
} }
@@ -135,7 +141,7 @@ namespace Barotrauma.Networking
{ {
const string banMsg = "Failed to enter correct password too many times"; const string banMsg = "Failed to enter correct password too many times";
BanPendingClient(pendingClient, banMsg, null); BanPendingClient(pendingClient, banMsg, null);
RemovePendingClient(pendingClient, DisconnectReason.Banned, banMsg); RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banMsg));
return; return;
} }
} }
@@ -190,13 +196,13 @@ namespace Barotrauma.Networking
{ {
if (IsPendingClientBanned(pendingClient, out string? banReason)) if (IsPendingClientBanned(pendingClient, out string? banReason))
{ {
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason));
return; return;
} }
if (connectedClients.Count >= serverSettings.MaxPlayers) if (connectedClients.Count >= serverSettings.MaxPlayers)
{ {
RemovePendingClient(pendingClient, DisconnectReason.ServerFull, ""); RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.ServerFull));
} }
if (pendingClient.InitializationStep == ConnectionInitialization.Success) if (pendingClient.InitializationStep == ConnectionInitialization.Success)
@@ -205,15 +211,15 @@ namespace Barotrauma.Networking
connectedClients.Add(newConnection); connectedClients.Add(newConnection);
pendingClients.Remove(pendingClient); pendingClients.Remove(pendingClient);
CheckOwnership(pendingClient); callbacks.OnInitializationComplete.Invoke(newConnection, pendingClient.Name);
OnInitializationComplete?.Invoke(newConnection, pendingClient.Name); CheckOwnership(pendingClient);
} }
pendingClient.TimeOut -= Timing.Step; pendingClient.TimeOut -= Timing.Step;
if (pendingClient.TimeOut < 0.0) 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; } if (Timing.TotalTime < pendingClient.UpdateTime) { return; }
@@ -237,7 +243,7 @@ namespace Barotrauma.Networking
structToSend = new ServerPeerContentPackageOrderPacket structToSend = new ServerPeerContentPackageOrderPacket
{ {
ServerName = GameMain.Server.ServerName, 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)) .Select(contentPackage => new ServerContentPackage(contentPackage, timeNow))
.ToImmutableArray() .ToImmutableArray()
}; };
@@ -267,11 +273,11 @@ namespace Barotrauma.Networking
protected virtual void CheckOwnership(PendingClient pendingClient) { } 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)) if (pendingClients.Contains(pendingClient))
{ {
Disconnect(pendingClient.Connection, $"{reason}/{msg}"); Disconnect(pendingClient.Connection, peerDisconnectPacket);
pendingClients.Remove(pendingClient); 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 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);
} }
} }
@@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Networking namespace Barotrauma.Networking
{ {
@@ -16,7 +17,7 @@ namespace Barotrauma.Networking
private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64); private SteamId ReadSteamId(IReadMessage inc) => new SteamId(inc.ReadUInt64() ^ ownerKey64);
private void WriteSteamId(IWriteMessage msg, SteamId val) => msg.WriteUInt64(val.Value ^ 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; serverSettings = settings;
@@ -43,7 +44,7 @@ namespace Barotrauma.Networking
started = true; started = true;
} }
public override void Close(string? msg = null) public override void Close()
{ {
if (!started) { return; } if (!started) { return; }
@@ -51,12 +52,12 @@ namespace Barotrauma.Networking
for (int i = pendingClients.Count - 1; i >= 0; i--) 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--) 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(); pendingClients.Clear();
@@ -64,19 +65,13 @@ namespace Barotrauma.Networking
ChildServerRelay.ShutDown(); ChildServerRelay.ShutDown();
OnShutdown?.Invoke(); callbacks.OnShutdown.Invoke();
} }
public override void Update(float deltaTime) public override void Update(float deltaTime)
{ {
if (!started) { return; } if (!started) { return; }
if (OnOwnerDetermined != null && OwnerConnection != null)
{
OnOwnerDetermined?.Invoke(OwnerConnection);
OnOwnerDetermined = null;
}
//backwards for loop so we can remove elements while iterating //backwards for loop so we can remove elements while iterating
for (int i = connectedClients.Count - 1; i >= 0; i--) for (int i = connectedClients.Count - 1; i >= 0; i--)
{ {
@@ -84,7 +79,7 @@ namespace Barotrauma.Networking
conn.Decay(deltaTime); conn.Decay(deltaTime);
if (conn.Timeout < 0.0) 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) if (pendingClient != null)
{ {
RemovePendingClient(pendingClient, DisconnectReason.Banned, banReason); RemovePendingClient(pendingClient, PeerDisconnectPacket.Banned(banReason));
} }
else if (connectedClient != null) else if (connectedClient != null)
{ {
Disconnect(connectedClient, $"{DisconnectReason.Banned}/ {banReason}"); Disconnect(connectedClient, PeerDisconnectPacket.Banned(banReason));
} }
} }
else if (packetHeader.IsDisconnectMessage()) else if (packetHeader.IsDisconnectMessage())
{ {
if (pendingClient != null) if (pendingClient != null)
{ {
string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={pendingClient.Name}"; RemovePendingClient(pendingClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
RemovePendingClient(pendingClient, DisconnectReason.Unknown, disconnectMsg);
} }
else if (connectedClient != null) else if (connectedClient != null)
{ {
string disconnectMsg = $"ServerMessage.HasDisconnected~[client]={GameMain.Server.ConnectedClients.First(c => c.Connection == connectedClient).Name}"; Disconnect(connectedClient, PeerDisconnectPacket.WithReason(DisconnectReason.Disconnected));
Disconnect(connectedClient, disconnectMsg, false);
} }
} }
else if (packetHeader.IsHeartbeatMessage()) else if (packetHeader.IsHeartbeatMessage())
@@ -176,28 +169,27 @@ namespace Barotrauma.Networking
} }
else if (packetHeader.IsConnectionInitializationStep()) else if (packetHeader.IsConnectionInitializationStep())
{ {
if (!initialization.HasValue) { return; }
ConnectionInitialization initializationStep = initialization.Value;
if (pendingClient != null) if (pendingClient != null)
{ {
pendingClient.Connection.SetAccountInfo(new AccountInfo(senderSteamId, sentOwnerSteamId)); pendingClient.Connection.SetAccountInfo(new AccountInfo(senderSteamId, sentOwnerSteamId));
ReadConnectionInitializationStep( ReadConnectionInitializationStep(
pendingClient, pendingClient,
new ReadWriteMessage(inc.Buffer, inc.BitPosition, inc.LengthBits, false), 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; pendingClients.Add(new PendingClient(new SteamP2PConnection(senderSteamId)));
if (initializationStep == ConnectionInitialization.ConnectionStarted)
{
pendingClients.Add(new PendingClient(new SteamP2PConnection(senderSteamId)));
}
} }
} }
else if (connectedClient != null) else if (connectedClient != null)
{ {
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc); var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, connectedClient); 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 else //sender is owner
@@ -227,7 +219,8 @@ namespace Barotrauma.Networking
}; };
OwnerConnection.SetAccountInfo(new AccountInfo(ownerSteamId, ownerSteamId)); OwnerConnection.SetAccountInfo(new AccountInfo(ownerSteamId, ownerSteamId));
OnInitializationComplete?.Invoke(OwnerConnection, packet.OwnerName); callbacks.OnInitializationComplete.Invoke(OwnerConnection, packet.OwnerName);
callbacks.OnOwnerDetermined.Invoke(OwnerConnection);
} }
return; return;
@@ -241,7 +234,7 @@ namespace Barotrauma.Networking
{ {
var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc); var packet = INetSerializableStruct.Read<PeerPacketMessage>(inc);
IReadMessage msg = new ReadOnlyMessage(packet.Buffer, packetHeader.IsCompressed(), 0, packet.Length, OwnerConnection); 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); SendMsgInternal(steamP2PConn, headers, body);
} }
private void SendDisconnectMessage(SteamId steamId, string? msg) private void SendDisconnectMessage(SteamId steamId, PeerDisconnectPacket peerDisconnectPacket)
{ {
if (!started) { return; } if (!started) { return; }
if (string.IsNullOrWhiteSpace(msg)) { return; }
var headers = new PeerPacketHeaders var headers = new PeerPacketHeaders
{ {
DeliveryMethod = DeliveryMethod.Reliable, DeliveryMethod = DeliveryMethod.Reliable,
PacketHeader = PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage, PacketHeader = PacketHeader.IsDisconnectMessage | PacketHeader.IsServerMessage,
Initialization = null 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; } if (!started) { return; }
@@ -309,26 +296,21 @@ namespace Barotrauma.Networking
if (!conn.AccountInfo.AccountId.TryUnwrap(out var connAccountId) || !(connAccountId is SteamId connSteamId)) { return; } 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)) if (connectedClients.Contains(steamp2pConn))
{ {
steamp2pConn.Status = NetworkConnectionStatus.Disconnected; steamp2pConn.Status = NetworkConnectionStatus.Disconnected;
connectedClients.Remove(steamp2pConn); connectedClients.Remove(steamp2pConn);
OnDisconnect?.Invoke(conn, msg); callbacks.OnDisconnect.Invoke(conn, peerDisconnectPacket);
Steam.SteamManager.StopAuthSession(connSteamId); Steam.SteamManager.StopAuthSession(connSteamId);
} }
else if (steamp2pConn == OwnerConnection) 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) protected override void SendMsgInternal(NetworkConnection conn, PeerPacketHeaders headers, INetSerializableStruct? body)
{ {
var connSteamId = conn is SteamP2PConnection { Endpoint: SteamP2PEndpoint { SteamId: var id } } ? id : null; var connSteamId = conn is SteamP2PConnection { Endpoint: SteamP2PEndpoint { SteamId: var id } } ? id : null;
@@ -505,9 +505,10 @@ namespace Barotrauma.Networking
var characterData = campaign?.GetClientCharacterData(clients[i]); var characterData = campaign?.GetClientCharacterData(clients[i]);
if (characterData != null && Level.Loaded?.Type != LevelData.LevelType.Outpost && characterData.HasSpawned) 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); GiveRespawnPenaltyAffliction(character);
} }
if (characterData == null || characterData.HasSpawned) if (characterData == null || characterData.HasSpawned)
{ {
//give the character the items they would've gotten if they had spawned in the main sub //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()) foreach (Skill skill in characterInfo.Job.GetSkills())
{ {
var skillPrefab = characterInfo.Job.Prefab.Skills.Find(s => skill.Identifier == s.Identifier); 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); skill.Level = MathHelper.Lerp(skill.Level, skillPrefab.LevelRange.End, SkillReductionOnDeath);
} }
} }
@@ -18,10 +18,14 @@ namespace Barotrauma.Networking
{ {
if (!PropEquals(lastSyncedValue, Value)) if (!PropEquals(lastSyncedValue, Value))
{ {
LastUpdateID = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID); LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID;
lastSyncedValue = Value; lastSyncedValue = Value;
} }
} }
public void ForceUpdate()
{
LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID++;
}
} }
public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml"; public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml";
@@ -54,6 +58,15 @@ namespace Barotrauma.Networking
LoadClientPermissions(); LoadClientPermissions();
} }
public void ForcePropertyUpdate()
{
UpdateFlag(NetFlags.Properties);
foreach (NetPropertyData property in netProperties.Values)
{
property.ForceUpdate();
}
}
private void WriteNetProperties(IWriteMessage outMsg, Client c) private void WriteNetProperties(IWriteMessage outMsg, Client c)
{ {
foreach (UInt32 key in netProperties.Keys) 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 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; 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; int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1;
if (traitorSetting < 0) traitorSetting = 2; if (traitorSetting < 0) { traitorSetting = 2; }
if (traitorSetting > 2) traitorSetting = 0; if (traitorSetting > 2) { traitorSetting = 0; }
TraitorsEnabled = (YesNoMaybe)traitorSetting; TraitorsEnabled = (YesNoMaybe)traitorSetting;
int botCount = BotCount + incMsg.ReadByte() - 1; int botCount = BotCount + incMsg.ReadByte() - 1;
if (botCount < 0) botCount = MaxBotCount; if (botCount < 0) { botCount = MaxBotCount; }
if (botCount > MaxBotCount) botCount = 0; if (botCount > MaxBotCount) { botCount = 0; }
BotCount = botCount; BotCount = botCount;
int botSpawnMode = (int)BotSpawnMode + incMsg.ReadByte() - 1; int botSpawnMode = (int)BotSpawnMode + incMsg.ReadByte() - 1;
if (botSpawnMode < 0) botSpawnMode = 1; if (botSpawnMode < 0) { botSpawnMode = 1; }
if (botSpawnMode > 1) botSpawnMode = 0; if (botSpawnMode > 1) { botSpawnMode = 0; }
BotSpawnMode = (BotSpawnMode)botSpawnMode; BotSpawnMode = (BotSpawnMode)botSpawnMode;
float levelDifficulty = incMsg.ReadSingle(); 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 changedAutoRestart = incMsg.ReadBoolean();
bool autoRestart = incMsg.ReadBoolean(); bool autoRestart = incMsg.ReadBoolean();
@@ -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) public void ServerRead(IReadMessage inc, Client sender)
{ {
if (GameMain.Server == null || sender == null) { return; } if (GameMain.Server == null || sender == null) { return; }
@@ -192,7 +192,7 @@ namespace Barotrauma
public override void Select() public override void Select()
{ {
base.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) if (SelectedMode != GameModePreset.MultiPlayerCampaign && GameMain.GameSession?.GameMode is CampaignMode && Selected == this)
{ {
GameMain.GameSession = null; GameMain.GameSession = null;
@@ -53,12 +53,13 @@ namespace Barotrauma.Steam
Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword; Steamworks.SteamServer.Passworded = server.ServerSettings.HasPassword;
Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName?.Value ?? ""; Steamworks.SteamServer.MapName = GameMain.NetLobbyScreen?.SelectedSub?.DisplayName?.Value ?? "";
Steamworks.SteamServer.SetKey("haspassword", server.ServerSettings.HasPassword.ToString()); 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("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("contentpackage", string.Join(",", contentPackages.Select(cp => cp.Name)));
Steamworks.SteamServer.SetKey("contentpackagehash", string.Join(",", contentPackages.Select(cp => cp.Hash.StringRepresentation))); 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("modeselectionmode", server.ServerSettings.ModeSelectionMode.ToString());
Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString()); Steamworks.SteamServer.SetKey("subselectionmode", server.ServerSettings.SubSelectionMode.ToString());
Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString()); Steamworks.SteamServer.SetKey("voicechatenabled", server.ServerSettings.VoiceChatEnabled.ToString());
@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace> <RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors> <Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product> <Product>Barotrauma Dedicated Server</Product>
<Version>0.19.3.0</Version> <Version>0.19.5.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -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. // We'll want this to run each time, because the delegate is used to find a valid button component.
bool canAccessButtons = false; 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))) if (button.HasAccess(character) && (buttonFilter == null || buttonFilter(button)))
{ {
@@ -675,6 +675,8 @@ namespace Barotrauma
} }
} }
float distance = Vector2.DistanceSquared(button.Item.WorldPosition, character.WorldPosition); 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)) if (closestButton == null || distance < closestDist && character.CanSeeTarget(button.Item))
{ {
closestButton = button; closestButton = button;
@@ -1634,9 +1634,9 @@ namespace Barotrauma
return id; 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> /// <summary>
@@ -12,12 +12,8 @@ namespace Barotrauma
{ {
public readonly static PrefabCollection<CharacterPrefab> Prefabs = new PrefabCollection<CharacterPrefab>(); public readonly static PrefabCollection<CharacterPrefab> Prefabs = new PrefabCollection<CharacterPrefab>();
private bool disposed = false;
public override void Dispose() public override void Dispose()
{ {
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
Character.RemoveByPrefab(this); Character.RemoveByPrefab(this);
} }
@@ -11,13 +11,7 @@ namespace Barotrauma
{ {
public static readonly PrefabCollection<CorpsePrefab> Prefabs = new PrefabCollection<CorpsePrefab>(); public static readonly PrefabCollection<CorpsePrefab> Prefabs = new PrefabCollection<CorpsePrefab>();
private bool disposed = false; public override void Dispose() { }
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
public static CorpsePrefab Get(Identifier identifier) public static CorpsePrefab Get(Identifier identifier)
{ {
@@ -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()) foreach (var subElement in element.Elements())
{ {
@@ -1260,6 +1260,7 @@ namespace Barotrauma
DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found."); DebugConsole.ThrowError($"Error while loading character health: affliction \"{id}\" not found.");
return; return;
} }
if (afflictionPredicate != null && !afflictionPredicate.Invoke(afflictionPrefab)) { return; }
float strength = afflictionElement.GetAttributeFloat("strength", 0.0f); float strength = afflictionElement.GetAttributeFloat("strength", 0.0f);
var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab); var irremovableAffliction = irremovableAfflictions.FirstOrDefault(a => a.Prefab == afflictionPrefab);
if (irremovableAffliction != null) if (irremovableAffliction != null)
@@ -65,13 +65,7 @@ namespace Barotrauma
{ {
public static readonly PrefabCollection<JobPrefab> Prefabs = new PrefabCollection<JobPrefab>(); public static readonly PrefabCollection<JobPrefab> Prefabs = new PrefabCollection<JobPrefab>();
private bool disposed = false; public override void Dispose() { }
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
Prefabs.Remove(this);
}
private static readonly Dictionary<Identifier, float> _itemRepairPriorities = new Dictionary<Identifier, float>(); private static readonly Dictionary<Identifier, float> _itemRepairPriorities = new Dictionary<Identifier, float>();
/// <summary> /// <summary>
@@ -56,11 +56,6 @@ namespace Barotrauma
} }
} }
private bool disposed = false; public override void Dispose() { }
public override void Dispose()
{
if (disposed) { return; }
disposed = true;
}
} }
} }
@@ -40,7 +40,9 @@ namespace Barotrauma
} }
catch 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; throw;
} }
} }
@@ -29,25 +29,25 @@ namespace Barotrauma
public readonly ImmutableArray<string> AltNames; public readonly ImmutableArray<string> AltNames;
public readonly string Path; public readonly string Path;
public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? ""; public string Dir => Barotrauma.IO.Path.GetDirectoryName(Path) ?? "";
public readonly UInt64 SteamWorkshopId; public readonly Option<ContentPackageId> UgcId;
public readonly Version GameVersion; public readonly Version GameVersion;
public readonly string ModVersion; public readonly string ModVersion;
public Md5Hash Hash { get; private set; } 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> Files { get; private set; }
public ImmutableArray<ContentFile.LoadError> Errors { get; private set; } public ImmutableArray<ContentFile.LoadError> Errors { get; private set; }
public async Task<bool> IsUpToDate() public async Task<bool> IsUpToDate()
{ {
if (SteamWorkshopId != 0 && InstallTime.HasValue) if (!UgcId.TryUnwrap(out var ugcId)) { return true; }
{ if (!(ugcId is SteamWorkshopId steamWorkshopId)) { return true; }
Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(SteamWorkshopId); if (!InstallTime.TryUnwrap(out var installTime)) { return true; }
if (item is null) { return true; }
return item.Value.LatestUpdateTime <= InstallTime; Steamworks.Ugc.Item? item = await SteamManager.Workshop.GetItem(steamWorkshopId.Value);
} if (item is null) { return true; }
return true; return item.Value.LatestUpdateTime <= installTime;
} }
public int Index => ContentPackageManager.EnabledPackages.IndexOf(this); public int Index => ContentPackageManager.EnabledPackages.IndexOf(this);
@@ -66,18 +66,19 @@ namespace Barotrauma
AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>()) AltNames = rootElement.GetAttributeStringArray("altnames", Array.Empty<string>())
.Select(n => n.Trim()).ToImmutableArray(); .Select(n => n.Trim()).ToImmutableArray();
AssertCondition(!string.IsNullOrEmpty(Name), "Name is null or empty"); 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); GameVersion = rootElement.GetAttributeVersion("gameversion", GameMain.Version);
ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion); ModVersion = rootElement.GetAttributeString("modversion", DefaultModVersion);
if (rootElement.Attribute("installtime") != null) UInt64 installTimeUnix = rootElement.GetAttributeUInt64("installtime", 0);
{ InstallTime = installTimeUnix != 0
InstallTime = ToolBox.Epoch.ToDateTime(rootElement.GetAttributeUInt("installtime", 0)); ? Option<DateTime>.Some(ToolBox.Epoch.ToDateTime(installTimeUnix))
} : Option<DateTime>.None();
else
{
InstallTime = null;
}
var fileResults = rootElement.Elements() var fileResults = rootElement.Elements()
.Select(e => ContentFile.CreateFromXElement(this, e)) .Select(e => ContentFile.CreateFromXElement(this, e))
@@ -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);
}
}
@@ -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));
}
}
}
@@ -181,7 +181,7 @@ namespace Barotrauma
{ {
if (Core != null && !ContentPackageManager.CorePackages.Contains(Core)) 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()); ContentPackageManager.CorePackages.First());
} }
@@ -193,7 +193,7 @@ namespace Barotrauma
newRegular.Add(p); newRegular.Add(p);
} }
else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2 else if (ContentPackageManager.WorkshopPackages.Regular.FirstOrDefault(p2
=> p2.SteamWorkshopId == p.SteamWorkshopId) is { } newP) => p2.UgcId == p.UgcId) is { } newP)
{ {
newRegular.Add(newP); newRegular.Add(newP);
} }

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