Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions BanjoBotAssets.Console/BanjoBotAssets.Console.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<None Remove="*.dll" />
</ItemGroup>

<ItemGroup>
<Content Remove="aes.json" />
<Content Remove="assets.json" />
<Content Remove="schematics.json" />
</ItemGroup>

<ItemGroup>
<None Remove="mappings.usmap" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BanjoBotAssets\BanjoBotAssets.csproj" />
</ItemGroup>

<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
66 changes: 66 additions & 0 deletions BanjoBotAssets.Console/Program.cs
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/


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<IExportStageReporter, ExampleStageReporter>();
//services.AddSingleton<IExportProgressReporter, ExampleProgressReporter>();
})
.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}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
10 changes: 8 additions & 2 deletions BanjoBotAssets.sln
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 36 additions & 6 deletions BanjoBotAssets/AssetExportService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +30,19 @@

namespace BanjoBotAssets
{
public enum ExportStage
{
DecryptingGameFiles,
InitialisingOodle,
LoadingVirtualPaths,
LoadingMappings,
LoadingLocalisation,
PreparingInterestingAssets,
RunningExporters,
RunningPostExporters,
GeneratingArtifacts,
}

internal sealed partial class AssetExportService(
ILogger<AssetExportService> logger,
IHostApplicationLifetime lifetime,
Expand All @@ -40,7 +54,9 @@ internal sealed partial class AssetExportService(
ITypeMappingsProviderFactory typeMappingsProviderFactory,
IOptions<ScopeOptions> scopeOptions,
IEnumerable<IPostExporter> allPostExporters,
LanguageProvider languageProvider) : BackgroundService
LanguageProvider languageProvider,
IExportProgressReporter? progressReporter = null,
IExportStageReporter? stageReporter = null) : BackgroundService
{
private readonly List<IExporter> exportersToRun = MakeExportersToRun(allExporters, scopeOptions);
private readonly ConcurrentDictionary<string, byte> failedAssets = new();
Expand Down Expand Up @@ -92,36 +108,46 @@ private async Task<int> 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
Expand Down Expand Up @@ -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()
Expand All @@ -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();
}
Expand Down Expand Up @@ -312,7 +338,7 @@ private void HandleProgressReport(ExportProgress progress)
}
}

// TODO: do something more with progress reports
progressReporter?.Report(progress);
}

private void ReportFailedAssets()
Expand Down Expand Up @@ -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));
}
}
}
4 changes: 2 additions & 2 deletions BanjoBotAssets/BanjoBotAssets.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">

<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
Expand Down Expand Up @@ -54,7 +54,7 @@
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Roslynator.Analyzers" Version="4.12.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
44 changes: 44 additions & 0 deletions BanjoBotAssets/BanjoEntryPoint.cs
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<AssetExportService>()
.AddHttpClient()
.AddAesProviders()
.AddMappingsProviders()
.AddGameFileProvider()
.AddAssetExporters();

services
.AddOptions<ScopeOptions>()
.Configure<IConfiguration>((scope, config) => config.Bind(scope));

return services;
}
}
}
12 changes: 6 additions & 6 deletions BanjoBotAssets/CachingFileProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,26 +98,26 @@ private void WriteToAssetLog(string line)
}
}

public override Task<IPackage> 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);

cacheMissesByPath.AddOrUpdate(file.Path, 1, (_, i) => i + 1);
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<IPackage?> TryLoadPackageAsync(GameFile file)
public override Task<IPackage> LoadPackageAsync(GameFile file)
{
Interlocked.Increment(ref cacheRequests);

Expand All @@ -131,7 +131,7 @@ public override Task<IPackage> 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 });
}
Expand Down
Loading
Loading