1using ICSharpCode.SharpZipLib.Zip;
2using Microsoft.Extensions.Logging;
16[JsonObject(IsReference =
true)]
17public sealed class AssetsFile : IDisposable
19 private static readonly List<AssetsFile> _allAssetsFiles =
new();
24 public static IReadOnlyList<AssetsFile>
AllAssetsFiles => _allAssetsFiles.AsReadOnly();
31 foreach (var assetFile
in _allAssetsFiles.ToList())
35 private ZipFile? _zipFile;
39 private readonly Dictionary<AssetsFileEntry, byte[]> _zipEntries =
new();
41 private bool _isLoaded;
50 public static void Validate(
string path,
string? password =
null,
bool testData =
true)
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)
57 if (!entry.IsFile)
continue;
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}");
63 if (!archive.TestArchive(testData))
throw new InvalidDataException(
"Bundle integrity check failed. Check the password and archive contents.");
71 private AssetsFile(
bool register)
73 if (
register) _allAssetsFiles.Add(
this);
84 public static AssetsFile
LoadOrCreate(
string path,
string? password =
null,
bool encrypt =
false)
85 =>
LoadOrCreate(path, password, encrypt,
register:
true);
96 public static AssetsFile
LoadOrCreate(
string path,
string? password,
bool encrypt,
bool register)
98 if (
string.IsNullOrWhiteSpace(path))
99 throw new ArgumentException(
"path cannot be null or empty.", nameof(path));
101 var assetFile =
new AssetsFile(
register)
110 if (File.Exists(path))
113 assetFile._isLoaded =
true;
135 public static AssetsFile
Load(Stream stream,
string? password =
null,
bool register =
true)
136 =>
Load(stream, password,
register, sourcePath:
null);
144 public static AssetsFile
Load(Stream stream,
string? password,
bool register,
string? sourcePath)
146 ArgumentNullException.ThrowIfNull(stream);
148 throw new ArgumentException(
"Stream must be readable.", nameof(stream));
150 var assetFile =
new AssetsFile(
register)
153 SourcePath =
string.IsNullOrWhiteSpace(sourcePath) ? null : sourcePath
158 assetFile.LoadZip(stream);
172 public string FilePath {
get;
private set; } =
string.Empty;
190 private string? _sourcePath;
192 private void EnsureLoaded()
198 private void LoadZip()
209 using var stream = File.OpenRead(
FilePath);
213 private void LoadZip(Stream stream)
220 Engine.Logger.LogInformation(
"Loading assets file.");
229 MemoryStream? bufferedStream = stream.CanSeek ? null :
new MemoryStream();
230 Stream archiveStream = stream;
231 if (bufferedStream is not
null)
233 stream.CopyTo(bufferedStream);
234 bufferedStream.Position = 0;
235 archiveStream = bufferedStream;
238 _zipFile =
new ZipFile(archiveStream) { IsStreamOwner =
false };
240 if (!
string.IsNullOrEmpty(
Password))
244 var testEntry = _zipFile.Cast<ZipEntry>().FirstOrDefault(e => e.IsFile);
245 if (testEntry !=
null)
247 using var testStream = _zipFile.GetInputStream(testEntry);
248 testStream.ReadByte();
251 foreach (ZipEntry entry
in _zipFile)
256 var key = AssetsFileEntry.FromString(entry.Name);
259 Engine.Logger.LogWarning(
"Invalid entry in assets file: {EntryName}", entry.Name);
263 using var entryStream = _zipFile.GetInputStream(entry);
264 using var ms =
new MemoryStream();
265 entryStream.CopyTo(ms);
267 _zipEntries[key] = ms.ToArray();
272 catch (ZipException zex)
274 Engine.Logger.LogError(zex,
"Decryption failed — check password or file integrity.");
279 Engine.Logger.LogError(ex,
"Failed to load asset file.");
302 if (streamFactory ==
null)
303 throw new ArgumentNullException(nameof(streamFactory));
307 using var stream = streamFactory();
308 Add(type, name, stream);
322 if (
string.IsNullOrWhiteSpace(filePath))
323 throw new ArgumentException(
"File path cannot be null or empty.", nameof(filePath));
325 var nameToUse = name ?? Path.GetFileName(filePath);
326 using var stream = File.OpenRead(filePath);
327 Add(type, nameToUse, stream);
345 throw new ArgumentNullException(nameof(stream));
347 if (
string.IsNullOrWhiteSpace(name))
348 throw new ArgumentException(
"Asset name cannot be null or empty.", nameof(name));
351 throw new ArgumentException(
"Stream must be readable.", nameof(stream));
361 using var ms =
new MemoryStream();
363 _zipEntries[key] = ms.ToArray();
373 if (
string.IsNullOrWhiteSpace(name))
374 throw new ArgumentException(
"Asset name cannot be null or empty.", nameof(name));
384 _zipEntries.Remove(key);
408 if (
string.IsNullOrWhiteSpace(name))
419 if (_zipEntries.TryGetValue(exactKey, out var exactData))
420 return new MemoryStream(exactData, writable:
false);
422 var requestedBaseName = Path.GetFileNameWithoutExtension(name);
424 var match = _zipEntries.Keys.FirstOrDefault(k =>
425 k.AssetType == type &&
427 Path.GetFileNameWithoutExtension(k.AssetName),
429 StringComparison.OrdinalIgnoreCase));
431 return match !=
null && _zipEntries.TryGetValue(match, out var fallbackData)
432 ?
new MemoryStream(fallbackData, writable:
false)
443 return _zipEntries.Keys.ToList();
457 using (var fs = File.Create(
FilePath))
458 using (var zipStream =
new ZipOutputStream(fs) { IsStreamOwner =
true })
460 if (!
string.IsNullOrEmpty(
Password))
463 foreach (var (key, data) in _zipEntries)
465 var entry =
new ZipEntry(key.ToString())
467 DateTime = DateTime.Now
471 entry.AESKeySize = 256;
473 zipStream.PutNextEntry(entry);
475 using var inputStream =
new MemoryStream(data, writable:
false);
476 inputStream.CopyTo(zipStream);
477 zipStream.CloseEntry();
484 "Assets file saved (Encrypted: {Encrypted}).",
500 _allAssetsFiles.Remove(
this);
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,...
static void ClearAll()
Releases all assets held by the application and clears the internal collection of asset files.
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,...
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.
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....
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,...
static ILogger< Engine > Logger
Gets the logger instance used by the engine for diagnostic and informational messages.
AssetTypes
Defines the types of assets that can be stored and managed by the engine.