LANCommander/LANCommander.SDK/PowerShell/Cmdlets/Edit-PatchGameSpy.cs

190 lines
No EOL
6.9 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Text.RegularExpressions;
using LANCommander.SDK.Helpers;
using Microsoft.Extensions.FileSystemGlobbing;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
namespace LANCommander.SDK.PowerShell.Cmdlets;
[Cmdlet(VerbsData.Edit, "PatchGameSpy")]
public class EditPatchGameSpy : BaseCmdlet
{
private const string GAMESPY_HOSTNAME = "gamespy.com";
private const string GAMESPY_PUBLICKEY = "BF05D63E93751AD4A59A4A7389CF0BE8A22CCDEEA1E7F12C062D6E194472EFDA5184CCECEB4FBADF5EB1D7ABFE91181453972AA971F624AF9BA8F0F82E2869FB7D44BDE8D56EE50977898F3FEE75869622C4981F07506248BD3D092E8EA05C12B2FA37881176084C8F8B8756C4722CDC57D2AD28ACD3AD85934FB48D6B2D2027";
private const string OPENSPY_HOSTNAME = "openspy.net";
private const string OPENSPY_PUBLICKEY = "afb5818995b3708d0656a5bdd20760aee76537907625f6d23f40bf17029e56808d36966c0804e1d797e310fedd8c06e6c4121d963863d765811fc9baeb2315c9a6eaeb125fad694d9ea4d4a928f223d9f4514533f18a5432dd0435c5c6ac8e276cf29489cb5ac880f16b0d7832ee927d4e27d622d6a450cd1560d7fa882c6c13";
[Parameter(Mandatory = true, Position = 0)]
public string Path { get; set; }
[Parameter(Mandatory = false, Position = 1)]
public string Hostname { get; set; } = OPENSPY_HOSTNAME;
[Parameter(Mandatory = false, Position = 2)]
public string PublicKey { get; set; } = OPENSPY_PUBLICKEY;
[Parameter(Mandatory = false, Position = 3)]
public string[] BinariesToPatch { get; set; } = { "*.dll", "*.exe", "*.ini" };
[Parameter(Mandatory = false, Position = 4)]
public string[] TextFilesToPatch { get; set; } = { "*.ini" };
protected override void ProcessRecord()
{
var binaryMatcher = new Matcher();
var textMatcher = new Matcher();
binaryMatcher.AddIncludePatterns(BinariesToPatch);
textMatcher.AddIncludePatterns(TextFilesToPatch);
var binaryResults = binaryMatcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(Path)));
var textResults = textMatcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(Path)));
if (binaryResults.HasMatches)
foreach (var binary in binaryResults.Files)
PatchBinary(binary.Path);
if (textResults.HasMatches)
foreach (var text in textResults.Files)
PatchText(text.Path);
}
private void PatchText(string path)
{
#region UT99 Games
// https://openspy.net/howto/ut99-engine/ut
//
// 1) Replace GameSpy master server with OpenSpy anywhere it appears
// (covers both [UBrowserAll] and [Engine.GameEngine] ServerActors lines)
TextFileHelper.ReplaceAll(
path,
@"MasterServerAddress\s*=\s*master0\.gamespy\.com",
$"MasterServerAddress=master.{Hostname}",
RegexOptions.IgnoreCase
);
// 2) Ensure bFallbackFactories=False inside [UBrowserAll] block
// This pattern:
// - Finds [UBrowserAll]
// - Consumes everything until the next [Section]
// - Finds "bFallbackFactories = True" inside that block
// - Replaces only the 'True' with 'False'
TextFileHelper.ReplaceAll(
path,
@"(\[UBrowserAll\][^\[]*?bFallbackFactories\s*=\s*)True",
"$1False",
RegexOptions.IgnoreCase | RegexOptions.Singleline
);
#endregion
#region Unreal 2 Games
// https://openspy.net/howto/ut2k-engine/ut2
//
// This pattern:
// - Captures the [IpDrv.MasterServerLink] line + its newline in group 1
// - Then consumes one or more MasterServerList=... lines after it
// - We replace that whole chunk with the header + a single OpenSpy entry
TextFileHelper.ReplaceAll(
path,
@"(\[IpDrv\.MasterServerLink\]\s*\r?\n)(?:\s*MasterServerList=.*\r?\n)+",
$"$1MasterServerList=(Address=\"utmaster.{Hostname}\",Port=28902)\r\n",
RegexOptions.IgnoreCase
);
#endregion
}
private void PatchBinary(string path)
{
if (Hostname.Length != GAMESPY_HOSTNAME.Length)
throw new ArgumentOutOfRangeException(nameof(Hostname),
$"Hostname must be {GAMESPY_HOSTNAME.Length} characters long");
if (PublicKey.Length != GAMESPY_PUBLICKEY.Length)
throw new ArgumentOutOfRangeException(nameof(PublicKey),
$"Public key must be {GAMESPY_PUBLICKEY.Length} characters long");
PatchBinary(path, GAMESPY_HOSTNAME, Hostname);
PatchBinary(path, OPENSPY_PUBLICKEY, PublicKey);
// TODO: AuthService requires a null character to be inserted at the end of the string
// PatchBinary(path, "https://%s.auth.pubsvs", "http://%s.auth.pubsvs");
}
private void PatchBinary(string path, string pattern, string replacement)
{
IEnumerable<long> offsets;
byte[] replacementData = Encoding.UTF8.GetBytes(replacement);
using (var fs = new FileStream(path, FileMode.Open))
{
offsets = IndexOf(fs, pattern);
if (!offsets.Any())
return;
}
using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite))
{
foreach (var offset in offsets)
{
fs.Seek(offset, SeekOrigin.Begin);
fs.Write(replacementData, 0, replacementData.Length);
}
}
}
private IEnumerable<long> IndexOf(Stream stream, string pattern, int bufferSize = 8192)
=> IndexOf(stream, Encoding.UTF8.GetBytes(pattern), bufferSize);
private IEnumerable<long> IndexOf(Stream stream, byte[] pattern, int bufferSize = 8192)
{
var buffer = new byte[bufferSize + pattern.Length - 1];
long offset = 0;
int tail = 0;
while (true)
{
int bytesRead = stream.Read(buffer, tail, bufferSize);
if (bytesRead == 0)
break;
int totalInBuffer = tail + bytesRead;
int searchLimit = totalInBuffer - pattern.Length + 1;
for (int i = 0; i < searchLimit; i++)
{
int j = 0;
for (; j < totalInBuffer; j++)
{
if (buffer[i + j] != pattern[j])
break;
}
if (j == pattern.Length)
{
yield return offset + i;
}
}
tail = Math.Min(pattern.Length - 1, totalInBuffer);
{
Buffer.BlockCopy(
buffer,
totalInBuffer - tail,
buffer,
0,
tail);
}
offset += totalInBuffer - tail;
}
}
}