diff --git a/BanjoBotAssets.Console/BanjoBotAssets.Console.csproj b/BanjoBotAssets.Console/BanjoBotAssets.Console.csproj
new file mode 100644
index 0000000..1b040cb
--- /dev/null
+++ b/BanjoBotAssets.Console/BanjoBotAssets.Console.csproj
@@ -0,0 +1,38 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/BanjoBotAssets.Console/Program.cs b/BanjoBotAssets.Console/Program.cs
new file mode 100644
index 0000000..8f5258b
--- /dev/null
+++ b/BanjoBotAssets.Console/Program.cs
@@ -0,0 +1,66 @@
+/* Copyright 2026 Tara "Dino" Cassatt
+ *
+ * This file is part of BanjoBotAssets.
+ *
+ * BanjoBotAssets is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * BanjoBotAssets is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with BanjoBotAssets. If not, see .
+ */
+
+
+using BanjoBotAssets;
+using BanjoBotAssets.Exporters;
+using BanjoBotAssets.Reporters;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using System.Reflection;
+
+
+await Host.CreateDefaultBuilder(args)
+#if DEBUG
+ .UseEnvironment("Development")
+#endif
+ .UseContentRoot(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? throw new InvalidOperationException("Null content root"))
+ .ConfigureLogging(logging =>
+ {
+ logging
+ .ClearProviders()
+ .AddSimpleConsole(console => console.SingleLine = true);
+ })
+ .ConfigureServices(services =>
+ {
+ services
+ .AddBanjoServices();
+
+ //services.AddSingleton();
+ //services.AddSingleton();
+ })
+ .RunConsoleAsync(o => o.SuppressStatusMessages = true);
+
+return Environment.ExitCode;
+
+class ExampleStageReporter : IExportStageReporter
+{
+ public void Report(ExportStage stage)
+ {
+ Console.WriteLine($"Reporting Stage: {stage}");
+ }
+}
+
+class ExampleProgressReporter : IExportProgressReporter
+{
+ public void Report(ExportProgress progress)
+ {
+ if (progress.ExportType is not null)
+ Console.WriteLine($"Reporting Progress for {progress.ExportType}: {progress.CompletedSteps}/{progress.TotalSteps}");
+ }
+}
\ No newline at end of file
diff --git a/BanjoBotAssets/appsettings.json b/BanjoBotAssets.Console/appsettings.json
similarity index 86%
rename from BanjoBotAssets/appsettings.json
rename to BanjoBotAssets.Console/appsettings.json
index ad1916d..6247b5b 100644
--- a/BanjoBotAssets/appsettings.json
+++ b/BanjoBotAssets.Console/appsettings.json
@@ -9,11 +9,11 @@
"ELanguage": ""
},
"AesOptions": {
- "AesApiUri": "https://fortnitecentral.genxgames.gg/api/v1/aes",
+ "AesApiUri": "https://api.fortniteapi.com/v1/aes",
"LocalFilePath": "aes.json"
},
"MappingsOptions": {
- "MappingsApiUri": "https://fortnitecentral.genxgames.gg/api/v1/mappings",
+ "MappingsApiUri": "https://api.fortniteapi.com/v1/mappings",
"LocalFilePath": "mappings.usmap"
},
"ExportedAssets": {
diff --git a/BanjoBotAssets.sln b/BanjoBotAssets.sln
index c101549..bcb5e2a 100644
--- a/BanjoBotAssets.sln
+++ b/BanjoBotAssets.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31919.166
+# Visual Studio Version 18
+VisualStudioVersion = 18.5.11801.241 oobstable
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BanjoBotAssets", "BanjoBotAssets\BanjoBotAssets.csproj", "{BE443DAB-C3C2-4505-8DE0-0FBAD609481A}"
EndProject
@@ -23,6 +23,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BanjoBotAssets.SourceGenera
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BanjoBotAssets.SourceGenerators.Tests", "BanjoBotAssets.SourceGenerators.Tests\BanjoBotAssets.SourceGenerators.Tests.csproj", "{A786A866-CE3F-49D4-A54A-CC6488B732E3}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BanjoBotAssets.Console", "BanjoBotAssets.Console\BanjoBotAssets.Console.csproj", "{7E3756BA-9BBF-461D-8ED9-3A0D43251039}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -57,6 +59,10 @@ Global
{A786A866-CE3F-49D4-A54A-CC6488B732E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A786A866-CE3F-49D4-A54A-CC6488B732E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A786A866-CE3F-49D4-A54A-CC6488B732E3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7E3756BA-9BBF-461D-8ED9-3A0D43251039}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7E3756BA-9BBF-461D-8ED9-3A0D43251039}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7E3756BA-9BBF-461D-8ED9-3A0D43251039}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7E3756BA-9BBF-461D-8ED9-3A0D43251039}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/BanjoBotAssets/AssetExportService.cs b/BanjoBotAssets/AssetExportService.cs
index f9f0735..f3d4331 100644
--- a/BanjoBotAssets/AssetExportService.cs
+++ b/BanjoBotAssets/AssetExportService.cs
@@ -20,6 +20,7 @@
using BanjoBotAssets.Config;
using BanjoBotAssets.Exporters;
using BanjoBotAssets.PostExporters;
+using BanjoBotAssets.Reporters;
using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
using CUE4Parse.UE4.Objects.Core.Misc;
@@ -29,6 +30,19 @@
namespace BanjoBotAssets
{
+ public enum ExportStage
+ {
+ DecryptingGameFiles,
+ InitialisingOodle,
+ LoadingVirtualPaths,
+ LoadingMappings,
+ LoadingLocalisation,
+ PreparingInterestingAssets,
+ RunningExporters,
+ RunningPostExporters,
+ GeneratingArtifacts,
+ }
+
internal sealed partial class AssetExportService(
ILogger logger,
IHostApplicationLifetime lifetime,
@@ -40,7 +54,9 @@ internal sealed partial class AssetExportService(
ITypeMappingsProviderFactory typeMappingsProviderFactory,
IOptions scopeOptions,
IEnumerable allPostExporters,
- LanguageProvider languageProvider) : BackgroundService
+ LanguageProvider languageProvider,
+ IExportProgressReporter? progressReporter = null,
+ IExportStageReporter? stageReporter = null) : BackgroundService
{
private readonly List exportersToRun = MakeExportersToRun(allExporters, scopeOptions);
private readonly ConcurrentDictionary failedAssets = new();
@@ -92,36 +108,46 @@ private async Task RunAsync(CancellationToken cancellationToken)
// by the time this method is called, the CUE4Parse file provider has already been created,
// and the game files have been located but not decrypted. we need to supply the AES keys,
// from cache or from an external API.
+ stageReporter?.Report(ExportStage.DecryptingGameFiles);
await DecryptGameFilesAsync(cancellationToken);
// download the oodle library if needed, and initialize it
+ stageReporter?.Report(ExportStage.InitialisingOodle);
await InitializeOodleAsync();
// load virtual paths
+ stageReporter?.Report(ExportStage.LoadingVirtualPaths);
LoadVirtualPaths();
// load the type mappings CUE4Parse uses to parse UE structures
+ stageReporter?.Report(ExportStage.LoadingMappings);
await LoadMappingsAsync(cancellationToken);
// load localized resources
+ stageReporter?.Report(ExportStage.LoadingLocalisation);
LoadLocalization(cancellationToken);
// register the export classes used to expose UE structures as strongly-typed C# objects
RegisterExportTypes();
// feed the file list to each exporter so they can record the paths they're interested in
+ // todo: make this async? would otherwise cause hanging in a GUI app
+ stageReporter?.Report(ExportStage.PreparingInterestingAssets);
OfferFileListToExporters();
// run exporters and collect their intermediate results
+ stageReporter?.Report(ExportStage.RunningExporters);
var (exportedAssets, exportedRecipes, stats1) = await RunSelectedExportersAsync(cancellationToken);
// run post-exporters to refine the intermediate results
+ stageReporter?.Report(ExportStage.RunningPostExporters);
var stats2 = await RunSelectedPostExportersAsync(exportedAssets, exportedRecipes, cancellationToken);
// report assets loaded and time elapsed
ReportAssetLoadingStats(stats1 + stats2);
// generate output artifacts
+ stageReporter?.Report(ExportStage.GeneratingArtifacts);
await GenerateSelectedArtifactsAsync(exportedAssets, exportedRecipes, cancellationToken);
// report cache stats
@@ -181,11 +207,11 @@ private async Task InitializeOodleAsync()
{
logger.LogInformation(Resources.Status_LocatingOodle);
- await OodleHelper.DownloadOodleDllAsync(OodleHelper.OODLE_DLL_NAME);
+ await OodleHelper.DownloadOodleDllAsync();
logger.LogInformation(Resources.Status_InitializingOodle);
- OodleHelper.Initialize(OodleHelper.OODLE_DLL_NAME);
+ OodleHelper.Initialize();
}
private void LoadVirtualPaths()
@@ -198,7 +224,7 @@ private async Task LoadMappingsAsync(CancellationToken cancellationToken)
{
logger.LogInformation(Resources.Status_LoadingMappings);
- if (provider.InternalGameName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase))
+ if (provider.ProjectName.Equals("FortniteGame", StringComparison.OrdinalIgnoreCase))
{
provider.MappingsContainer = typeMappingsProviderFactory.Create();
}
@@ -312,7 +338,7 @@ private void HandleProgressReport(ExportProgress progress)
}
}
- // TODO: do something more with progress reports
+ progressReporter?.Report(progress);
}
private void ReportFailedAssets()
@@ -351,8 +377,12 @@ private void OfferFileListToExporters()
private void LoadLocalization(CancellationToken cancellationToken)
{
+ //PostMount is intended to be used to validate encryption keys, but it now also prepares localization dictionary (for some reason)
+ //might be better off at the end of DecryptGameFilesAsync
+ provider.PostMount();
+
logger.LogInformation(Resources.Status_LoadingLocalization, languageProvider.Language.ToString());
- provider.LoadLocalization(languageProvider.Language, cancellationToken);
+ provider.ChangeCulture(provider.GetLanguageCode(languageProvider.Language));
}
}
}
diff --git a/BanjoBotAssets/BanjoBotAssets.csproj b/BanjoBotAssets/BanjoBotAssets.csproj
index 6750ce7..aea723c 100644
--- a/BanjoBotAssets/BanjoBotAssets.csproj
+++ b/BanjoBotAssets/BanjoBotAssets.csproj
@@ -1,7 +1,7 @@
- Exe
+ Library
net8.0
enable
enable
@@ -54,7 +54,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/BanjoBotAssets/BanjoEntryPoint.cs b/BanjoBotAssets/BanjoEntryPoint.cs
new file mode 100644
index 0000000..db922fb
--- /dev/null
+++ b/BanjoBotAssets/BanjoEntryPoint.cs
@@ -0,0 +1,44 @@
+/* Copyright 2026 Tara "Dino" Cassatt
+ *
+ * This file is part of BanjoBotAssets.
+ *
+ * BanjoBotAssets is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * BanjoBotAssets is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with BanjoBotAssets. If not, see .
+ */
+using BanjoBotAssets.Config;
+
+namespace BanjoBotAssets
+{
+ public static class BanjoEntryPoint
+ {
+ public static IServiceCollection AddBanjoServices(this IServiceCollection services)
+ {
+ // TODO: export per-difficulty stat clamp tables (GameDifficultyGrowthBounds, CombatStatClampsPerTheater)
+ // TODO: export collection book categories and recruitment/research/voucher options (CollectionBookSlots)
+
+ services
+ .AddHostedService()
+ .AddHttpClient()
+ .AddAesProviders()
+ .AddMappingsProviders()
+ .AddGameFileProvider()
+ .AddAssetExporters();
+
+ services
+ .AddOptions()
+ .Configure((scope, config) => config.Bind(scope));
+
+ return services;
+ }
+ }
+}
diff --git a/BanjoBotAssets/CachingFileProvider.cs b/BanjoBotAssets/CachingFileProvider.cs
index 52f8672..381c145 100644
--- a/BanjoBotAssets/CachingFileProvider.cs
+++ b/BanjoBotAssets/CachingFileProvider.cs
@@ -98,13 +98,13 @@ private void WriteToAssetLog(string line)
}
}
- public override Task LoadPackageAsync(GameFile file)
+ public override IPackage LoadPackage(GameFile file)
{
Interlocked.Increment(ref cacheRequests);
- return cache.Cache.GetOrAddAsync(
+ return cache.Cache.GetOrAdd(
file.Path,
- async _ =>
+ _ =>
{
Interlocked.Increment(ref cacheMisses);
@@ -112,12 +112,12 @@ public override Task LoadPackageAsync(GameFile file)
logger.LogDebug(Resources.Status_CacheMiss, file.Path, file.Size);
WriteToAssetLog(file.Path);
- return await base.LoadPackageAsync(file);
+ return base.LoadPackage(file);
},
new MemoryCacheEntryOptions { Size = file.Size });
}
- public override Task TryLoadPackageAsync(GameFile file)
+ public override Task LoadPackageAsync(GameFile file)
{
Interlocked.Increment(ref cacheRequests);
@@ -131,7 +131,7 @@ public override Task LoadPackageAsync(GameFile file)
logger.LogDebug(Resources.Status_CacheMiss, file.Path, file.Size);
WriteToAssetLog(file.Path);
- return await base.TryLoadPackageAsync(file);
+ return await base.LoadPackageAsync(file);
},
new MemoryCacheEntryOptions { Size = file.Size });
}
diff --git a/BanjoBotAssets/Config/ServiceCollectionExtensions.AppConfig.cs b/BanjoBotAssets/Config/ServiceCollectionExtensions.AppConfig.cs
index c0d49a4..f3ea3cb 100644
--- a/BanjoBotAssets/Config/ServiceCollectionExtensions.AppConfig.cs
+++ b/BanjoBotAssets/Config/ServiceCollectionExtensions.AppConfig.cs
@@ -45,7 +45,7 @@ public static IServiceCollection AddAesProviders(this IServiceCollection service
.AddOptions()
.Configure((options, config) =>
{
- options.AesApiUri = "https://fortnitecentral.genxgames.gg/api/v1/aes";
+ options.AesApiUri = "https://api.fortniteapi.com/v1/aes";
options.LocalFilePath = "aes.json";
config.GetSection(nameof(AesOptions)).Bind(options);
});
@@ -152,7 +152,7 @@ public static IServiceCollection AddGameFileProvider(this IServiceCollection ser
directory: gameDirectory,
searchOption: SearchOption.TopDirectoryOnly,
isCaseInsensitive: true,
- versions: new VersionContainer(EGame.GAME_UE5_4),
+ versions: new VersionContainer(EGame.GAME_UE5_6),
assetLogPath: perfOptions.Value.AssetLogPath);
provider.Initialize();
@@ -192,7 +192,7 @@ public static IServiceCollection AddMappingsProviders(this IServiceCollection se
services.AddOptions()
.Configure((options, config) =>
{
- options.MappingsApiUri = "https://fortnitecentral.genxgames.gg/api/v1/mappings";
+ options.MappingsApiUri = "https://api.fortniteapi.com/v1/mappings";
options.LocalFilePath = "mappings.usmap";
config.GetSection(nameof(MappingsOptions)).Bind(options);
});
diff --git a/BanjoBotAssets/Exporters/BaseExporter.cs b/BanjoBotAssets/Exporters/BaseExporter.cs
index c109ebd..7967949 100644
--- a/BanjoBotAssets/Exporters/BaseExporter.cs
+++ b/BanjoBotAssets/Exporters/BaseExporter.cs
@@ -56,7 +56,7 @@ public void CountAssetLoaded()
var file = provider[path];
CountAssetLoaded();
- return await provider.LoadObjectAsync(file.PathWithoutExtension);
+ return await provider.SafeLoadPackageObjectAsync(file.PathWithoutExtension);
}
public abstract Task ExportAssetsAsync(IProgress progress, IAssetOutput output, CancellationToken cancellationToken);
diff --git a/BanjoBotAssets/Exporters/Blueprints/BlueprintExporter.cs b/BanjoBotAssets/Exporters/Blueprints/BlueprintExporter.cs
index 71b4cb4..bb375e3 100644
--- a/BanjoBotAssets/Exporters/Blueprints/BlueprintExporter.cs
+++ b/BanjoBotAssets/Exporters/Blueprints/BlueprintExporter.cs
@@ -38,6 +38,7 @@ private void Report(IProgress progress, string current)
{
progress.Report(new ExportProgress
{
+ ExportType = $"Blueprint.{Type}",
TotalSteps = numToProcess,
CompletedSteps = processedSoFar,
AssetsLoaded = assetsLoaded,
diff --git a/BanjoBotAssets/Exporters/DifficultyExporter.cs b/BanjoBotAssets/Exporters/DifficultyExporter.cs
index ae1f550..2481154 100644
--- a/BanjoBotAssets/Exporters/DifficultyExporter.cs
+++ b/BanjoBotAssets/Exporters/DifficultyExporter.cs
@@ -32,7 +32,7 @@ public override async Task ExportAssetsAsync(IProgress progress,
var file = provider[growthBoundsPath];
Interlocked.Increment(ref assetsLoaded);
- var dataTable = await provider.LoadObjectAsync(file.PathWithoutExtension);
+ var dataTable = await provider.SafeLoadPackageObjectAsync(file.PathWithoutExtension);
if (dataTable == null)
{
diff --git a/BanjoBotAssets/Exporters/ExpeditionCriteriaExporter.cs b/BanjoBotAssets/Exporters/ExpeditionCriteriaExporter.cs
index b9059ca..306796d 100644
--- a/BanjoBotAssets/Exporters/ExpeditionCriteriaExporter.cs
+++ b/BanjoBotAssets/Exporters/ExpeditionCriteriaExporter.cs
@@ -17,7 +17,7 @@ public override async Task ExportAssetsAsync(IProgress progress,
var file = provider[criteriaPath];
Interlocked.Increment(ref assetsLoaded);
- var dataTable = await provider.LoadObjectAsync(file.PathWithoutExtension);
+ var dataTable = await provider.SafeLoadPackageObjectAsync(file.PathWithoutExtension);
if (dataTable == null)
{
diff --git a/BanjoBotAssets/Exporters/ExportProgress.cs b/BanjoBotAssets/Exporters/ExportProgress.cs
index 79927b3..7e036a8 100644
--- a/BanjoBotAssets/Exporters/ExportProgress.cs
+++ b/BanjoBotAssets/Exporters/ExportProgress.cs
@@ -17,8 +17,9 @@
*/
namespace BanjoBotAssets.Exporters
{
- internal sealed record ExportProgress
+ public sealed record ExportProgress
{
+ public string? ExportType { get; init; }
public int TotalSteps { get; init; }
public int CompletedSteps { get; init; }
public required string CurrentItem { get; init; }
diff --git a/BanjoBotAssets/Exporters/Groups/GroupExporter.cs b/BanjoBotAssets/Exporters/Groups/GroupExporter.cs
index 6b60060..390287d 100644
--- a/BanjoBotAssets/Exporters/Groups/GroupExporter.cs
+++ b/BanjoBotAssets/Exporters/Groups/GroupExporter.cs
@@ -128,7 +128,7 @@ await Parallel.ForEachAsync(assetsToProcess, opts, async (grouping, _) =>
Report(progress, file.PathWithoutExtension);
- var asset = await provider.LoadObjectAsync(file.PathWithoutExtension);
+ var asset = await provider.SafeLoadPackageObjectAsync(file.PathWithoutExtension);
if (asset == null)
{
diff --git a/BanjoBotAssets/Exporters/Groups/HeroExporter.cs b/BanjoBotAssets/Exporters/Groups/HeroExporter.cs
index 56ec7b8..a8a1281 100644
--- a/BanjoBotAssets/Exporters/Groups/HeroExporter.cs
+++ b/BanjoBotAssets/Exporters/Groups/HeroExporter.cs
@@ -165,8 +165,8 @@ public override async Task ExportAssetsAsync(IProgress progress,
{
if (itemToQuestPath != null && questRewardsPath != null)
{
- var itemToQuestTask = provider.LoadObjectAsync(provider.Files[itemToQuestPath].PathWithoutExtension);
- var questRewardsTask = provider.LoadObjectAsync(provider.Files[questRewardsPath].PathWithoutExtension);
+ var itemToQuestTask = provider.LoadPackageObjectAsync(provider.Files[itemToQuestPath].PathWithoutExtension);
+ var questRewardsTask = provider.LoadPackageObjectAsync(provider.Files[questRewardsPath].PathWithoutExtension);
var itemToQuestTable = await itemToQuestTask;
var questRewardsTable = await questRewardsTask;
diff --git a/BanjoBotAssets/Exporters/Groups/SchematicExporter.cs b/BanjoBotAssets/Exporters/Groups/SchematicExporter.cs
index bd1781c..a8feaae 100644
--- a/BanjoBotAssets/Exporters/Groups/SchematicExporter.cs
+++ b/BanjoBotAssets/Exporters/Groups/SchematicExporter.cs
@@ -195,7 +195,7 @@ public override async Task ExportAssetsAsync(IProgress progress,
}
var widOrTidFile = provider[widOrTidPath];
Interlocked.Increment(ref assetsLoaded);
- return await provider.LoadObjectAsync(widOrTidFile.PathWithoutExtension);
+ return await provider.SafeLoadPackageObjectAsync(widOrTidFile.PathWithoutExtension);
}
protected override async Task ExtractCommonFieldsAsync(UObject asset, IGrouping grouping)
@@ -347,7 +347,7 @@ private static (string category, string subType) CategoryAndSubTypeFromTags(FGam
return await cachedAmmoTypesFromPaths.GetOrAdd(ammoDataPath.AssetPathName.Text, static async (path, provider) =>
{
- var asset = await provider.LoadObjectAsync(path);
+ var asset = await provider.SafeLoadPackageObjectAsync(path);
if (asset.ItemName?.Text is string str)
{
var i = str.IndexOf(':');
diff --git a/BanjoBotAssets/Exporters/ItemRatingExporter.cs b/BanjoBotAssets/Exporters/ItemRatingExporter.cs
index 4ad2feb..db7a16c 100644
--- a/BanjoBotAssets/Exporters/ItemRatingExporter.cs
+++ b/BanjoBotAssets/Exporters/ItemRatingExporter.cs
@@ -47,7 +47,7 @@ private async Task ExportDefaultItemRatingsAsync(IAssetOutput output)
var file = provider[baseItemRatingPath];
Interlocked.Increment(ref assetsLoaded);
- var curveTable = await provider.LoadObjectAsync(file.PathWithoutExtension);
+ var curveTable = await provider.SafeLoadPackageObjectAsync(file.PathWithoutExtension);
if (curveTable == null)
{
@@ -71,7 +71,7 @@ private async Task ExportSurvivorItemRatingsAsync(IAssetOutput output)
var file = provider[survivorItemRatingPath];
Interlocked.Increment(ref assetsLoaded);
- var curveTable = await provider.LoadObjectAsync(file.PathWithoutExtension);
+ var curveTable = await provider.SafeLoadPackageObjectAsync(file.PathWithoutExtension);
if (curveTable == null)
{
diff --git a/BanjoBotAssets/Exporters/QuestMapExporter.cs b/BanjoBotAssets/Exporters/QuestMapExporter.cs
index 0f6150f..d22ba4c 100644
--- a/BanjoBotAssets/Exporters/QuestMapExporter.cs
+++ b/BanjoBotAssets/Exporters/QuestMapExporter.cs
@@ -29,7 +29,7 @@ public override async Task ExportAssetsAsync(IProgress progress,
var file = provider[assetPaths[0]];
Interlocked.Increment(ref assetsLoaded);
- var mapData = await provider.LoadObjectAsync(file.PathWithoutExtension);
+ var mapData = await provider.SafeLoadPackageObjectAsync(file.PathWithoutExtension);
cancellationToken.ThrowIfCancellationRequested();
diff --git a/BanjoBotAssets/Exporters/UObjects/AbilityExporter.cs b/BanjoBotAssets/Exporters/UObjects/AbilityExporter.cs
index 17057af..70afb24 100644
--- a/BanjoBotAssets/Exporters/UObjects/AbilityExporter.cs
+++ b/BanjoBotAssets/Exporters/UObjects/AbilityExporter.cs
@@ -18,6 +18,7 @@
using BanjoBotAssets.UExports;
using CUE4Parse.FN.Structs.GA;
using CUE4Parse.UE4.Objects.Engine;
+using CUE4Parse.Utilities;
using System.Data;
namespace BanjoBotAssets.Exporters.UObjects
diff --git a/BanjoBotAssets/Exporters/UObjects/UObjectExporter.cs b/BanjoBotAssets/Exporters/UObjects/UObjectExporter.cs
index cdcd3b7..af05a62 100644
--- a/BanjoBotAssets/Exporters/UObjects/UObjectExporter.cs
+++ b/BanjoBotAssets/Exporters/UObjects/UObjectExporter.cs
@@ -17,6 +17,7 @@
*/
using CUE4Parse.FN.Enums.FortniteGame;
using System.Collections.Concurrent;
+using CUE4Parse.Utilities;
namespace BanjoBotAssets.Exporters.UObjects
{
@@ -50,6 +51,7 @@ private void Report(IProgress progress, string current)
{
progress.Report(new ExportProgress
{
+ ExportType = $"UObject.{Type}",
TotalSteps = numToProcess,
CompletedSteps = processedSoFar,
AssetsLoaded = assetsLoaded,
@@ -85,15 +87,23 @@ await Parallel.ForEachAsync(assetsToProcess, opts, async (path, _) =>
TAsset? uobject;
if (IgnoreLoadFailures)
{
- var pkg = await provider.TryLoadPackageAsync(file);
+ IPackage pkg;
+ try
+ {
+ pkg = await provider.LoadPackageAsync(file);
+ }
+ catch
+ {
+ return;
+ }
cancellationToken.ThrowIfCancellationRequested();
- if (pkg?.GetExportOrNull(file.NameWithoutExtension, StringComparison.OrdinalIgnoreCase) is TAsset asset)
+ if (pkg.GetExportOrNull(file.NameWithoutExtension, StringComparison.OrdinalIgnoreCase) is TAsset asset)
{
uobject = asset;
}
- else if (pkg?.GetExportOrNull(file.NameWithoutExtension + "_C", StringComparison.OrdinalIgnoreCase) is TAsset assetC)
+ else if (pkg.GetExportOrNull(file.NameWithoutExtension + "_C", StringComparison.OrdinalIgnoreCase) is TAsset assetC)
{
uobject = assetC;
}
diff --git a/BanjoBotAssets/Exporters/VenturesSeasonExporter.cs b/BanjoBotAssets/Exporters/VenturesSeasonExporter.cs
index c639677..5a43da8 100644
--- a/BanjoBotAssets/Exporters/VenturesSeasonExporter.cs
+++ b/BanjoBotAssets/Exporters/VenturesSeasonExporter.cs
@@ -47,17 +47,19 @@ public override async Task ExportAssetsAsync(IProgress progress,
return;
}
- var levelRewardsTask = provider.LoadObjectAsync(provider[levelRewardsPath].PathWithoutExtension);
- var pastLevelRewardsTask = provider.LoadObjectAsync(provider[pastLevelRewardsPath].PathWithoutExtension);
- var defaultGameDataTask = provider.LoadObjectAsync(provider[defaultGameDataPath].PathWithoutExtension);
+ var levelRewardsTask = provider.SafeLoadPackageObjectAsync(provider[levelRewardsPath].PathWithoutExtension);
+ var pastLevelRewardsTask = provider.SafeLoadPackageObjectAsync(provider[pastLevelRewardsPath].PathWithoutExtension);
+ var defaultGameDataTask = provider.SafeLoadPackageObjectAsync(provider[defaultGameDataPath].PathWithoutExtension);
ExportLevelRewards(await levelRewardsTask, output, cancellationToken);
ExportPastLevelRewards(await pastLevelRewardsTask, output, cancellationToken);
ExportPastLevelXPRequirements(await defaultGameDataTask, output, cancellationToken);
}
- private static void ExportPastLevelXPRequirements(UObject defaultGameData, IAssetOutput output, CancellationToken cancellationToken)
+ private static void ExportPastLevelXPRequirements(UObject? defaultGameData, IAssetOutput output, CancellationToken cancellationToken)
{
+ if (defaultGameData is null)
+ return;
cancellationToken.ThrowIfCancellationRequested();
var map = defaultGameData.Get("PhoenixEventOverlevelXPPerLevel");
@@ -70,8 +72,10 @@ private static void ExportPastLevelXPRequirements(UObject defaultGameData, IAsse
}
}
- private static void ExportLevelRewards(UDataTable levelRewardsTable, IAssetOutput output, CancellationToken cancellationToken = default)
+ private static void ExportLevelRewards(UDataTable? levelRewardsTable, IAssetOutput output, CancellationToken cancellationToken = default)
{
+ if (levelRewardsTable is null)
+ return;
int level = 1;
foreach (var (key, row) in levelRewardsTable.RowMap)
@@ -111,8 +115,10 @@ private static void ExportLevelRewards(UDataTable levelRewardsTable, IAssetOutpu
}
}
- private static void ExportPastLevelRewards(UDataTable pastLevelRewardsTable, IAssetOutput output, CancellationToken cancellationToken = default)
+ private static void ExportPastLevelRewards(UDataTable? pastLevelRewardsTable, IAssetOutput output, CancellationToken cancellationToken = default)
{
+ if (pastLevelRewardsTable is null)
+ return;
int pastLevel = 1;
foreach (var (key, row) in pastLevelRewardsTable.RowMap)
diff --git a/BanjoBotAssets/PostExporters/ImageFilesPostExporter.cs b/BanjoBotAssets/PostExporters/ImageFilesPostExporter.cs
index 34277f8..120c253 100644
--- a/BanjoBotAssets/PostExporters/ImageFilesPostExporter.cs
+++ b/BanjoBotAssets/PostExporters/ImageFilesPostExporter.cs
@@ -18,6 +18,7 @@
using BanjoBotAssets.Config;
using CUE4Parse.UE4.Assets.Exports.Texture;
using CUE4Parse_Conversion.Textures;
+using CUE4Parse_Conversion.Textures.BC;
using Microsoft.Extensions.Options;
namespace BanjoBotAssets.PostExporters
@@ -44,6 +45,8 @@ public async Task ProcessExportsAsync(ExportedAssets exportedAssets, IList(imagePath);
- using var bitmap = asset.Decode();
+ var asset = await provider.SafeLoadPackageObjectAsync(imagePath);
+ var bitmap = asset?.Decode();
if (bitmap == null)
{
logger.LogError(Resources.Error_CannotDecodeTexture, imagePath);
continue;
}
- await using var stream = new FileStream(exportedPath, FileMode.Create, FileAccess.Write);
- if (!bitmap.Encode(stream, SkiaSharp.SKEncodedImageFormat.Png, 100))
+ var bytes = bitmap.Encode(ETextureFormat.Png, false, out var ext);
+ if (ext != "png")
{
logger.LogError(Resources.Error_CannotEncodeTexture, imagePath);
}
+ await using var stream = new FileStream(exportedPath, FileMode.Create, FileAccess.Write);
+ stream.Write(bytes, 0, bytes.Length);
filesWritten++;
}
}
diff --git a/BanjoBotAssets/Program.cs b/BanjoBotAssets/Program.cs
index f5727e3..e34c311 100644
--- a/BanjoBotAssets/Program.cs
+++ b/BanjoBotAssets/Program.cs
@@ -23,6 +23,12 @@
[assembly: InternalsVisibleTo("BanjoBotAssets.SourceGenerators.Tests")]
+namespace BanjoBotAssets;
+
+internal class OldProgram
+{
+ internal async Task Main(string[] args)
+ {
// TODO: export per-difficulty stat clamp tables (GameDifficultyGrowthBounds, CombatStatClampsPerTheater)
// TODO: export collection book categories and recruitment/research/voucher options (CollectionBookSlots)
@@ -54,3 +60,5 @@ await Host.CreateDefaultBuilder(args)
.RunConsoleAsync(o => o.SuppressStatusMessages = true);
return Environment.ExitCode;
+ }
+}
diff --git a/BanjoBotAssets/Reporters/IExportProgressReporter.cs b/BanjoBotAssets/Reporters/IExportProgressReporter.cs
new file mode 100644
index 0000000..483ec37
--- /dev/null
+++ b/BanjoBotAssets/Reporters/IExportProgressReporter.cs
@@ -0,0 +1,31 @@
+/* Copyright 2026 Tara "Dino" Cassatt
+ *
+ * This file is part of BanjoBotAssets.
+ *
+ * BanjoBotAssets is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * BanjoBotAssets is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with BanjoBotAssets. If not, see .
+ */
+using BanjoBotAssets.Exporters;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BanjoBotAssets.Reporters
+{
+ public interface IExportProgressReporter
+ {
+ public void Report(ExportProgress progress);
+ }
+}
diff --git a/BanjoBotAssets/Reporters/IExportStageReporter.cs b/BanjoBotAssets/Reporters/IExportStageReporter.cs
new file mode 100644
index 0000000..687c98b
--- /dev/null
+++ b/BanjoBotAssets/Reporters/IExportStageReporter.cs
@@ -0,0 +1,31 @@
+/* Copyright 2026 Tara "Dino" Cassatt
+ *
+ * This file is part of BanjoBotAssets.
+ *
+ * BanjoBotAssets is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * BanjoBotAssets is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with BanjoBotAssets. If not, see .
+ */
+using BanjoBotAssets.Exporters;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace BanjoBotAssets.Reporters
+{
+ public interface IExportStageReporter
+ {
+ public void Report(ExportStage stage);
+ }
+}
diff --git a/external/CUE4Parse b/external/CUE4Parse
index 0566f26..ecc4878 160000
--- a/external/CUE4Parse
+++ b/external/CUE4Parse
@@ -1 +1 @@
-Subproject commit 0566f26df1a8fab6e7980fc8dc498487953d9378
+Subproject commit ecc4878950336126f125af0747190edf474b2a21
diff --git a/external/CUE4Parse-FortniteTypes b/external/CUE4Parse-FortniteTypes
index 424118b..72fbc06 160000
--- a/external/CUE4Parse-FortniteTypes
+++ b/external/CUE4Parse-FortniteTypes
@@ -1 +1 @@
-Subproject commit 424118b4072aaccf64dbb3f6e801860c5945b3a1
+Subproject commit 72fbc06ef0cc31c7ac8778f9c94c435272410426