Skip to content

Commit

Permalink
some infrastructure stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
AnakinRaW committed May 30, 2024
1 parent 60a4ed1 commit 74c92fb
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 42 deletions.
4 changes: 2 additions & 2 deletions src/ModVerify.CliApp/ModVerify.CliApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AlamoEngineTools.PG.StarWarsGame.Infrastructure.Clients" Version="3.0.6" />
<PackageReference Include="AlamoEngineTools.SteamAbstraction" Version="3.0.6" />
<PackageReference Include="AlamoEngineTools.PG.StarWarsGame.Infrastructure.Clients" Version="3.1.5" />
<PackageReference Include="AlamoEngineTools.SteamAbstraction" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
Expand Down
2 changes: 1 addition & 1 deletion src/ModVerify.CliApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ private static VerifyFocPipeline BuildPipeline(IPlayableObject playableObject, I
playableObject.Game.Directory.FullName,
fallbackGame.Directory.FullName);

return new VerifyFocPipeline(gameLocations, _services);
return new VerifyFocPipeline(gameLocations, VerificationSettings.Default, _services);
}

private static IServiceProvider CreateAppServices()
Expand Down
4 changes: 4 additions & 0 deletions src/ModVerify/ModVerify.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@

<ItemGroup>
<PackageReference Include="AnakinRaW.CommonUtilities.SimplePipeline" Version="12.0.2-beta" />
<PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 3 additions & 1 deletion src/ModVerify/Steps/GameVerificationStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public abstract class GameVerificationStep(CreateGameDatabaseStep createDatabase

protected abstract string LogFileName { get; }

public abstract string Name { get; }

protected sealed override void RunCore(CancellationToken token)
{
createDatabaseStep.Wait();
Expand Down Expand Up @@ -78,7 +80,7 @@ protected void GuardedVerify(Action action, Predicate<Exception> handleException
}
private StreamWriter CreateVerificationLogFile()
{
var fileName = $"VerifyLog_{LogFileName}.txt";
var fileName = $"VerifyResult_{LogFileName}.txt";
var fs = FileSystem.FileStream.New(fileName, FileMode.Create, FileAccess.Write, FileShare.Read);
return new StreamWriter(fs);
}
Expand Down
6 changes: 4 additions & 2 deletions src/ModVerify/Steps/VerifyReferencedModelsStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

namespace AET.ModVerify.Steps;

internal class VerifyReferencedModelsStep(CreateGameDatabaseStep createDatabaseStep, IGameRepository repository, IServiceProvider serviceProvider)
internal sealed class VerifyReferencedModelsStep(CreateGameDatabaseStep createDatabaseStep, IGameRepository repository, IServiceProvider serviceProvider)
: GameVerificationStep(createDatabaseStep, repository, serviceProvider)
{
public const string ModelNotFound = "ALO00";
Expand All @@ -32,7 +32,9 @@ internal class VerifyReferencedModelsStep(CreateGameDatabaseStep createDatabaseS

private readonly IAloFileService _modelFileService = serviceProvider.GetRequiredService<IAloFileService>();

protected override string LogFileName => "ModelTextureShader";
protected override string LogFileName => "Model";

public override string Name => "Model";

protected override void RunVerification(CancellationToken token)
{
Expand Down
52 changes: 45 additions & 7 deletions src/ModVerify/VerifyPipeline.cs
Original file line number Diff line number Diff line change
@@ -1,23 +1,42 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AET.ModVerify.Steps;
using AnakinRaW.CommonUtilities.SimplePipeline;
using Microsoft.Extensions.Logging;
using PG.StarWarsGame.Engine.FileSystem;
using PG.StarWarsGame.Engine.Pipeline;

namespace AET.ModVerify;

public class VerifyFocPipeline(GameLocations gameLocations, IServiceProvider serviceProvider)
: ParallelPipeline(serviceProvider, 4, false)

public record VerificationSettings
{
public static readonly VerificationSettings Default = new();

public bool ThrowOnError { get; init; }
}


public class VerifyFocPipeline : ParallelPipeline
{
private IList<GameVerificationStep> _verificationSteps = null!;
private readonly GameLocations _gameLocations;
private readonly VerificationSettings _settings;

public VerifyFocPipeline(GameLocations gameLocations, VerificationSettings settings, IServiceProvider serviceProvider)
: base(serviceProvider, 4, false)
{
_gameLocations = gameLocations;
_settings = settings;
}

protected override Task<IList<IStep>> BuildSteps()
{
var repository = new FocGameRepository(gameLocations, ServiceProvider);
var repository = new FocGameRepository(_gameLocations, ServiceProvider);

var buildIndexStep = new CreateGameDatabaseStep(repository, ServiceProvider);
_verificationSteps = new List<GameVerificationStep>
Expand All @@ -36,10 +55,29 @@ protected override Task<IList<IStep>> BuildSteps()

public override async Task RunAsync(CancellationToken token = default)
{
await base.RunAsync(token).ConfigureAwait(false);
Logger?.LogInformation("Verifying game...");
try
{
await base.RunAsync(token).ConfigureAwait(false);

var stepsWithVerificationErrors = _verificationSteps.Where(x => x.VerifyErrors.Any()).ToList();
if (stepsWithVerificationErrors.Any())
throw new GameVerificationException(stepsWithVerificationErrors);
var stepsWithVerificationErrors = _verificationSteps.Where(x => x.VerifyErrors.Any()).ToList();

var failedSteps = new List<GameVerificationStep>();
foreach (var verificationStep in _verificationSteps)
{
if (verificationStep.VerifyErrors.Any())
{
failedSteps.Add(verificationStep);
Logger?.LogWarning($"Verifier '{verificationStep.Name}' reported errors!");
}
}

if (_settings.ThrowOnError && failedSteps.Count > 0)
throw new GameVerificationException(stepsWithVerificationErrors);
}
finally
{
Logger?.LogInformation("Finished game verification");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,38 @@ namespace PG.StarWarsGame.Engine.Language;

internal sealed class GameLanguageManager(IServiceProvider serviceProvider) : ServiceBase(serviceProvider), IGameLanguageManager
{
private static readonly IDictionary<LanguageType, string> LanguageToFileSuffixMap =
private static readonly IDictionary<LanguageType, string> LanguageToFileSuffixMapMp3 =
new Dictionary<LanguageType, string>
{
{ LanguageType.English, "_ENG"},
{ LanguageType.German, "_GER"},
{ LanguageType.French, "_FRE"},
{ LanguageType.Spanish, "_SPA"},
{ LanguageType.Italian, "_ITA"},
{ LanguageType.Japanese, "_JAP"},
{ LanguageType.Korean, "_KOR"},
{ LanguageType.Chinese, "_CHI"},
{ LanguageType.Russian, "_RUS"},
{ LanguageType.Polish, "_POL"},
{ LanguageType.Thai, "_THA"},
{ LanguageType.English, "_ENG.MP3" },
{ LanguageType.German, "_GER.MP3" },
{ LanguageType.French, "_FRE.MP3" },
{ LanguageType.Spanish, "_SPA.MP3" },
{ LanguageType.Italian, "_ITA.MP3" },
{ LanguageType.Japanese, "_JAP.MP3" },
{ LanguageType.Korean, "_KOR.MP3" },
{ LanguageType.Chinese, "_CHI.MP3" },
{ LanguageType.Russian, "_RUS.MP3" },
{ LanguageType.Polish, "_POL.MP3" },
{ LanguageType.Thai, "_THA.MP3" },
};


private static readonly IDictionary<LanguageType, string> LanguageToFileSuffixMapWav =
new Dictionary<LanguageType, string>
{
{ LanguageType.English, "_ENG.WAV" },
{ LanguageType.German, "_GER.WAV" },
{ LanguageType.French, "_FRE.WAV" },
{ LanguageType.Spanish, "_SPA.WAV" },
{ LanguageType.Italian, "_ITA.WAV" },
{ LanguageType.Japanese, "_JAP.WAV" },
{ LanguageType.Korean, "_KOR.WAV" },
{ LanguageType.Chinese, "_CHI.WAV" },
{ LanguageType.Russian, "_RUS.WAV" },
{ LanguageType.Polish, "_POL.WAV" },
{ LanguageType.Thai, "_THA.WAV" },
};

public IReadOnlyCollection<LanguageType> FocSupportedLanguages { get; } =
new HashSet<LanguageType>
{
Expand All @@ -41,38 +57,100 @@ internal sealed class GameLanguageManager(IServiceProvider serviceProvider) : Se
LanguageType.Polish, LanguageType.Thai
};


public bool IsFileNameLocalizable(string fileName, bool requiredEnglishName)
{
var fileSpan = fileName.AsSpan();

if (requiredEnglishName)
{
if (fileSpan.EndsWith("_ENG.WAV".AsSpan(), StringComparison.OrdinalIgnoreCase))
return true;
if (fileSpan.EndsWith("_ENG.MP3".AsSpan(), StringComparison.OrdinalIgnoreCase))
return true;
return false;
}

var isWav = fileSpan.EndsWith(".WAV".AsSpan(), StringComparison.OrdinalIgnoreCase);
var isMp3 = fileSpan.EndsWith(".MP3".AsSpan(), StringComparison.OrdinalIgnoreCase);

ICollection<string>? checkList = null;
if (isWav)
checkList = LanguageToFileSuffixMapWav.Values;
else if (isMp3)
checkList = LanguageToFileSuffixMapMp3.Values;

if (checkList is null)
return false;

foreach (var toCheck in checkList)
{
if (fileSpan.EndsWith(toCheck.AsSpan(), StringComparison.OrdinalIgnoreCase))
return true;
}

return false;
}


public string LocalizeFileName(string fileName, LanguageType language, out bool localized)
{
if (fileName.Length > 260)
throw new ArgumentOutOfRangeException(nameof(fileName), "fileName is too long");

localized = true;

// The game assumes that all localized audio files are referenced by using their english name.
// Thus, PG takes this shortcut
if (language == LanguageType.English)
return fileName;

var upperFileName = fileName.ToUpper();
var fileSpan = fileName.AsSpan();

var isWav = false;
var isMp3 = false;

var fileSpan = upperFileName.AsSpan();
// The game only localizes file names iff they have the english suffix
// NB: Also note that the engine does *not* check whether the filename actually ends with this suffix
// but instead only take the first occurrence. This means that a file name like
// 'test_eng.wav_ger.wav'
// will trick the algorithm.
var engSuffixIndex = fileSpan.IndexOf("_ENG.WAV".AsSpan(), StringComparison.OrdinalIgnoreCase);
if (engSuffixIndex != -1)
isWav = true;

#if NETSTANDARD2_1_OR_GREATER || NET
var extension = FileSystem.Path.GetExtension(fileSpan);
#else
var extension = FileSystem.Path.GetExtension(upperFileName).AsSpan();
#endif
if (!isWav)
{
engSuffixIndex = fileSpan.IndexOf("_ENG.MP3".AsSpan(), StringComparison.OrdinalIgnoreCase);
if (engSuffixIndex != -1)
isMp3 = true;
}

var withoutExtension = fileSpan.Slice(0, fileSpan.Length - extension.Length);
var engSuffixIndex = withoutExtension.LastIndexOf("_ENG".AsSpan());

if (engSuffixIndex == -1 ||
(!extension.Equals(".MP3".AsSpan(), StringComparison.Ordinal) &&
!extension.Equals(".WAV".AsSpan(), StringComparison.Ordinal)))
if (engSuffixIndex == -1)
{
localized = false;
Logger.LogTrace($"Unable to localize '{fileName}'");
Logger.LogWarning($"Unable to localize '{fileName}'");
return fileName;
}

var withoutSuffix = withoutExtension.Slice(0, engSuffixIndex);
var localizedSuffix = LanguageToFileSuffixMap[language].AsSpan();
var withoutSuffix = fileSpan.Slice(0, engSuffixIndex);

ReadOnlySpan<char> newLocalizedSuffix;
if (isWav)
newLocalizedSuffix = LanguageToFileSuffixMapWav[language].AsSpan();
else if (isMp3)
newLocalizedSuffix = LanguageToFileSuffixMapMp3[language].AsSpan();
else
throw new InvalidOperationException();

// 260 is roughly the MAX file path size on default Windows,
// so we don't expect game files to be larger.
var sb = new ValueStringBuilder(stackalloc char[260]);

sb.Append(withoutSuffix);
sb.Append(newLocalizedSuffix);

return StringUtilities.Concat(withoutSuffix, localizedSuffix, extension);
return sb.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface IGameLanguageManager
IReadOnlyCollection<LanguageType> EawSupportedLanguages { get; }

string LocalizeFileName(string fileName, LanguageType language, out bool localized);

bool IsFileNameLocalizable(string fileName, bool requireEnglishName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace PG.StarWarsGame.Engine.Utilities;

// From https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/ValueStringBuilder.cs
internal ref struct ValueStringBuilder(Span<char> initialBuffer)
{
private Span<char> _chars = initialBuffer;
private int _pos = 0;

public override string ToString()
{
return _chars.Slice(0, _pos).ToString();
}

public void Append(scoped ReadOnlySpan<char> value)
{
var pos = _pos;
if (pos > _chars.Length - value.Length)
throw new InvalidOperationException("Value string builder is too small.");

value.CopyTo(_chars.Slice(_pos));
_pos += value.Length;
}
}

0 comments on commit 74c92fb

Please sign in to comment.