Merge missing commits from 1.5.0.x branch.

This commit is contained in:
Nick Blakely 2022-09-04 20:29:59 -07:00
parent 038b0a9d29
commit 5b3286c2a6
24 changed files with 191 additions and 95 deletions

View File

@ -16,7 +16,6 @@ namespace Giants.Launcher
public partial class LauncherForm : Form
{
// Constant settings
private const string GameName = "Giants: Citizen Kabuto";
private const string GamePath = "GiantsMain.exe";
private const string RegistryKey = @"HKEY_CURRENT_USER\Software\PlanetMoon\Giants";
private const string RegistryValue = "DestDir";
@ -38,7 +37,7 @@ namespace Giants.Launcher
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
// Set window title
this.Text = GameName;
this.SetTitle();
// Read newer file-based game settings
this.config = new Config();
@ -97,7 +96,7 @@ namespace Giants.Launcher
private void btnOptions_Click(object sender, EventArgs e)
{
OptionsForm form = new OptionsForm(GameName + " Options", this.gamePath);
OptionsForm form = new OptionsForm(Resources.AppName + " Options", this.gamePath);
form.StartPosition = FormStartPosition.CenterParent;
form.ShowDialog();
@ -116,7 +115,7 @@ namespace Giants.Launcher
if (this.gamePath == null || !File.Exists(this.gamePath))
{
string message = string.Format(Resources.AppNotFound, GameName);
string message = string.Format(Resources.AppNotFound, Resources.AppName);
MessageBox.Show(message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Exit();
return;
@ -131,7 +130,7 @@ namespace Giants.Launcher
Version gameVersion = VersionHelper.GetGameVersion(this.gamePath);
if (gameVersion == null)
{
string message = string.Format(Resources.AppNotFound, GameName);
string message = string.Format(Resources.AppNotFound, Resources.AppName);
MessageBox.Show(message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Exit();
}
@ -330,6 +329,21 @@ namespace Giants.Launcher
}
Process.Start(this.communityAppUri);
}
private void SetTitle()
{
string title = Resources.AppName;
#if DEBUG
title += " DEBUG";
#endif
#if BETA
title += " BETA";
#endif
this.Text = title;
}
}
}

View File

@ -22,6 +22,7 @@ namespace Giants.Launcher
{
// Must come first as other options depend on it
this.PopulateRenderers();
this.SetRenderer();
this.PopulateResolution();
this.PopulateAnisotropy();
@ -40,6 +41,24 @@ namespace Giants.Launcher
.ToArray());
}
private void SetRenderer()
{
string selectedRenderer = GameSettings.Get<string>(RegistryKeys.Renderer);
RendererInfo renderer =
GameSettings.CompatibleRenderers.Find(
r => Path.GetFileName(r.FilePath).Equals(selectedRenderer, StringComparison.OrdinalIgnoreCase));
if (renderer != null)
{
this.cmbRenderer.SelectedItem = renderer;
}
else
{
renderer = GameSettings.CompatibleRenderers.Find(r => r.FileName == "gg_dx7r.dll");
this.cmbRenderer.SelectedItem = renderer;
}
}
private void SetOptions()
{
var resolutions = (List<ScreenResolution>)this.cmbResolution.DataSource;
@ -53,19 +72,6 @@ namespace Giants.Launcher
this.cmbAntialiasing.SelectedIndex = 0;
this.chkUpdates.Checked = GameSettings.Get<int>(RegistryKeys.NoAutoUpdate) != 1;
RendererInfo renderer = GameSettings.CompatibleRenderers.Find(
r => StringComparer.OrdinalIgnoreCase.Compare(Path.GetFileName(r.FilePath), GameSettings.Get<string>(RegistryKeys.Renderer)) == 0);
if (renderer != null)
{
this.cmbRenderer.SelectedItem = renderer;
}
else
{
renderer = GameSettings.CompatibleRenderers.Find(r => r.FileName == "gg_dx7r.dll");
this.cmbRenderer.SelectedItem = renderer;
}
}
private void PopulateAntialiasing()

View File

@ -6,9 +6,13 @@ namespace Giants.Launcher
{
static class NativeMethods
{
[DllImport("kernel32.dll")]
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr LoadLibrary(string dllToLoad);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetDllDirectory(string lpPathName);
[DllImport("kernel32.dll")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procedureName);

View File

@ -58,6 +58,8 @@ namespace Giants.Launcher
{
try
{
NativeMethods.SetDllDirectory(Environment.CurrentDirectory);
// Make interop call to native renderer DLLs to get capability info
var interopCaps = new RendererInterop.GFXCapabilityInfo();
string path = Path.Combine(file.DirectoryName, file.Name);

View File

@ -60,6 +60,15 @@ namespace Giants.Launcher {
}
}
/// <summary>
/// Looks up a localized string similar to Giants: Citizen Kabuto.
/// </summary>
internal static string AppName {
get {
return ResourceManager.GetString("AppName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Could not locate an installation of {0}. The launcher will now exit..
/// </summary>

View File

@ -205,4 +205,7 @@
<data name="ErrorNoConfigFile" xml:space="preserve">
<value>Settings file {0} was not found.</value>
</data>
<data name="AppName" xml:space="preserve">
<value>Giants: Citizen Kabuto</value>
</data>
</root>

View File

@ -4,7 +4,7 @@
public class ServerInfo : DataContract.V1.ServerInfo, IIdentifiable
{
public string id => this.HostIpAddress;
public string id => $"{this.HostIpAddress}-{this.GameName}-{this.Port}";
public string DocumentType => nameof(ServerInfo);
@ -17,6 +17,8 @@
return obj is ServerInfo info &&
base.Equals(obj) &&
this.HostIpAddress == info.HostIpAddress &&
this.Port == info.Port &&
this.GameName == info.GameName &&
this.DocumentType == info.DocumentType;
}

View File

@ -7,6 +7,8 @@
{
Task DeleteServer(string ipAddress);
Task DeleteServer(string ipAddress, string gameName, int port);
Task<IEnumerable<ServerInfo>> GetAllServers();
Task AddServer(ServerInfo server);

View File

@ -4,16 +4,18 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AutoMapper;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
public class ServerRegistryService : IServerRegistryService
{
private static readonly string[] SupportedGameNames = new[] { "Giants" };
private static readonly string[] SupportedGameNames = new[] { "Giants", "Giants Beta", "Giants Beta Dedicated", "Giants Dedicated" };
private readonly ILogger<ServerRegistryService> logger;
private readonly IServerRegistryStore registryStore;
private readonly IConfiguration configuration;
private readonly int maxServerCount;
private readonly int maxServersPerIp;
public ServerRegistryService(
ILogger<ServerRegistryService> logger,
@ -24,6 +26,7 @@
this.registryStore = registryStore;
this.configuration = configuration;
this.maxServerCount = Convert.ToInt32(this.configuration["MaxServerCount"]);
this.maxServersPerIp = Convert.ToInt32(this.configuration["MaxServersPerIp"]);
}
public async Task AddServer(
@ -32,11 +35,19 @@
ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo));
ArgumentUtility.CheckStringForNullOrEmpty(serverInfo.HostIpAddress, nameof(serverInfo.HostIpAddress));
string gameName = serverInfo.GameName.Replace("Dedicated", string.Empty).Trim();
serverInfo.GameName = gameName;
if (!SupportedGameNames.Contains(serverInfo.GameName, StringComparer.OrdinalIgnoreCase))
{
throw new ArgumentException($"Unsupported game name {serverInfo.GameName}", nameof(serverInfo));
}
var existingServers = await this.registryStore.GetServerInfos(whereExpression: x => x.HostIpAddress == serverInfo.HostIpAddress);
if (existingServers.GroupBy(g => new { g.HostIpAddress }).Any(g => g.Count() > this.maxServersPerIp))
{
throw new InvalidOperationException("Exceeded maximum servers per IP.");
}
await this.registryStore.UpsertServerInfo(serverInfo);
}
@ -46,16 +57,36 @@
.Take(this.maxServerCount);
}
// Old API, soon to be deprecated
public async Task DeleteServer(string ipAddress)
{
ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress));
ServerInfo serverInfo = await this.registryStore.GetServerInfo(ipAddress);
var serverInfos = await this.registryStore.GetServerInfos(whereExpression: x => x.HostIpAddress == ipAddress);
if (serverInfo != null)
foreach (var serverInfo in serverInfos)
{
await this.registryStore.DeleteServer(serverInfo.id);
}
}
public async Task DeleteServer(string ipAddress, string gameName, int port)
{
ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress));
ArgumentUtility.CheckStringForNullOrEmpty(gameName, nameof(gameName));
ArgumentUtility.CheckForNonnegativeInt(port, nameof(port));
var existingServerInfo = (await this.registryStore.GetServerInfos(
whereExpression:
x => x.HostIpAddress == ipAddress &&
x.Port == port &&
x.GameName.Equals(gameName, StringComparison.OrdinalIgnoreCase)))
.FirstOrDefault();
if (existingServerInfo != null)
{
await this.registryStore.DeleteServer(existingServerInfo.id);
}
}
}
}

View File

@ -41,10 +41,11 @@
bool includeExpired = false,
string partitionKey = null)
{
ConcurrentDictionary<string, ServerInfo> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
ConcurrentDictionary<string, IList<ServerInfo>> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
IQueryable<ServerInfo> serverInfoQuery = serverInfo
.Values
.SelectMany(s => s)
.AsQueryable();
if (whereExpression != null)
@ -67,10 +68,11 @@
Expression<Func<ServerInfo, bool>> whereExpression = null,
string partitionKey = null)
{
ConcurrentDictionary<string, ServerInfo> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
ConcurrentDictionary<string, IList<ServerInfo>> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
IQueryable<ServerInfo> serverInfoQuery = serverInfo
.Values
.SelectMany(s => s)
.AsQueryable();
if (serverInfoQuery != null)
@ -88,65 +90,55 @@
.ToList();
}
public async Task<ServerInfo> GetServerInfo(string ipAddress)
{
ArgumentUtility.CheckStringForNullOrEmpty(ipAddress, nameof(ipAddress));
ConcurrentDictionary<string, ServerInfo> serverInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
if (serverInfo.ContainsKey(ipAddress))
{
try
{
return serverInfo[ipAddress];
}
catch (Exception e)
{
this.logger.LogInformation("Cached server for {IPAddress} no longer found: {Exception}", ipAddress, e.ToString());
// May have been removed from the cache by another thread. Ignore and query DB instead.
}
}
return await this.client.GetItemById<ServerInfo>(ipAddress);
}
public async Task UpsertServerInfo(ServerInfo serverInfo)
{
ArgumentUtility.CheckForNull(serverInfo, nameof(serverInfo));
// Check cache before we write to store
ConcurrentDictionary<string, ServerInfo> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
ConcurrentDictionary<string, IList<ServerInfo>> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
if (allServerInfo.ContainsKey(serverInfo.HostIpAddress))
{
ServerInfo existingServerInfo = allServerInfo[serverInfo.HostIpAddress];
IList<ServerInfo> serverInfoForHostIp = allServerInfo[serverInfo.HostIpAddress];
ServerInfo existingServerInfo = FindExistingServerForHostIp(serverInfoForHostIp, serverInfo);
// DDOS protection: skip write to Cosmos if parameters have not changed,
// or it's not been long enough.
if (existingServerInfo.Equals(serverInfo)
&& Math.Abs((existingServerInfo.LastHeartbeat - serverInfo.LastHeartbeat).TotalMinutes) < ServerRefreshIntervalInMinutes)
if (existingServerInfo != null && Math.Abs((existingServerInfo.LastHeartbeat - serverInfo.LastHeartbeat).TotalMinutes) < ServerRefreshIntervalInMinutes)
{
this.logger.LogInformation("State for {IPAddress} is unchanged. Skipping write to store.", serverInfo.HostIpAddress);
return;
}
this.logger.LogInformation("State for {IPAddress} has changed. Committing update to store.", serverInfo.HostIpAddress);
await this.client.UpsertItem(
item: serverInfo,
partitionKey: new PartitionKey(serverInfo.DocumentType));
// Update cache
if (existingServerInfo != null)
{
var newServerInfo = serverInfoForHostIp.Where(s => !s.Equals(serverInfo)).ToList();
newServerInfo.Add(serverInfo);
allServerInfo[serverInfo.HostIpAddress] = newServerInfo;
this.logger.LogInformation("Updating cache for request from {IPAddress} (replaced existing server).", serverInfo.HostIpAddress);
}
else
{
this.logger.LogInformation("State for {IPAddress} has changed. Committing update to store.", serverInfo.HostIpAddress);
allServerInfo[serverInfo.HostIpAddress].Add(serverInfo);
this.logger.LogInformation("Updating cache for request from {IPAddress} (added new server).", serverInfo.HostIpAddress);
}
}
await this.client.UpsertItem(
item: serverInfo,
partitionKey: new PartitionKey(serverInfo.DocumentType));
this.logger.LogInformation("Updating cache for request from {IPAddress}.", serverInfo.HostIpAddress);
if (allServerInfo.ContainsKey(serverInfo.HostIpAddress))
{
allServerInfo[serverInfo.HostIpAddress] = serverInfo;
}
else
{
allServerInfo.TryAdd(serverInfo.HostIpAddress, serverInfo);
// Update cache
await this.client.UpsertItem(
item: serverInfo,
partitionKey: new PartitionKey(serverInfo.DocumentType));
this.logger.LogInformation("Updating cache for request from {IPAddress} (no existing servers).", serverInfo.HostIpAddress);
allServerInfo.TryAdd(serverInfo.HostIpAddress, new List<ServerInfo>() { serverInfo });
}
}
@ -155,8 +147,21 @@
await this.client.DeleteItem<ServerInfo>(id, partitionKey);
// Remove from cache
ConcurrentDictionary<string, ServerInfo> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
allServerInfo.TryRemove(id, out ServerInfo _);
ConcurrentDictionary<string, IList<ServerInfo>> allServerInfo = await this.memoryCache.GetOrCreateAsync(CacheKeys.ServerInfo, this.PopulateCache);
if (allServerInfo.ContainsKey(id))
{
var serverInfoCopy = allServerInfo[id].Where(s => s.id != id).ToList();
if (!serverInfoCopy.Any())
{
// No remaining servers, remove the key
allServerInfo.TryRemove(id, out IList<ServerInfo> _);
}
else
{
// Shallow-copy and replace to keep thread safety guarantee
allServerInfo[id] = serverInfoCopy;
}
}
}
public async Task DeleteServers(IEnumerable<string> ids, string partitionKey = null)
@ -182,15 +187,31 @@
await this.client.Initialize();
}
private async Task<ConcurrentDictionary<string, ServerInfo>> PopulateCache(ICacheEntry entry)
private async Task<ConcurrentDictionary<string, IList<ServerInfo>>> PopulateCache(ICacheEntry entry)
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
IDictionary<string, ServerInfo> serverInfo =
(await this.client.GetItems<ServerInfo>())
.ToDictionary(x => x.HostIpAddress, y => y);
var allServerInfo = (await this.client.GetItems<ServerInfo>());
var serverInfoDictionary = new ConcurrentDictionary<string, IList<ServerInfo>>();
return new ConcurrentDictionary<string, ServerInfo>(serverInfo);
foreach (var serverInfo in allServerInfo)
{
if (!serverInfoDictionary.ContainsKey(serverInfo.HostIpAddress))
{
serverInfoDictionary[serverInfo.HostIpAddress] = new List<ServerInfo>() { serverInfo };
}
else
{
serverInfoDictionary[serverInfo.HostIpAddress].Add(serverInfo);
}
}
return serverInfoDictionary;
}
private static ServerInfo FindExistingServerForHostIp(IList<ServerInfo> serverInfoForHostIp, ServerInfo candidateServerInfo)
{
return serverInfoForHostIp.FirstOrDefault(s => s.Equals(candidateServerInfo));
}
}
}

View File

@ -13,8 +13,6 @@
Task DeleteServers(IEnumerable<string> ids, string partitionKey = null);
Task<ServerInfo> GetServerInfo(string ipAddress);
Task<IEnumerable<ServerInfo>> GetServerInfos(Expression<Func<ServerInfo, bool>> whereExpression = null, bool includeExpired = false, string partitionKey = null);
Task<IEnumerable<TSelect>> GetServerInfos<TSelect>(

View File

@ -6,6 +6,7 @@
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1")]
[Route("api/[controller]")]
public class CommunityController : ControllerBase
{

View File

@ -13,6 +13,7 @@
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1")]
[Route("api/[controller]")]
public class CrashReportsController : ControllerBase
{

View File

@ -14,6 +14,7 @@ namespace Giants.Web.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1")]
[Route("api/[controller]")]
public class ServersController : ControllerBase
{
@ -44,6 +45,17 @@ namespace Giants.Web.Controllers
await this.serverRegistryService.DeleteServer(requestIpAddress);
}
[HttpDelete]
[MapToApiVersion("1.1")]
public async Task DeleteServer(string gameName, int port)
{
string requestIpAddress = this.GetRequestIpAddress();
this.logger.LogInformation("Deleting server from {IPAddress}", requestIpAddress);
await this.serverRegistryService.DeleteServer(requestIpAddress, gameName, port);
}
[HttpGet]
public async Task<IEnumerable<ServerInfoWithHostAddress>> GetServers()
{

View File

@ -7,6 +7,7 @@ namespace Giants.WebApi.Controllers
{
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("1.1")]
[Route("api/[controller]")]
public class VersionController : ControllerBase
{

View File

@ -12,6 +12,7 @@
"ServerTimeoutPeriodInMinutes": "7",
"ServerCleanupIntervalInMinutes": "1",
"MaxServerCount": 1000,
"MaxServersPerIp": 5,
"DiscordUri": "https://discord.gg/Avj4azU",
"BlobConnectionString": "",
"CrashBlobContainerName": "crashes"

View File

@ -82,6 +82,9 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<PropertyGroup Label="Vcpkg">
<VcpkgEnabled>false</VcpkgEnabled>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>

View File

@ -111,6 +111,7 @@
<EnablePREfast>true</EnablePREfast>
<LanguageStandard>stdcpplatest</LanguageStandard>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@ -165,6 +166,7 @@
<EnablePREfast>true</EnablePREfast>
<LanguageStandard>stdcpplatest</LanguageStandard>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@ -220,7 +222,6 @@
</ClCompile>
<ClCompile Include="ServerDialog.cpp" />
<ClCompile Include="ServerConsoleApp.cpp" />
<ClCompile Include="Utils.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="res\ServerConsole.rc2" />
@ -233,7 +234,6 @@
<ClInclude Include="ServerDialog.h" />
<ClInclude Include="ServerConsoleApp.h" />
<ClInclude Include="targetver.h" />
<ClInclude Include="Utils.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="ServerConsole.rc" />

View File

@ -1,5 +1,4 @@
#include "pch.h"
#include <initguid.h>
#include "ServerConsoleApp.h"
#include "ServerDialog.h"

View File

@ -2,7 +2,6 @@
#include "ServerConsoleApp.h"
#include "ServerDialog.h"
#include "Utils.h"
IMPLEMENT_DYNAMIC(ServerDialog, CDialogEx)
@ -110,10 +109,11 @@ void ServerDialog::OnOK()
void ServerDialog::OnCancel()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
KillTimer((UINT_PTR)this);
ShowWindow(SW_HIDE);
DestroyWindow();
KillTimer((UINT_PTR)this);
m_pParentWnd->SendMessage(WM_CLOSE, 0, 0);
}
void ServerDialog::RefreshPlayers()

View File

@ -1,12 +0,0 @@
#include "pch.h"
#include <codecvt>
#include "Utils.h"
std::wstring to_wstring(const std::string_view& sourceString)
{
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> converter;
return converter.from_bytes(sourceString.data());
}

View File

@ -1,5 +0,0 @@
#pragma once
#include <string>
std::wstring to_wstring(const std::string_view& sourceString);

View File

@ -38,6 +38,9 @@
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup />
<PropertyGroup Label="Vcpkg">
<VcpkgEnabled>false</VcpkgEnabled>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<FxCompile>
<ShaderType>Effect</ShaderType>