Skip to content

Commit

Permalink
Add MasterAsset extensions (#928)
Browse files Browse the repository at this point in the history
Adds support for specifying an extension, with an optional feature flag,
to MasterAsset. This will generate code that loads an additional file to
override the contents of a master asset at load time, which will be
gated by the feature flag if specified.

This is primarily to enable some small-scale "mods" that I want to
implement, specifically around boosting daily endeavour rewards in the
absence of regular events. All such mods will be put behind feature
flags so that they can be turned off and this project can still act as a
'reference implementation' of a vanilla Dragalia Lost backend.

A secondary purpose, which has been used as a test run here, is to
'patch' the MasterAsset where it contains incorrect data. A previous
workaround for welfare adventurer story IDs being incorrect has been
repurposed to use this new functionality.
  • Loading branch information
SapiensAnatis authored Jul 5, 2024
1 parent 3d0d8b4 commit 2ae2105
Show file tree
Hide file tree
Showing 32 changed files with 1,139 additions and 320 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.6.0" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.FeatureManagement" Version="3.4.0" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.6" />
Expand Down
3 changes: 2 additions & 1 deletion DragaliaAPI/DragaliaAPI.Database.Test/ModuleInitializer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using DragaliaAPI.Shared.MasterAsset;
using DragaliaAPI.Test.Utils;

namespace DragaliaAPI.Database.Test;

Expand All @@ -16,7 +17,7 @@ public static class ModuleInitializer
public static void InitializeMasterAsset()
{
TaskFactory
.StartNew(MasterAsset.LoadAsync)
.StartNew(() => MasterAsset.LoadAsync(FeatureFlagUtils.AllEnabledFeatureManager))
.Unwrap()
.ConfigureAwait(false)
.GetAwaiter()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public CustomWebApplicationFactory()

public async Task InitializeAsync()
{
await MasterAsset.LoadAsync();
await MasterAsset.LoadAsync(FeatureFlagUtils.AllEnabledFeatureManager);

await this.testContainersHelper.StartAsync();

Expand Down
69 changes: 37 additions & 32 deletions DragaliaAPI/DragaliaAPI.MasterAssetConverter/AttributeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,58 @@ namespace DragaliaAPI.MasterAssetConverter;

public static class AttributeHelper
{
public static AttributeInstance ParseAttribute(Attribute attribute)
public static GenerateMasterAssetAttributeInstance ParseGenerateMasterAssetAttribute(
Attribute attribute
)
{
Type attributeType = attribute.GetType();
Type itemType = attributeType.GetGenericArguments()[0];

if (
attributeType
.GetProperty(nameof(GenerateMasterAssetAttribute<CharaData>.Filepath))
?.GetValue(attribute)
is not string jsonPath
)
{
throw new InvalidOperationException("Failed to get file path");
}
string jsonPath = attribute.GetPropertyValue<string>(
nameof(GenerateMasterAssetAttribute<CharaData>.Filepath)
);

if (
attributeType
.GetProperty(nameof(GenerateMasterAssetAttribute<CharaData>.Key))
?.GetValue(attribute)
is not string key
)
{
throw new InvalidOperationException("Failed to get key");
}
string key = attribute.GetPropertyValue<string>(
nameof(GenerateMasterAssetAttribute<CharaData>.Key)
);

if (
attributeType
.GetProperty(nameof(GenerateMasterAssetAttribute<CharaData>.Group))
?.GetValue(attribute)
is not bool group
)
{
throw new InvalidOperationException("Failed to get group");
}
bool group = attribute.GetPropertyValue<bool>(
nameof(GenerateMasterAssetAttribute<CharaData>.Group)
);

Type keyType =
itemType.GetProperty(key)?.PropertyType
?? throw new InvalidOperationException("Failed to get key type");

return new AttributeInstance(itemType, keyType, jsonPath, key, group);
return new GenerateMasterAssetAttributeInstance(itemType, keyType, jsonPath, group);
}
}

file static class AttributeExtensions
{
public static TValue GetPropertyValue<TValue>(this Attribute attribute, string propertyName)
{
object value =
attribute.GetType().GetProperty(propertyName)?.GetValue(attribute)
?? throw new InvalidOperationException($"Failed to get property value: {propertyName}");

return (TValue)value;
}
}

public record AttributeInstance(
public record GenerateMasterAssetAttributeInstance(
Type ItemType,
Type KeyType,
string JsonPath,
string Key,
bool Group
);
)
{
public string PropertyName
{
get
{
string rawPath = this.JsonPath.Replace(".json", "");
return rawPath.Split('/')[^1];
}
}
}
31 changes: 25 additions & 6 deletions DragaliaAPI/DragaliaAPI.MasterAssetConverter/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,36 @@
using DragaliaAPI.Shared.Serialization;
using MessagePack;

string resourcesPath = args[^2];
string outputDir = args[^1];
string resourcesPath = args[^2];

List<AttributeInstance> attributeInstances = typeof(MasterAsset)
List<GenerateMasterAssetAttributeInstance> attributeInstances = typeof(MasterAsset)
.GetCustomAttributes(typeof(GenerateMasterAssetAttribute<>))
.Select(AttributeHelper.ParseAttribute)
.Select(AttributeHelper.ParseGenerateMasterAssetAttribute)
.ToList();

foreach (AttributeInstance instance in attributeInstances)
Dictionary<string, ExtendMasterAssetAttribute> extensionAttributes = typeof(MasterAsset)
.Assembly.GetCustomAttributes<ExtendMasterAssetAttribute>()
.ToDictionary(x => x.MasterAssetName, x => x);

foreach (GenerateMasterAssetAttributeInstance instance in attributeInstances)
{
await Convert(instance.JsonPath, instance);

if (
extensionAttributes.TryGetValue(
instance.PropertyName,
out ExtendMasterAssetAttribute? extensionAttribute
)
)
{
await Convert(extensionAttribute.Filepath, instance);
}
}

async Task Convert(string inputPath, GenerateMasterAssetAttributeInstance instance)
{
string fullJsonPath = Path.Combine(resourcesPath, instance.JsonPath);
string fullJsonPath = Path.Combine(resourcesPath, inputPath);
string relativePath = Path.GetRelativePath(resourcesPath, fullJsonPath);
string outputPath = Path.Combine(outputDir, relativePath.Replace(".json", ".msgpack"));

Expand All @@ -28,7 +47,7 @@
Console.WriteLine(
$"Skipping conversion of {relativePath} - binary converted file is newer"
);
continue;
return;
}

Type listType = typeof(IList<>).MakeGenericType(instance.ItemType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

namespace DragaliaAPI.Shared.MasterAsset;

[System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
[global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public sealed class GenerateMasterAssetAttribute<TItem> : System.Attribute
where TItem : class
{
Expand All @@ -19,4 +19,20 @@ public GenerateMasterAssetAttribute(string filepath)
public string Key { get; set; } = "Id";

public bool Group { get; set; }
}

[global::System.AttributeUsage(global::System.AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)]
public sealed class ExtendMasterAssetAttribute : System.Attribute
{
public ExtendMasterAssetAttribute(string masterAssetName, string filepath)
{
this.MasterAssetName = masterAssetName;
this.Filepath = filepath;
}

public string MasterAssetName { get; }

public string Filepath { get; }

public string? FeatureFlag { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//HintName: MasterAsset.Extensions.g.cs
// <auto-generated/>

#nullable enable

namespace DragaliaAPI.Shared.MasterAsset;

public static partial class MasterAsset
{
private static async Task<List<TItem>> LoadFile<TItem>(string msgpackPath)
{
string path = Path.Join(
global::System.IO.Path.GetDirectoryName(global::System.Reflection.Assembly.GetExecutingAssembly().Location),
"Resources",
msgpackPath
);

await using FileStream fs = File.OpenRead(path);

return await global::MessagePack.MessagePackSerializer.DeserializeAsync<List<TItem>>(
fs,
MasterAssetMessagePackOptions.Instance
) ?? throw new global::MessagePack.MessagePackSerializationException($"Deserialized MasterAsset extension for {path} was null");
}

public static async global::System.Threading.Tasks.Task<global::System.Collections.Generic.IEnumerable<global::DragaliaAPI.Shared.MasterAsset.Models.Event.EventData>> LoadEventDataExtension(global::Microsoft.FeatureManagement.IFeatureManager featureManager)
{
global::System.Collections.Generic.List<global::DragaliaAPI.Shared.MasterAsset.Models.Event.EventData> extendedData = [];

extendedData.AddRange(await LoadFile<global::DragaliaAPI.Shared.MasterAsset.Models.Event.EventData>("Event/BuildEventReward.extension.msgpack"));

return extendedData;
}

public static async global::System.Threading.Tasks.Task<global::System.Collections.Generic.IEnumerable<global::DragaliaAPI.Shared.MasterAsset.Models.DragonData>> LoadDragonDataExtension(global::Microsoft.FeatureManagement.IFeatureManager featureManager)
{
global::System.Collections.Generic.List<global::DragaliaAPI.Shared.MasterAsset.Models.DragonData> extendedData = [];

if (await featureManager.IsEnabledAsync("ModdedDragons"))
{
extendedData.AddRange(await LoadFile<global::DragaliaAPI.Shared.MasterAsset.Models.DragonData>("DragonData.modded.msgpack"));
}

return extendedData;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,58 @@ public static partial class MasterAsset

private static global::DragaliaAPI.Shared.MasterAsset.MasterAssetGroup<int, int, global::DragaliaAPI.Shared.MasterAsset.Models.Event.BuildEventReward>? buildEventReward;
public static global::DragaliaAPI.Shared.MasterAsset.MasterAssetGroup<int, int, global::DragaliaAPI.Shared.MasterAsset.Models.Event.BuildEventReward> BuildEventReward => buildEventReward ?? throw new InvalidOperationException(ErrorUninitialized);
public static async Task LoadAsync()

private static global::DragaliaAPI.Shared.MasterAsset.MasterAssetData<int, global::DragaliaAPI.Shared.MasterAsset.Models.Event.EventData>? eventData;
public static global::DragaliaAPI.Shared.MasterAsset.MasterAssetData<int, global::DragaliaAPI.Shared.MasterAsset.Models.Event.EventData> EventData => eventData ?? throw new InvalidOperationException(ErrorUninitialized);

private static global::DragaliaAPI.Shared.MasterAsset.MasterAssetData<global::DragaliaAPI.Shared.Definitions.Enums.Dragons, global::DragaliaAPI.Shared.MasterAsset.Models.DragonData>? dragonData;
public static global::DragaliaAPI.Shared.MasterAsset.MasterAssetData<global::DragaliaAPI.Shared.Definitions.Enums.Dragons, global::DragaliaAPI.Shared.MasterAsset.Models.DragonData> DragonData => dragonData ?? throw new InvalidOperationException(ErrorUninitialized);
public static async Task LoadAsync(global::Microsoft.FeatureManagement.IFeatureManager featureManager)
{
if (loaded)
{
return;
}

global::System.Threading.Tasks.ValueTask<global::DragaliaAPI.Shared.MasterAsset.MasterAssetData<global::DragaliaAPI.Shared.Definitions.Enums.Charas, global::DragaliaAPI.Shared.MasterAsset.Models.CharaData>> charaDataTask =
global::DragaliaAPI.Shared.MasterAsset.MasterAssetData.LoadAsync<global::DragaliaAPI.Shared.Definitions.Enums.Charas, global::DragaliaAPI.Shared.MasterAsset.Models.CharaData>(
"CharaData.msgpack",
x => x.Id
(global::DragaliaAPI.Shared.MasterAsset.Models.CharaData x) => x.Id,
null
);

global::System.Threading.Tasks.ValueTask<global::DragaliaAPI.Shared.MasterAsset.MasterAssetData<int, global::DragaliaAPI.Shared.MasterAsset.Models.TimeAttack.RankingData>> rankingDataTask =
global::DragaliaAPI.Shared.MasterAsset.MasterAssetData.LoadAsync<int, global::DragaliaAPI.Shared.MasterAsset.Models.TimeAttack.RankingData>(
"TimeAttack/RankingData.msgpack",
x => x.QuestId
(global::DragaliaAPI.Shared.MasterAsset.Models.TimeAttack.RankingData x) => x.QuestId,
null
);

global::System.Threading.Tasks.ValueTask<global::DragaliaAPI.Shared.MasterAsset.MasterAssetGroup<int, int, global::DragaliaAPI.Shared.MasterAsset.Models.Event.BuildEventReward>> buildEventRewardTask =
global::DragaliaAPI.Shared.MasterAsset.MasterAssetGroup.LoadAsync<int, int, global::DragaliaAPI.Shared.MasterAsset.Models.Event.BuildEventReward>(
"Event/BuildEventReward.msgpack",
x => x.Id
(global::DragaliaAPI.Shared.MasterAsset.Models.Event.BuildEventReward x) => x.Id
);

global::System.Threading.Tasks.ValueTask<global::DragaliaAPI.Shared.MasterAsset.MasterAssetData<int, global::DragaliaAPI.Shared.MasterAsset.Models.Event.EventData>> eventDataTask =
global::DragaliaAPI.Shared.MasterAsset.MasterAssetData.LoadAsync<int, global::DragaliaAPI.Shared.MasterAsset.Models.Event.EventData>(
"Event/EventData.msgpack",
(global::DragaliaAPI.Shared.MasterAsset.Models.Event.EventData x) => x.Id,
await LoadEventDataExtension(featureManager)
);

global::System.Threading.Tasks.ValueTask<global::DragaliaAPI.Shared.MasterAsset.MasterAssetData<global::DragaliaAPI.Shared.Definitions.Enums.Dragons, global::DragaliaAPI.Shared.MasterAsset.Models.DragonData>> dragonDataTask =
global::DragaliaAPI.Shared.MasterAsset.MasterAssetData.LoadAsync<global::DragaliaAPI.Shared.Definitions.Enums.Dragons, global::DragaliaAPI.Shared.MasterAsset.Models.DragonData>(
"DragonData.msgpack",
(global::DragaliaAPI.Shared.MasterAsset.Models.DragonData x) => x.Id,
await LoadDragonDataExtension(featureManager)
);

charaData = await charaDataTask;
rankingData = await rankingDataTask;
buildEventReward = await buildEventRewardTask;
eventData = await eventDataTask;
dragonData = await dragonDataTask;
loaded = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//HintName: MasterAssetMessagePackOptions.g.cs
// <auto-generated/>

#nullable enable

namespace DragaliaAPI.Shared.MasterAsset;

public class MasterAssetMessagePackOptions
{
public static global::MessagePack.MessagePackSerializerOptions Instance { get; } =
global::MessagePack.MessagePackSerializerOptions
.Standard.WithResolver(global::MessagePack.Resolvers.ContractlessStandardResolver.Instance)
.WithCompression(global::MessagePack.MessagePackCompression.Lz4BlockArray);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,21 @@ namespace DragaliaAPI.Shared.MasterAsset;
[GenerateMasterAsset<CharaData>("CharaData.json")]
[GenerateMasterAsset<RankingData>("TimeAttack/RankingData.json", Key = nameof(Models.TimeAttack.RankingData.QuestId))]
[GenerateMasterAsset<BuildEventReward>("Event/BuildEventReward.json", Group = true)]
[GenerateMasterAsset<EventData>("Event/EventData.json")]
[GenerateMasterAsset<DragonData>("DragonData.json")]
public static partial class MasterAsset
{
}

[ExtendMasterAsset(nameof(MasterAsset.EventData), "Event/BuildEventReward.extension.json")]
public static class EventDataExtensions
{
}

[ExtendMasterAsset(nameof(MasterAsset.DragonData), "DragonData.modded.json", FeatureFlag = "ModdedDragons")]
public static class DragonDataExtensions
{
}
""";

await Verify(source);
Expand All @@ -40,7 +52,7 @@ private static Task Verify(string source)

CSharpCompilation compilation = CSharpCompilation.Create(
assemblyName: "Tests",
syntaxTrees: new[] { syntaxTree },
syntaxTrees: [syntaxTree],
references
);

Expand Down
Loading

0 comments on commit 2ae2105

Please sign in to comment.