Skip to content

Commit a3b6429

Browse files
committed
feat: engine version indicator
- add "IsDeveloper" to the application context, this is true when the client host is non-public or failed to be resolved and false in all other cases - Version indicator is shown in bottom left corner of main menu - Version indicator is shown in bottom left corner of in-game escape menu (not including the simplified menu)
1 parent f6c09cb commit a3b6429

File tree

19 files changed

+341
-33
lines changed

19 files changed

+341
-33
lines changed

Framework/Intersect.Framework.Core/Color.cs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Globalization;
3+
using Intersect.Framework;
14
using Intersect.Localization;
25
using MessagePack;
36

@@ -232,7 +235,61 @@ public static Color FromRgba(int rgba)
232235

233236
public static string ToString(Color clr) => clr?.ToString() ?? string.Empty;
234237

235-
public static Color FromString(string val, Color defaultColor = null)
238+
[return: NotNullIfNotNull(nameof(defaultColor))]
239+
public static Color? FromHex(string hexString, Color? defaultColor = default)
240+
{
241+
const NumberStyles hexStyles = NumberStyles.HexNumber | NumberStyles.AllowHexSpecifier;
242+
243+
if (string.IsNullOrWhiteSpace(hexString))
244+
{
245+
return defaultColor;
246+
}
247+
248+
hexString = hexString.Trim();
249+
250+
if (hexString.StartsWith('#'))
251+
{
252+
hexString = hexString[1..];
253+
}
254+
255+
string reversedHexString = hexString.Reverse();
256+
257+
if (!uint.TryParse(reversedHexString, hexStyles, CultureInfo.InvariantCulture, out var abgr))
258+
{
259+
return defaultColor;
260+
}
261+
262+
int a, g, b, r;
263+
264+
if (reversedHexString.Length <= 4)
265+
{
266+
a = (int)(abgr >> 12 & 0xf);
267+
b = (int)(abgr >> 8 & 0xf);
268+
g = (int)(abgr >> 4 & 0xf);
269+
r = (int)(abgr & 0xf);
270+
a |= a << 4;
271+
b |= b << 4;
272+
g |= g << 4;
273+
r |= r << 4;
274+
}
275+
else
276+
{
277+
a = (int)(abgr >> 28 & 0xf | (abgr >> 24 & 0xf) << 4);
278+
b = (int)(abgr >> 20 & 0xf | (abgr >> 16 & 0xf) << 4);
279+
g = (int)(abgr >> 12 & 0xf | (abgr >> 8 & 0xf) << 4);
280+
r = (int)(abgr >> 4 & 0xf | (abgr & 0xf) << 4);
281+
}
282+
283+
a = reversedHexString.Length switch
284+
{
285+
< 4 or > 4 and < 8 => 0xff,
286+
_ => a,
287+
};
288+
289+
return new Color(a: a, r: r, g: g, b: b);
290+
}
291+
292+
public static Color? FromString(string val, Color? defaultColor = default)
236293
{
237294
if (string.IsNullOrEmpty(val))
238295
{

Framework/Intersect.Framework.Core/Core/IApplicationContext.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11

2+
using Intersect.Framework.Reflection;
23
using Intersect.Plugins.Interfaces;
34
using Intersect.Threading;
45
using Microsoft.Extensions.Logging;
@@ -15,6 +16,26 @@ public interface IApplicationContext : IDisposable
1516
/// </summary>
1617
bool HasErrors { get; }
1718

19+
/// <summary>
20+
/// If the application is a debug build.
21+
/// </summary>
22+
bool IsDebug
23+
{
24+
get
25+
{
26+
#if !DEBUG
27+
return true;
28+
#else
29+
return false;
30+
#endif
31+
}
32+
}
33+
34+
/// <summary>
35+
/// If the application is being run in developer mode.
36+
/// </summary>
37+
bool IsDeveloper => false;
38+
1839
/// <summary>
1940
/// If the application has been disposed.
2041
/// </summary>
@@ -50,6 +71,11 @@ public interface IApplicationContext : IDisposable
5071
/// </summary>
5172
List<IApplicationService> Services { get; }
5273

74+
/// <summary>
75+
/// The human-friendly version string.
76+
/// </summary>
77+
string VersionName => GetType().Assembly.GetMetadataVersionName();
78+
5379
/// <summary>
5480
/// Gets a service of type <typeparamref name="TApplicationService"/> if one has been registered.
5581
/// </summary>

Framework/Intersect.Framework/Reflection/AssemblyExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public static string GetMetadataVersion(this Assembly assembly, bool excludeComm
5656
return GetMetadataVersionFrom(assembly, assemblyName, excludeCommitSha);
5757
}
5858

59+
public static string GetMetadataVersionName(this Assembly assembly, bool excludeCommitSha = false) =>
60+
$"v{GetMetadataVersion(assembly: assembly, excludeCommitSha: excludeCommitSha)}";
61+
5962
private static string GetMetadataVersionFrom(Assembly assembly, AssemblyName assemblyName, bool excludeCommitSha)
6063
{
6164
var assemblyVersion = assemblyName.Version ?? new Version(0, 0, 0, 0);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Text;
2+
3+
namespace Intersect.Framework;
4+
5+
public static class StringExtensions
6+
{
7+
public static string Reverse(this string @string)
8+
{
9+
var codepoints = new int[@string.Length];
10+
var numCodepoints = 0;
11+
for (var index = 0; index < @string.Length; ++index)
12+
{
13+
++numCodepoints;
14+
15+
codepoints[index] = char.ConvertToUtf32(@string, index);
16+
if (char.IsSurrogatePair(@string, index))
17+
{
18+
++index;
19+
}
20+
}
21+
22+
codepoints = codepoints[..numCodepoints];
23+
Array.Reverse(codepoints);
24+
25+
return codepoints.Aggregate(string.Empty, (current, codepoint) => current + char.ConvertFromUtf32(codepoint));
26+
}
27+
}

Intersect.Client.Core/Core/Bootstrapper.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,28 @@ public static void Start(params string[] args)
7878
var clientConfiguration = ClientConfiguration.LoadAndSave();
7979
loggingLevelSwitch.MinimumLevel = LevelConvert.ToSerilogLevel(clientConfiguration.LogLevel);
8080

81-
var context = new ClientContext(commandLineOptions, logger, packetHelper);
81+
if (commandLineOptions.Server is { } server)
82+
{
83+
var serverParts = server.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
84+
var host = serverParts.First();
85+
86+
var portPart = serverParts.Skip(1).FirstOrDefault();
87+
if (string.IsNullOrEmpty(portPart))
88+
{
89+
portPart = "5400";
90+
}
91+
var port = ushort.Parse(portPart);
92+
93+
clientConfiguration.Host = host;
94+
clientConfiguration.Port = port;
95+
}
96+
97+
commandLineOptions = commandLineOptions with
98+
{
99+
Server = $"{clientConfiguration.Host}:{clientConfiguration.Port}",
100+
};
101+
102+
ClientContext context = new(commandLineOptions, clientConfiguration, logger, packetHelper);
82103
context.Start();
83104
}
84105

Intersect.Client.Core/Core/ClientCommandLineOptions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace Intersect.Client.Core;
66

77
internal readonly partial struct ClientCommandLineOptions : ICommandLineOptions
88
{
9+
// ReSharper disable once ConvertToPrimaryConstructor
910
public ClientCommandLineOptions(
1011
bool borderlessWindow,
1112
int screenWidth,
@@ -33,7 +34,7 @@ IEnumerable<string> pluginDirectories
3334
public int ScreenHeight { get; }
3435

3536
[Option('S', "server", Default = null, Required = false)]
36-
public string Server { get; }
37+
public string Server { get; init; }
3738

3839
[Option("working-directory", Default = null, Required = false)]
3940
public string WorkingDirectory { get; }

Intersect.Client.Core/Core/ClientContext.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
using System.Net;
2+
using System.Net.Sockets;
13
using Intersect.Client.Networking;
24
using Intersect.Client.Plugins.Contexts;
5+
using Intersect.Configuration;
36
using Intersect.Core;
47
using Intersect.Factories;
8+
using Intersect.Framework.Net;
9+
using Intersect.Network;
510
using Intersect.Plugins;
611
using Intersect.Plugins.Interfaces;
712
using Intersect.Reflection;
@@ -18,13 +23,33 @@ internal sealed partial class ClientContext : ApplicationContext<ClientContext,
1823

1924
private IPlatformRunner? mPlatformRunner;
2025

21-
internal ClientContext(ClientCommandLineOptions startupOptions, ILogger logger, IPacketHelper packetHelper) : base(
26+
internal ClientContext(ClientCommandLineOptions startupOptions, ClientConfiguration clientConfiguration, ILogger logger, IPacketHelper packetHelper) : base(
2227
startupOptions, logger, packetHelper
2328
)
2429
{
30+
var hostNameOrAddress = clientConfiguration.Host;
31+
try
32+
{
33+
var address = Dns.GetHostAddresses(hostNameOrAddress).FirstOrDefault();
34+
IsDeveloper = !(address?.IsPublic() ?? false);
35+
}
36+
catch (SocketException socketException)
37+
{
38+
if (socketException.SocketErrorCode != SocketError.HostNotFound)
39+
{
40+
throw;
41+
}
42+
43+
ClientNetwork.UnresolvableHostNames.Add(startupOptions.Server);
44+
ApplicationContext.Context.Value?.Logger.LogError(socketException, $"Failed to resolve host: '{hostNameOrAddress}'");
45+
IsDeveloper = true;
46+
}
47+
2548
_ = FactoryRegistry<IPluginContext>.RegisterFactory(new ClientPluginContext.Factory());
2649
}
2750

51+
public bool IsDeveloper { get; }
52+
2853
protected override bool UsesMainThread => true;
2954

3055
public IPlatformRunner PlatformRunner

Intersect.Client.Core/Interface/Game/EscapeMenu.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using Intersect.Client.Core;
22
using Intersect.Client.Framework.File_Management;
3+
using Intersect.Client.Framework.Gwen;
34
using Intersect.Client.Framework.Gwen.Control;
45
using Intersect.Client.Framework.Gwen.Control.EventArguments;
56
using Intersect.Client.General;
67
using Intersect.Client.Interface.Shared;
78
using Intersect.Client.Localization;
9+
using Intersect.Core;
810
using Intersect.Utilities;
911

1012
namespace Intersect.Client.Interface.Game;
@@ -13,6 +15,8 @@ public partial class EscapeMenu : ImagePanel
1315
{
1416
private readonly SettingsWindow _settingsWindow;
1517
private readonly Button _buttonCharacterSelect;
18+
private readonly Panel _versionPanel;
19+
private readonly Label _versionLabel;
1620

1721
public EscapeMenu(Canvas gameCanvas) : base(gameCanvas, nameof(EscapeMenu))
1822
{
@@ -76,6 +80,29 @@ public EscapeMenu(Canvas gameCanvas) : base(gameCanvas, nameof(EscapeMenu))
7680
{
7781
_buttonCharacterSelect.IsDisabled = true;
7882
}
83+
84+
_versionPanel = new Panel(this, name: nameof(_versionPanel))
85+
{
86+
Alignment = [Alignments.Bottom, Alignments.Right],
87+
BackgroundColor = new Color(0x7f, 0, 0, 0),
88+
Padding = new Padding(8, 4),
89+
RestrictToParent = true,
90+
IsVisible = ApplicationContext.CurrentContext.IsDeveloper, // TODO: Remove this when showing a game version is added
91+
};
92+
93+
_versionLabel = new Label(_versionPanel, name: nameof(_versionLabel))
94+
{
95+
Alignment = [Alignments.Center],
96+
AutoSizeToContents = true,
97+
Font = GameContentManager.Current.GetFont("sourcesansproblack", 10),
98+
Text = ApplicationContext.CurrentContext.VersionName,
99+
TextPadding = new Padding(2, 0),
100+
IsVisible = ApplicationContext.CurrentContext.IsDeveloper,
101+
};
102+
103+
_versionLabel.SizeToContents();
104+
105+
_versionPanel.SizeToChildren();
79106
}
80107

81108
public override void Invalidate()

Intersect.Client.Core/Interface/Menu/MenuGuiBase.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using Intersect.Client.Core;
22
using Intersect.Client.Framework.File_Management;
3+
using Intersect.Client.Framework.Gwen;
34
using Intersect.Client.Framework.Gwen.Control;
45
using Intersect.Client.Localization;
56
using Intersect.Client.Networking;
7+
using Intersect.Core;
68

79
namespace Intersect.Client.Interface.Menu;
810

@@ -11,6 +13,8 @@ public partial class MenuGuiBase : IMutableInterface
1113
private readonly Canvas _menuCanvas;
1214
private readonly ImagePanel _serverStatusArea;
1315
private readonly Label _serverStatusLabel;
16+
private readonly Panel _versionPanel;
17+
private readonly Label _versionLabel;
1418

1519
public MainMenu MainMenu { get; }
1620

@@ -21,6 +25,30 @@ public MenuGuiBase(Canvas myCanvas)
2125
_menuCanvas = myCanvas;
2226

2327
MainMenu = new MainMenu(_menuCanvas);
28+
29+
_versionPanel = new Panel(_menuCanvas, name: nameof(_versionPanel))
30+
{
31+
Alignment = [Alignments.Bottom, Alignments.Right],
32+
BackgroundColor = new Color(0x7f, 0, 0, 0),
33+
Padding = new Padding(8, 4),
34+
RestrictToParent = true,
35+
IsVisible = ApplicationContext.CurrentContext.IsDeveloper, // TODO: Remove this when showing a game version is added
36+
};
37+
38+
_versionLabel = new Label(_versionPanel, name: nameof(_versionLabel))
39+
{
40+
Alignment = [Alignments.Center],
41+
AutoSizeToContents = true,
42+
Font = GameContentManager.Current.GetFont("sourcesansproblack", 10),
43+
Text = ApplicationContext.CurrentContext.VersionName,
44+
TextPadding = new Padding(2, 0),
45+
IsVisible = ApplicationContext.CurrentContext.IsDeveloper,
46+
};
47+
48+
_versionLabel.SizeToContents();
49+
50+
_versionPanel.SizeToChildren();
51+
2452
_serverStatusArea = new ImagePanel(_menuCanvas, "ServerStatusArea")
2553
{
2654
IsHidden = ClientContext.IsSinglePlayer,

Intersect.Client.Core/MonoGame/Network/MonoSocket.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,6 @@ public override IClient Network
9696
private IPEndPoint? _lastEndpoint;
9797
private volatile bool _resolvingHost;
9898

99-
private static readonly HashSet<string> UnresolvableHostNames = [];
100-
10199
public static MonoSocket Instance { get; private set; } = default!;
102100

103101
internal MonoSocket(IClientContext context)
@@ -113,7 +111,7 @@ internal MonoSocket(IClientContext context)
113111
private bool TryResolveEndPoint([NotNullWhen(true)] out IPEndPoint? endPoint)
114112
{
115113
var currentHost = ClientConfiguration.Instance.Host;
116-
if (UnresolvableHostNames.Contains(currentHost))
114+
if (ClientNetwork.UnresolvableHostNames.Contains(currentHost))
117115
{
118116
endPoint = default;
119117
return false;
@@ -165,7 +163,7 @@ private bool TryResolveEndPoint([NotNullWhen(true)] out IPEndPoint? endPoint)
165163
throw;
166164
}
167165

168-
UnresolvableHostNames.Add(_lastHost);
166+
ClientNetwork.UnresolvableHostNames.Add(_lastHost);
169167
Interface.Interface.ShowError(Strings.Errors.HostNotFound);
170168
ApplicationContext.Context.Value?.Logger.LogError(socketException, $"Failed to resolve host: '{_lastHost}'");
171169
endPoint = default;
@@ -237,7 +235,7 @@ public override void Update()
237235
network.SendUnconnected(serverEndpoint, new ServerStatusRequestPacket());
238236
}
239237
}
240-
else if (!UnresolvableHostNames.Contains(_lastHost))
238+
else if (!ClientNetwork.UnresolvableHostNames.Contains(_lastHost))
241239
{
242240
ApplicationContext.Context.Value?.Logger.LogInformation($"Unable to resolve '{_lastHost}:{_lastPort}'");
243241
}

0 commit comments

Comments
 (0)