Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
AssetsFile.cs
1using ICSharpCode.SharpZipLib.Zip;
2using Microsoft.Extensions.Logging;
3using Newtonsoft.Json;
4
6
16[JsonObject(IsReference = true)]
17public sealed class AssetsFile : IDisposable
18{
19 private static readonly List<AssetsFile> _allAssetsFiles = new();
20
24 public static IReadOnlyList<AssetsFile> AllAssetsFiles => _allAssetsFiles.AsReadOnly();
25
29 public static void ClearAll()
30 {
31 foreach (var assetFile in _allAssetsFiles.ToList())
32 assetFile.Dispose();
33 }
34
35 private ZipFile? _zipFile;
36
37 // In-memory source of truth.
38 // Each entry stores its complete bytes independently of any live ZipFile handle.
39 private readonly Dictionary<AssetsFileEntry, byte[]> _zipEntries = new();
40
41 private bool _isLoaded;
42
50 public static void Validate(string path, string? password = null, bool testData = true)
51 {
52 using var archive = new ZipFile(File.OpenRead(path));
53 archive.Password = password;
54 var keys = new HashSet<AssetsFileEntry>();
55 foreach (ZipEntry entry in archive)
56 {
57 if (!entry.IsFile) continue;
58 var key = AssetsFileEntry.FromString(entry.Name);
59 if (key is null || !Enum.IsDefined(key.AssetType) || string.IsNullOrWhiteSpace(key.AssetName))
60 throw new InvalidDataException($"Invalid bundle entry key: {entry.Name}");
61 if (!keys.Add(key)) throw new InvalidDataException($"Duplicate bundle entry key: {entry.Name}");
62 }
63 if (!archive.TestArchive(testData)) throw new InvalidDataException("Bundle integrity check failed. Check the password and archive contents.");
64 }
65
66 [JsonConstructor]
67 private AssetsFile() : this(register: true)
68 {
69 }
70
71 private AssetsFile(bool register)
72 {
73 if (register) _allAssetsFiles.Add(this);
74 }
75
84 public static AssetsFile LoadOrCreate(string path, string? password = null, bool encrypt = false)
85 => LoadOrCreate(path, password, encrypt, register: true);
86
96 public static AssetsFile LoadOrCreate(string path, string? password, bool encrypt, bool register)
97 {
98 if (string.IsNullOrWhiteSpace(path))
99 throw new ArgumentException("path cannot be null or empty.", nameof(path));
100
101 var assetFile = new AssetsFile(register)
102 {
103 FilePath = path,
104 Password = password,
105 UseEncryption = encrypt
106 };
107
108 try
109 {
110 if (File.Exists(path))
111 assetFile.LoadZip();
112 else
113 assetFile._isLoaded = true;
114 return assetFile;
115 }
116 catch
117 {
118 assetFile.Dispose();
119 throw;
120 }
121 }
122
135 public static AssetsFile Load(Stream stream, string? password = null, bool register = true)
136 => Load(stream, password, register, sourcePath: null);
137
144 public static AssetsFile Load(Stream stream, string? password, bool register, string? sourcePath)
145 {
146 ArgumentNullException.ThrowIfNull(stream);
147 if (!stream.CanRead)
148 throw new ArgumentException("Stream must be readable.", nameof(stream));
149
150 var assetFile = new AssetsFile(register)
151 {
152 Password = password,
153 SourcePath = string.IsNullOrWhiteSpace(sourcePath) ? null : sourcePath
154 };
155
156 try
157 {
158 assetFile.LoadZip(stream);
159 return assetFile;
160 }
161 catch
162 {
163 assetFile.Dispose();
164 throw;
165 }
166 }
167
171 [JsonProperty]
172 public string FilePath { get; private set; } = string.Empty;
173
177 [JsonProperty]
178 public string? Password { get; private set; }
179
183 [JsonProperty]
184 public bool UseEncryption { get; private set; }
185
188 [JsonIgnore]
189 public string? SourcePath { get => string.IsNullOrWhiteSpace(FilePath) ? _sourcePath : FilePath; private set => _sourcePath = value; }
190 private string? _sourcePath;
191
192 private void EnsureLoaded()
193 {
194 if (!_isLoaded)
195 LoadZip();
196 }
197
198 private void LoadZip()
199 {
200 if (_isLoaded)
201 return;
202
203 if (!File.Exists(FilePath))
204 {
205 _isLoaded = true;
206 return;
207 }
208
209 using var stream = File.OpenRead(FilePath);
210 LoadZip(stream);
211 }
212
213 private void LoadZip(Stream stream)
214 {
215 if (_isLoaded)
216 return;
217
218 try
219 {
220 Engine.Logger.LogInformation("Loading assets file.");
221
222 _zipFile?.Close();
223 _zipFile = null;
224 _zipEntries.Clear();
225
226 // SharpZipLib's ZipFile expects seekable input. Buffering here also gives
227 // stream-loaded GAFs the same lifetime semantics as path-loaded GAFs:
228 // the source stream is needed only for the duration of this call.
229 MemoryStream? bufferedStream = stream.CanSeek ? null : new MemoryStream();
230 Stream archiveStream = stream;
231 if (bufferedStream is not null)
232 {
233 stream.CopyTo(bufferedStream);
234 bufferedStream.Position = 0;
235 archiveStream = bufferedStream;
236 }
237
238 _zipFile = new ZipFile(archiveStream) { IsStreamOwner = false };
239
240 if (!string.IsNullOrEmpty(Password))
241 _zipFile.Password = Password;
242
243 // Force decryption on the first entry so a bad password fails early.
244 var testEntry = _zipFile.Cast<ZipEntry>().FirstOrDefault(e => e.IsFile);
245 if (testEntry != null)
246 {
247 using var testStream = _zipFile.GetInputStream(testEntry);
248 testStream.ReadByte();
249 }
250
251 foreach (ZipEntry entry in _zipFile)
252 {
253 if (!entry.IsFile)
254 continue;
255
256 var key = AssetsFileEntry.FromString(entry.Name);
257 if (key == null)
258 {
259 Engine.Logger.LogWarning("Invalid entry in assets file: {EntryName}", entry.Name);
260 continue;
261 }
262
263 using var entryStream = _zipFile.GetInputStream(entry);
264 using var ms = new MemoryStream();
265 entryStream.CopyTo(ms);
266
267 _zipEntries[key] = ms.ToArray();
268 }
269
270 _isLoaded = true;
271 }
272 catch (ZipException zex)
273 {
274 Engine.Logger.LogError(zex, "Decryption failed — check password or file integrity.");
275 throw;
276 }
277 catch (Exception ex)
278 {
279 Engine.Logger.LogError(ex, "Failed to load asset file.");
280 throw;
281 }
282 finally
283 {
284 // Once contents are buffered, no need to keep the archive or source stream open.
285 _zipFile?.Close();
286 _zipFile = null;
287 }
288 }
289
300 public void Add(AssetTypes type, string name, Func<Stream> streamFactory)
301 {
302 if (streamFactory == null)
303 throw new ArgumentNullException(nameof(streamFactory));
304
305 EnsureLoaded();
306
307 using var stream = streamFactory();
308 Add(type, name, stream);
309 }
310
320 public void Add(AssetTypes type, string filePath, string? name = null)
321 {
322 if (string.IsNullOrWhiteSpace(filePath))
323 throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
324
325 var nameToUse = name ?? Path.GetFileName(filePath);
326 using var stream = File.OpenRead(filePath);
327 Add(type, nameToUse, stream);
328 }
329
342 public void Add(AssetTypes type, string name, Stream stream)
343 {
344 if (stream == null)
345 throw new ArgumentNullException(nameof(stream));
346
347 if (string.IsNullOrWhiteSpace(name))
348 throw new ArgumentException("Asset name cannot be null or empty.", nameof(name));
349
350 if (!stream.CanRead)
351 throw new ArgumentException("Stream must be readable.", nameof(stream));
352
353 EnsureLoaded();
354
355 var key = new AssetsFileEntry
356 {
357 AssetType = type,
358 AssetName = name
359 };
360
361 using var ms = new MemoryStream();
362 stream.CopyTo(ms);
363 _zipEntries[key] = ms.ToArray();
364 }
365
371 public void Remove(AssetTypes type, string name)
372 {
373 if (string.IsNullOrWhiteSpace(name))
374 throw new ArgumentException("Asset name cannot be null or empty.", nameof(name));
375
376 EnsureLoaded();
377
378 var key = new AssetsFileEntry
379 {
380 AssetType = type,
381 AssetName = name
382 };
383
384 _zipEntries.Remove(key);
385 }
386
393 public Stream? this[AssetTypes type, string name] => Get(type, name);
394
406 public Stream? Get(AssetTypes type, string name)
407 {
408 if (string.IsNullOrWhiteSpace(name))
409 return null;
410
411 EnsureLoaded();
412
413 var exactKey = new AssetsFileEntry
414 {
415 AssetType = type,
416 AssetName = name
417 };
418
419 if (_zipEntries.TryGetValue(exactKey, out var exactData))
420 return new MemoryStream(exactData, writable: false);
421
422 var requestedBaseName = Path.GetFileNameWithoutExtension(name);
423
424 var match = _zipEntries.Keys.FirstOrDefault(k =>
425 k.AssetType == type &&
426 string.Equals(
427 Path.GetFileNameWithoutExtension(k.AssetName),
428 requestedBaseName,
429 StringComparison.OrdinalIgnoreCase));
430
431 return match != null && _zipEntries.TryGetValue(match, out var fallbackData)
432 ? new MemoryStream(fallbackData, writable: false)
433 : null;
434 }
435
440 public IEnumerable<AssetsFileEntry> GetAllEntries()
441 {
442 EnsureLoaded();
443 return _zipEntries.Keys.ToList();
444 }
445
449 public void Save()
450 {
451 EnsureLoaded();
452
453 _zipFile?.Close();
454 _zipFile = null;
455 _isLoaded = false;
456
457 using (var fs = File.Create(FilePath))
458 using (var zipStream = new ZipOutputStream(fs) { IsStreamOwner = true })
459 {
460 if (!string.IsNullOrEmpty(Password))
461 zipStream.Password = Password;
462
463 foreach (var (key, data) in _zipEntries)
464 {
465 var entry = new ZipEntry(key.ToString())
466 {
467 DateTime = DateTime.Now
468 };
469
470 if (UseEncryption && !string.IsNullOrEmpty(Password))
471 entry.AESKeySize = 256;
472
473 zipStream.PutNextEntry(entry);
474
475 using var inputStream = new MemoryStream(data, writable: false);
476 inputStream.CopyTo(zipStream);
477 zipStream.CloseEntry();
478 }
479
480 zipStream.Finish();
481 }
482
483 Engine.Logger.LogInformation(
484 "Assets file saved (Encrypted: {Encrypted}).",
486
487 // Keep the in-memory copy as the source of truth.
488 _isLoaded = true;
489 }
490
494 public void Dispose()
495 {
496 _zipFile?.Close();
497 _zipFile = null;
498 _isLoaded = false;
499 _zipEntries.Clear();
500 _allAssetsFiles.Remove(this);
501 }
502}
Represents an entry for an engine asset file, including its type and name.
Represents an asset file used by the engine, providing functionality to load, manage,...
Definition AssetsFile.cs:18
static void ClearAll()
Releases all assets held by the application and clears the internal collection of asset files.
Definition AssetsFile.cs:29
void Save()
Saves the current set of entries to a zip file at the specified file path.
string FilePath
Gets the file path associated with the current instance.
static void Validate(string path, string? password=null, bool testData=true)
Strictly validates an existing bundle's entry keys and archive integrity. Unlike runtime loading,...
Definition AssetsFile.cs:50
static AssetsFile Load(Stream stream, string? password, bool register, string? sourcePath)
Loads a buffered archive with an optional reconstructible source path.
string? SourcePath
Gets the archive's reconstructible origin, including an optional stream origin.
void Add(AssetTypes type, string name, Stream stream)
Adds an asset file entry to the collection with the specified type, name, and source stream.
string? Password
Gets the password associated with the current instance.
void Dispose()
Releases all resources used by the AssetsFile instance.
static IReadOnlyList< AssetsFile > AllAssetsFiles
Gets a read-only list of registered AssetsFile instances.
Definition AssetsFile.cs:24
static AssetsFile LoadOrCreate(string path, string? password=null, bool encrypt=false)
Loads an existing asset file from the specified path or creates a new one if the file does not exist.
void Add(AssetTypes type, string filePath, string? name=null)
Adds an asset file to the engine with the specified type and file path.
bool UseEncryption
Gets a value indicating whether encryption is enabled for the current operation.
static AssetsFile Load(Stream stream, string? password=null, bool register=true)
Loads an existing asset file from a readable stream.
static AssetsFile LoadOrCreate(string path, string? password, bool encrypt, bool register)
Loads or creates an asset file, optionally without adding it to the runtime collection....
Definition AssetsFile.cs:96
void Remove(AssetTypes type, string name)
Removes the specified asset file entry from the collection.
Stream? Get(AssetTypes type, string name)
Retrieves a stream for the specified asset type and name.
IEnumerable< AssetsFileEntry > GetAllEntries()
Retrieves all entries from the asset file.
void Add(AssetTypes type, string name, Func< Stream > streamFactory)
Adds an asset file entry to the collection with the specified type, name, and stream factory.
Represents the core game engine singleton responsible for managing the main update loop,...
Definition Engine.cs:36
static ILogger< Engine > Logger
Gets the logger instance used by the engine for diagnostic and informational messages.
Definition Engine.cs:59
AssetTypes
Defines the types of assets that can be stored and managed by the engine.
Definition AssetTypes.cs:8