feat(launcher): stabilize prepared content updates
Some checks failed
CI / linux-portable (push) Failing after 3m12s
CI / windows-gate (push) Failing after 6m35s
CI / release (push) Has been skipped

This commit is contained in:
Erik 2026-08-25 19:17:13 +02:00
parent f160f3fee1
commit af9327a17b
42 changed files with 3706 additions and 147 deletions

View file

@ -51,6 +51,9 @@ internal sealed record ContentEffectsAudioResult(
internal sealed record ContentEffectsAudioDependencies(
string DatDirectory,
string PreparedAssetPath,
string? PreparedAssetOverlayPath,
uint? PreparedAssetBaseRecipeVersion,
uint? PreparedAssetEffectiveRecipeVersion,
ResidencyBudgetOptions ResidencyBudgets,
PhysicsDataCache PhysicsDataCache,
bool DumpMotionEnabled,
@ -99,6 +102,9 @@ internal interface IContentEffectsAudioCompositionFactory
IDatReaderWriter OpenDatCollection(string datDirectory);
IPreparedAssetSource OpenPreparedAssetSource(
string path,
string? overlayPath,
uint? baseRecipeVersion,
uint? effectiveRecipeVersion,
IDatReaderWriter dats,
Action<string> diagnostic);
MagicCatalog LoadMagicCatalog(IDatReaderWriter dats);
@ -169,9 +175,54 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
public IPreparedAssetSource OpenPreparedAssetSource(
string path,
string? overlayPath,
uint? baseRecipeVersion,
uint? effectiveRecipeVersion,
IDatReaderWriter dats,
Action<string> diagnostic) =>
new PakPreparedAssetSource(path, dats, diagnostic);
Action<string> diagnostic)
{
if (string.IsNullOrWhiteSpace(overlayPath))
{
return new PakPreparedAssetSource(path, dats, diagnostic);
}
if (baseRecipeVersion is not > 0
|| effectiveRecipeVersion is not > 0)
{
throw new InvalidDataException(
"Layered prepared content is missing its recipe identities.");
}
if (effectiveRecipeVersion
!= AcDream.Content.Pak.PakFormat.CurrentBakeToolVersion)
{
throw new InvalidDataException(
$"Prepared content recipe {effectiveRecipeVersion} does not "
+ $"match client recipe "
+ $"{AcDream.Content.Pak.PakFormat.CurrentBakeToolVersion}.");
}
PakPreparedAssetSource? baseSource = null;
PakPreparedAssetSource? overlaySource = null;
try
{
baseSource = new PakPreparedAssetSource(
path,
PreparedAssetCatalogIdentity.From(dats, baseRecipeVersion.Value),
diagnostic);
overlaySource = new PakPreparedAssetSource(
overlayPath,
PreparedAssetCatalogIdentity.From(dats, effectiveRecipeVersion.Value),
diagnostic);
return new LayeredPreparedAssetSource(baseSource, overlaySource);
}
catch
{
overlaySource?.Dispose();
baseSource?.Dispose();
throw;
}
}
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) =>
MagicCatalog.Load(dats);
@ -370,6 +421,9 @@ internal sealed class ContentEffectsAudioCompositionPhase :
"prepared asset source",
() => _factory.OpenPreparedAssetSource(
_dependencies.PreparedAssetPath,
_dependencies.PreparedAssetOverlayPath,
_dependencies.PreparedAssetBaseRecipeVersion,
_dependencies.PreparedAssetEffectiveRecipeVersion,
dats,
_dependencies.Error),
static value => value.Dispose()).Publish(

View file

@ -71,6 +71,12 @@ internal sealed class SessionContentDescriptor
[JsonRequired]
public string PreparedAssetPath { get; init; } = string.Empty;
public string? PreparedAssetOverlayPath { get; init; }
public uint? PreparedAssetBaseRecipeVersion { get; init; }
public uint? PreparedAssetEffectiveRecipeVersion { get; init; }
}
internal sealed record SessionDescriptor

View file

@ -83,6 +83,18 @@ internal static class SessionConfigurationLoader
throw new SessionConfigurationException(
"process.content requires non-empty datDirectory and preparedAssetPath.");
}
bool hasOverlay = !string.IsNullOrWhiteSpace(
content.PreparedAssetOverlayPath);
bool hasBaseRecipe = content.PreparedAssetBaseRecipeVersion is > 0;
bool hasEffectiveRecipe =
content.PreparedAssetEffectiveRecipeVersion is > 0;
if (hasOverlay != hasBaseRecipe || hasOverlay != hasEffectiveRecipe)
{
throw new SessionConfigurationException(
"process.content overlay path, base recipe, and effective recipe "
+ "must be supplied together.");
}
}
private static void ValidateSession(SessionDescriptor session)

View file

@ -1418,6 +1418,9 @@ public sealed class GameWindow :
new ContentEffectsAudioDependencies(
_datDir,
_options.PreparedAssetPath,
_options.PreparedAssetOverlayPath,
_options.PreparedAssetBaseRecipeVersion,
_options.PreparedAssetEffectiveRecipeVersion,
_options.ResidencyBudgets,
_physicsDataCache,
_animationDiagnostics.DumpMotionEnabled,

View file

@ -116,6 +116,12 @@ public sealed record RuntimeOptions(
/// <see cref="LoginCommands"/>, milliseconds.</summary>
int LoginCommandDelayMs)
{
public string? PreparedAssetOverlayPath { get; init; }
public uint? PreparedAssetBaseRecipeVersion { get; init; }
public uint? PreparedAssetEffectiveRecipeVersion { get; init; }
/// <summary>
/// Build options from the process environment. Used by
/// <c>Program.cs</c> at startup.
@ -272,6 +278,12 @@ public sealed record RuntimeOptions(
{
PreparedAssetPath = NullIfEmpty(content?.PreparedAssetPath)
?? baseOptions.PreparedAssetPath,
PreparedAssetOverlayPath =
NullIfEmpty(content?.PreparedAssetOverlayPath),
PreparedAssetBaseRecipeVersion =
content?.PreparedAssetBaseRecipeVersion,
PreparedAssetEffectiveRecipeVersion =
content?.PreparedAssetEffectiveRecipeVersion,
LiveMode = true,
// Campaign LA gate round 2: a session-config launch IS a product
// launch — the retail UI is the shipped UI, not a dev option.