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