Skip to content

Commit 3499030

Browse files
committed
Refactor: Replace generic filesystem abstraction with PetroglyphFileSystem for consistent, cross-platform path handling across the engine.
1 parent e40030b commit 3499030

14 files changed

Lines changed: 130 additions & 234 deletions

src/PetroglyphTools/PG.StarWarsGame.Engine/CommandBar/CommandBarGameManager_Initialization.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ private void SetMegaTexture()
196196
if (Components.FirstOrDefault(x => x is CommandBarShellComponent) is null)
197197
return;
198198
// Note: The tag <Mega_Texture_Name> is not used by the engine
199-
var mtdPath = FileSystem.Path.Combine("DATA\\ART\\TEXTURES", $"{CommandBarConstants.MegaTextureBaseName}.mtd");
199+
var mtdPath = PGFileSystem.CombinePath("DATA\\ART\\TEXTURES", $"{CommandBarConstants.MegaTextureBaseName}.mtd");
200200
using var megaTexture = GameRepository.TryOpenFile(mtdPath);
201201

202202
try
@@ -211,7 +211,7 @@ private void SetMegaTexture()
211211
}
212212

213213
GameRepository.TextureRepository.FileExists($"{CommandBarConstants.MegaTextureBaseName}.tga", false, out _, out var actualFilePath);
214-
MegaTextureFileName = FileSystem.Path.GetFileName(actualFilePath);
214+
MegaTextureFileName = PGFileSystem.GetFileName(actualFilePath);
215215
}
216216

217217
private void SetComponentGroup(IEnumerable<CommandBarBaseComponent> components)

src/PetroglyphTools/PG.StarWarsGame.Engine/GameManagerBase.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.IO.Abstractions;
43
using System.Threading;
54
using System.Threading.Tasks;
65
using AnakinRaW.CommonUtilities.Collections;
76
using Microsoft.Extensions.DependencyInjection;
87
using Microsoft.Extensions.Logging;
98
using PG.Commons.Hashing;
109
using PG.StarWarsGame.Engine.ErrorReporting;
10+
using PG.StarWarsGame.Engine.IO;
1111
using PG.StarWarsGame.Engine.IO.Repositories;
1212

1313
namespace PG.StarWarsGame.Engine;
@@ -37,7 +37,9 @@ internal abstract class GameManagerBase
3737
private bool _initialized;
3838
private protected readonly GameRepository GameRepository;
3939
protected readonly IServiceProvider ServiceProvider;
40-
protected readonly IFileSystem FileSystem;
40+
41+
// ReSharper disable once InconsistentNaming
42+
protected readonly PetroglyphFileSystem PGFileSystem;
4143
protected readonly ILogger? Logger;
4244

4345
protected readonly GameEngineErrorReporterWrapper ErrorReporter;
@@ -55,7 +57,7 @@ protected GameManagerBase(
5557
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
5658
EngineType = repository.EngineType;
5759
Logger = serviceProvider.GetService<ILoggerFactory>()?.CreateLogger(GetType());
58-
FileSystem = serviceProvider.GetRequiredService<IFileSystem>();
60+
PGFileSystem = repository.PGFileSystem;
5961
ErrorReporter = errorReporter ?? throw new ArgumentNullException(nameof(errorReporter));
6062
}
6163

src/PetroglyphTools/PG.StarWarsGame.Engine/GameObjects/GameObjectTypeGameManager.Initialization.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ private void ParseGameObjectDatabases()
2929
}, ServiceProvider, ErrorReporter);
3030

3131
var xmlFileList = gameParser.ParseFileList(@"DATA\XML\GAMEOBJECTFILES.XML").Files
32-
.Select(x => FileSystem.Path.Combine(@".\DATA\XML\", x))
32+
.Select(x => PGFileSystem.CombinePath(@".\DATA\XML\", x))
3333
.Where(VerifyFilePathLength)
3434
.ToList();
3535

@@ -111,7 +111,7 @@ private void PostLoadFixup()
111111

112112
private bool IsSameFile(string filePathA, string filePathB)
113113
{
114-
return FileSystem.Path.AreEqual(filePathA, filePathB);
114+
return PGFileSystem.PathsAreEqual(filePathA, filePathB);
115115
}
116116

117117
private void OnGameObjectParsed(object sender, GameObjectParsedEventArgs e)

src/PetroglyphTools/PG.StarWarsGame.Engine/GuiDialog/GuiDialogGameManager_Initialization.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ private void InitializeMegaTextures(GuiDialogsXmlTextureData guiDialogs)
145145
}
146146
else
147147
{
148-
var mtdPath = FileSystem.Path.Combine("DATA\\ART\\TEXTURES", $"{guiDialogs.MegaTexture}.mtd");
148+
var mtdPath = PGFileSystem.CombinePath("DATA\\ART\\TEXTURES", $"{guiDialogs.MegaTexture}.mtd");
149149

150150
if (mtdPath.Length > MegaTextureMaxFilePathLength)
151151
{

src/PetroglyphTools/PG.StarWarsGame.Engine/IO/IGameRepository.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ public interface IGameRepository : IRepository
77
/// <summary>
88
/// Gets the full qualified path of this repository with a trailing directory separator
99
/// </summary>
10-
public string Path { get; }
10+
string Path { get; }
11+
12+
// ReSharper disable once InconsistentNaming
13+
PetroglyphFileSystem PGFileSystem { get; }
1114

12-
public GameEngineType EngineType { get; }
15+
GameEngineType EngineType { get; }
1316

1417
IRepository EffectsRepository { get; }
1518

src/PetroglyphTools/PG.StarWarsGame.Engine/IO/MultiPassRepository.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
using Microsoft.Extensions.DependencyInjection;
2-
using PG.StarWarsGame.Engine.IO.Repositories;
1+
using PG.StarWarsGame.Engine.IO.Repositories;
32
using PG.StarWarsGame.Engine.Utilities;
43
using System;
54
using System.Diagnostics.CodeAnalysis;
65
using System.IO;
7-
using System.IO.Abstractions;
86

97
namespace PG.StarWarsGame.Engine.IO;
108

11-
internal abstract class MultiPassRepository(GameRepository baseRepository, IServiceProvider serviceProvider) : IRepository
9+
internal abstract class MultiPassRepository(GameRepository baseRepository) : IRepository
1210
{
13-
protected readonly IFileSystem FileSystem = serviceProvider.GetRequiredService<IFileSystem>();
1411
protected readonly GameRepository BaseRepository = baseRepository;
15-
12+
1613
public Stream OpenFile(string filePath, bool megFileOnly = false)
1714
{
1815
return OpenFile(filePath.AsSpan(), megFileOnly);

src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/EffectsRepository.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
using System;
2-
using PG.StarWarsGame.Engine.IO.Utilities;
32
using PG.StarWarsGame.Engine.Utilities;
43

54
namespace PG.StarWarsGame.Engine.IO.Repositories;
65

7-
internal class EffectsRepository(GameRepository baseRepository, IServiceProvider serviceProvider)
8-
: MultiPassRepository(baseRepository, serviceProvider)
6+
internal class EffectsRepository(GameRepository baseRepository) : MultiPassRepository(baseRepository)
97
{
108
private static readonly string[] LookupPaths =
119
[
@@ -73,7 +71,7 @@ private FileFoundInfo FindEffect(
7371
multiPassStringBuilder.Length = 0;
7472

7573
if (directory != ReadOnlySpan<char>.Empty)
76-
FileSystem.Path.Join(directory, strippedName, ref multiPassStringBuilder);
74+
BaseRepository.PGFileSystem.JoinPath(directory, strippedName, ref multiPassStringBuilder);
7775
else
7876
multiPassStringBuilder.Append(strippedName);
7977

src/PetroglyphTools/PG.StarWarsGame.Engine/IO/Repositories/FocGameRepository.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ public FocGameRepository(GameLocations gameLocations, GameEngineErrorReporterWra
2525
if (firstFallback is not null)
2626
{
2727
var eawMegs = LoadMegArchivesFromXml(firstFallback);
28-
var eawPatch = LoadMegArchive(FileSystem.Path.Combine(firstFallback, "Data\\Patch.meg"));
29-
var eawPatch2 = LoadMegArchive(FileSystem.Path.Combine(firstFallback, "Data\\Patch2.meg"));
30-
var eaw64Patch = LoadMegArchive(FileSystem.Path.Combine(firstFallback, "Data\\64Patch.meg"));
28+
var eawPatch = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path.Combine(firstFallback, "Data/Patch.meg"));
29+
var eawPatch2 = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path.Combine(firstFallback, "Data/Patch2.meg"));
30+
var eaw64Patch = LoadMegArchive(PGFileSystem.UnderlyingFileSystem.Path.Combine(firstFallback, "Data/64Patch.meg"));
3131

3232
megsToConsider.AddRange(eawMegs);
3333
if (eawPatch is not null)
@@ -39,9 +39,9 @@ public FocGameRepository(GameLocations gameLocations, GameEngineErrorReporterWra
3939
}
4040

4141
var focOrModMegs = LoadMegArchivesFromXml(".");
42-
var focPatch = LoadMegArchive("Data\\Patch.meg");
43-
var focPatch2 = LoadMegArchive("Data\\Patch2.meg");
44-
var foc64Patch = LoadMegArchive("Data\\64Patch.meg");
42+
var focPatch = LoadMegArchive("Data/Patch.meg");
43+
var focPatch2 = LoadMegArchive("Data/Patch2.meg");
44+
var foc64Patch = LoadMegArchive("Data/64Patch.meg");
4545

4646
megsToConsider.AddRange(focOrModMegs);
4747
if (focPatch is not null)
Lines changed: 16 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
using AnakinRaW.CommonUtilities.FileSystem;
2-
using Microsoft.Extensions.Logging;
3-
using PG.StarWarsGame.Engine.IO.Utilities;
1+
using Microsoft.Extensions.Logging;
42
using PG.StarWarsGame.Engine.Utilities;
53
using PG.StarWarsGame.Files.MEG.Binary;
64
using System;
75
using System.Collections.Generic;
86
using System.Diagnostics;
97
using System.Diagnostics.CodeAnalysis;
108
using System.IO;
11-
using System.Runtime.CompilerServices;
12-
using System.Runtime.InteropServices;
139

1410
namespace PG.StarWarsGame.Engine.IO.Repositories;
1511

@@ -21,7 +17,7 @@ public bool FileExists(string filePath, string[] extensions, bool megFileOnly =
2117
{
2218
foreach (var extension in extensions)
2319
{
24-
var newPath = FileSystem.Path.ChangeExtension(filePath, extension);
20+
var newPath = PGFileSystem.ChangeExtension(filePath, extension);
2521
if (FileExists(newPath, megFileOnly))
2622
return true;
2723
}
@@ -89,7 +85,14 @@ public Stream OpenFile(ReadOnlySpan<char> filePath, bool megFileOnly = false)
8985
sb.Dispose();
9086
return fileStream;
9187
}
92-
88+
89+
/// <summary>
90+
/// The core routine for finding a file using the game's specific lookup rules.
91+
/// </summary>
92+
/// <param name="filePath">The file path.</param>
93+
/// <param name="pathStringBuilder">The string builder used for constructing the file path.</param>
94+
/// <param name="megFileOnly">Whether to only search for files in MEG archives.</param>
95+
/// <returns>The file found information.</returns>
9396
protected internal abstract FileFoundInfo FindFile(ReadOnlySpan<char> filePath,
9497
ref ValueStringBuilder pathStringBuilder, bool megFileOnly = false);
9598

@@ -99,11 +102,11 @@ protected FileFoundInfo GetFileInfoFromMasterMeg(ReadOnlySpan<char> filePath)
99102

100103
var sb = new ValueStringBuilder(stackalloc char[Math.Max(filePath.Length, PGConstants.MaxMegEntryPathLength)]);
101104
sb.Append(filePath);
102-
NormalizePath(ref sb);
105+
PGFileSystem.NormalizePath(ref sb);
103106

104107
if (sb.Length > PGConstants.MaxMegEntryPathLength)
105108
{
106-
Logger.LogWarning("Trying to open a MEG entry which is longer than 259 characters: '{FileName}'", sb.ToString());
109+
_logger.LogWarning("Trying to open a MEG entry which is longer than 259 characters: '{FileName}'", sb.ToString());
107110
sb.Dispose();
108111
return default;
109112
}
@@ -129,41 +132,8 @@ protected FileFoundInfo GetFileInfoFromMasterMeg(ReadOnlySpan<char> filePath)
129132

130133
protected FileFoundInfo FindFileCore(ReadOnlySpan<char> filePath, ref ValueStringBuilder stringBuilder)
131134
{
132-
bool exists;
133-
134-
stringBuilder.Length = 0;
135-
136-
if (FileSystem.Path.IsPathFullyQualified(filePath))
137-
stringBuilder.Append(filePath);
138-
else
139-
FileSystem.Path.Join(GameDirectory.AsSpan(), filePath, ref stringBuilder);
140-
141-
142-
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
143-
{
144-
NormalizePath(ref stringBuilder);
145-
146-
var actualFilePath = stringBuilder.AsSpan();
147-
exists = FileSystemPathExistsCaseInsensitive(actualFilePath, ref stringBuilder);
148-
}
149-
else
150-
{
151-
// We *could* also use the slightly faster GetFileAttributesA.
152-
// However, CreateFileA and GetFileAttributesA are implemented complete independent.
153-
// The game uses CreateFileA.
154-
// Thus, we should stick to what the game uses in order to be as close to the engine as possible
155-
// NB: It's also important that the string builder is zero-terminated, as otherwise CreateFileA might get invalid data.
156-
var fileHandle = CreateFile(
157-
in stringBuilder.GetPinnableReference(true),
158-
FileAccess.Read,
159-
FileShare.Read,
160-
IntPtr.Zero,
161-
FileMode.Open,
162-
FileAttributes.Normal, IntPtr.Zero);
163-
164-
exists = IsValidAndClose(fileHandle);
165-
}
166-
return !exists ? new FileFoundInfo() : new FileFoundInfo(stringBuilder.AsSpan());
135+
var exists = PGFileSystem.FileExists(filePath, ref stringBuilder, GameDirectory.AsSpan());
136+
return !exists ? default : new FileFoundInfo(stringBuilder.AsSpan());
167137
}
168138

169139
protected FileFoundInfo FileFromAltExists(ReadOnlySpan<char> filePath, IList<string> fallbackPaths, ref ValueStringBuilder pathStringBuilder)
@@ -179,7 +149,7 @@ protected FileFoundInfo FileFromAltExists(ReadOnlySpan<char> filePath, IList<str
179149
{
180150
pathStringBuilder.Length = 0;
181151

182-
FileSystem.Path.Join(fallbackPath.AsSpan(), pathWithNormalizedData, ref pathStringBuilder);
152+
PGFileSystem.JoinPath(fallbackPath.AsSpan(), pathWithNormalizedData, ref pathStringBuilder);
183153
var newPath = pathStringBuilder.AsSpan();
184154

185155
var fileFoundInfo = FindFileCore(newPath, ref pathStringBuilder);
@@ -190,79 +160,6 @@ protected FileFoundInfo FileFromAltExists(ReadOnlySpan<char> filePath, IList<str
190160
return default;
191161
}
192162

193-
private static int NormalizePath(Span<char> path)
194-
{
195-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
196-
return path.Length;
197-
198-
var writePos = 0;
199-
var lastWasSeparator = false;
200-
for (var i = 0; i < path.Length; i++)
201-
{
202-
var c = path[i];
203-
var isSeparator = c is '\\' or '/';
204-
if (isSeparator && lastWasSeparator)
205-
continue;
206-
path[writePos++] = isSeparator ? '/' : c;
207-
lastWasSeparator = isSeparator;
208-
}
209-
210-
return writePos;
211-
}
212-
213-
private static void NormalizePath(ref ValueStringBuilder stringBuilder)
214-
{
215-
stringBuilder.Length = NormalizePath(stringBuilder.RawChars.Slice(0, stringBuilder.Length));
216-
}
217-
218-
private bool FileSystemPathExistsCaseInsensitive(ReadOnlySpan<char> filePath, ref ValueStringBuilder stringBuilder)
219-
{
220-
var pathString = filePath.ToString();
221-
if (FileSystem.File.Exists(pathString))
222-
return true;
223-
224-
var directory = FileSystem.Path.GetDirectoryName(pathString);
225-
var fileName = FileSystem.Path.GetFileName(pathString);
226-
227-
if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(fileName))
228-
return false;
229-
230-
if (!FileSystem.Directory.Exists(directory))
231-
{
232-
if (!FileSystemPathExistsCaseInsensitive(directory.AsSpan(), ref stringBuilder))
233-
return false;
234-
235-
directory = stringBuilder.AsSpan().ToString();
236-
}
237-
238-
var files = FileSystem.Directory.GetFiles(directory);
239-
var directories = FileSystem.Directory.GetDirectories(directory);
240-
241-
foreach (var file in files)
242-
{
243-
var name = FileSystem.Path.GetFileName(file);
244-
if (name.Equals(fileName, StringComparison.OrdinalIgnoreCase))
245-
{
246-
stringBuilder.Length = 0;
247-
stringBuilder.Append(file);
248-
return true;
249-
}
250-
}
251-
252-
foreach (var dir in directories)
253-
{
254-
var name = FileSystem.Path.GetFileName(dir);
255-
if (name.Equals(fileName, StringComparison.OrdinalIgnoreCase))
256-
{
257-
stringBuilder.Length = 0;
258-
stringBuilder.Append(dir);
259-
return true;
260-
}
261-
}
262-
263-
return false;
264-
}
265-
266163
private static bool PathStartsWithDataDirectory(ReadOnlySpan<char> path, out int cutoffLength)
267164
{
268165
cutoffLength = 0;
@@ -288,29 +185,6 @@ private static bool PathStartsWithDataDirectory(ReadOnlySpan<char> path, out int
288185
if (fileFoundInfo.InMeg)
289186
return _megExtractor.GetData(fileFoundInfo.MegDataEntryReference.Location);
290187

291-
return FileSystem.FileStream.New(fileFoundInfo.FilePath.ToString(), FileMode.Open, FileAccess.Read, FileShare.Read);
292-
}
293-
294-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
295-
private static bool IsValidAndClose(IntPtr handle)
296-
{
297-
var isValid = handle != IntPtr.Zero && handle != new IntPtr(-1);
298-
if (isValid)
299-
CloseHandle(handle);
300-
return isValid;
188+
return PGFileSystem.OpenRead(fileFoundInfo.FilePath.ToString());
301189
}
302-
303-
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
304-
private static extern IntPtr CreateFile(
305-
in char lpFileName,
306-
[MarshalAs(UnmanagedType.U4)] FileAccess access,
307-
[MarshalAs(UnmanagedType.U4)] FileShare share,
308-
IntPtr securityAttributes,
309-
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
310-
[MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
311-
IntPtr templateFile);
312-
313-
[DllImport("kernel32.dll", SetLastError = true)]
314-
[return: MarshalAs(UnmanagedType.Bool)]
315-
private static extern bool CloseHandle(IntPtr hObject);
316190
}

0 commit comments

Comments
 (0)