Preparation fo Mystery rolling
This commit is contained in:
@@ -9,15 +9,16 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Identity" Version="1.13.2" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" />
|
||||
<PackageReference Include="Microsoft.Azure.WebPubSub.AspNetCore" Version="1.4.0" />
|
||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.2" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.21.0" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.28.0" />
|
||||
<PackageReference Include="Microsoft.Azure.WebPubSub.AspNetCore" Version="1.5.0" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
|
||||
<PackageReference Include="System.Text.Json" Version="10.0.8" />
|
||||
<PackageReference Include="YamlDotNet" Version="18.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace ALttPRandomizer.Azure {
|
||||
using global::Azure.Storage.Blobs;
|
||||
using global::Azure.Storage.Blobs.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -43,7 +44,7 @@
|
||||
|
||||
public async Task<Dictionary<string, BinaryData>> GetFiles(string seedId) {
|
||||
var prefix = seedId + "/";
|
||||
var blobs = this.BlobClient.GetBlobsAsync(prefix: prefix);
|
||||
var blobs = this.BlobClient.GetBlobsAsync(new GetBlobsOptions() { Prefix = prefix });
|
||||
|
||||
var data = new Dictionary<string, BinaryData>();
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
namespace ALttPRandomizer {
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ALttPRandomizer.Serialization;
|
||||
|
||||
public static class JsonOptions {
|
||||
public static readonly JsonSerializerOptions Default = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
|
||||
@@ -10,6 +11,7 @@
|
||||
|
||||
public static JsonSerializerOptions WithStringEnum(this JsonSerializerOptions options) {
|
||||
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower, false));
|
||||
options.Converters.Add(new RandomizableWeightsJsonConverter());
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
153
ALttPRandomizer/Model/MysterySettings.cs
Normal file
153
ALttPRandomizer/Model/MysterySettings.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
namespace ALttPRandomizer.Model {
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
public class MysterySettings {
|
||||
public RandomizableWeights<Mode> Mode { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<Weapons> Weapons { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<Goal> Goal { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<EntryRequirement> CrystalsGanon { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<BossRequirement> BossesGanon { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<TriforceRequirement> TriforcePieces { get; set; } = new();
|
||||
|
||||
[YamlMember(Alias = "crystals_gt")]
|
||||
public RandomizableWeights<EntryRequirement> CrystalsGT { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<GanonItem> GanonItem { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<EntranceShuffle> EntranceShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<OverworldMapDungeons> OverworldMapDungeons { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<LinksHouse> LinksHouse { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<SkullWoodsShuffle> SkullWoods { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<LinkedDrops> LinkedDrops { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<BossShuffle> BossShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<EnemyShuffle> EnemyShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DamageTableShuffle> DamageTableShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<KeyLocations> SmallKeys { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DungeonItemLocations> BigKeys { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DungeonItemLocations> Maps { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DungeonItemLocations> Compasses { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<ShowLoot> ShowLoot { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<ShowLootHud> ShowLootHud { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<ShowMap> ShowMap { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<ShopShuffle> ShopShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DropShuffle> DropShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<PotShuffle> PotShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<PrizeShuffle> PrizeShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<BootsSettings> Boots { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<FluteSettings> Flute { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DarkRoomSettings> DarkRooms { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<BombSettings> Bombs { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<BookSettings> Book { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<MirrorSettings> Mirror { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DoorShuffle> DoorShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DoorLobbies> Lobbies { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DoorTypeMode> DoorTypeMode { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<TrapDoorMode> TrapDoorMode { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<ExtraKeysMode> ExtraKeys { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<FollowerShuffle> FollowerShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<FluteShuffle> FluteShuffle { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<OverworldLayout> OverworldLayout { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<OverworldWorldLayouts> OverworldWorldLayouts { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<OverworldLayoutTerrain> OverworldLayoutTerrain { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<OverworldLayoutEdges> OverworldLayoutEdges { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<OverworldMapFog> OverworldMapFog { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<TileSwap> TileSwap { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<DamageChallengeMode> DamageChallenge { get; set; } = new();
|
||||
|
||||
public RandomizableWeights<Hints> Hints { get; set; } = new();
|
||||
}
|
||||
|
||||
public class DefaultMysterySettingsFilter : IOperationFilter {
|
||||
private readonly MysterySettings exampleMystery;
|
||||
private readonly JsonNode? exampleMysteryJson;
|
||||
|
||||
public DefaultMysterySettingsFilter() {
|
||||
this.exampleMystery = new MysterySettings();
|
||||
|
||||
var mysteryFields = typeof(MysterySettings).GetProperties();
|
||||
|
||||
foreach (var field in mysteryFields) {
|
||||
var type = field.PropertyType;
|
||||
|
||||
if (!type.IsGenericType || type.GetGenericTypeDefinition() != typeof(RandomizableWeights<>)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Type itemType = type.GetGenericArguments()[0];
|
||||
Type innerType = typeof(RandomizableWeights<>).MakeGenericType(itemType)!;
|
||||
|
||||
var weights = innerType.GetProperty("EqualAll")?.GetValue(null);
|
||||
|
||||
if (weights != null) {
|
||||
field.SetValue(this.exampleMystery, weights);
|
||||
}
|
||||
}
|
||||
|
||||
this.exampleMysteryJson = JsonSerializer.SerializeToNode(this.exampleMystery, JsonOptions.Default);
|
||||
}
|
||||
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context) {
|
||||
foreach (var description in context.ApiDescription.ParameterDescriptions) {
|
||||
if (description.Type != typeof(MysterySettings)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (description.Source.Id == "Body") {
|
||||
if (operation.RequestBody != null && operation.RequestBody.Content != null) {
|
||||
foreach ((_, var value) in operation.RequestBody.Content) {
|
||||
value.Example = this.exampleMysteryJson;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
ALttPRandomizer/Model/RandomizableWeights.cs
Normal file
80
ALttPRandomizer/Model/RandomizableWeights.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
namespace ALttPRandomizer.Model {
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
public interface IRandomizableWeights {
|
||||
public bool IsEmpty { get; }
|
||||
|
||||
public object? Roll();
|
||||
}
|
||||
|
||||
public class RandomizableWeights<T> : IRandomizableWeights where T : struct, Enum {
|
||||
private static readonly Random random = new();
|
||||
|
||||
public ImmutableList<(T Item, int Weight)> Options { get; }
|
||||
public int TotalWeights { get; }
|
||||
|
||||
public RandomizableWeights(IList<(T Item, int Weight)>? items = null) {
|
||||
if (items == null) {
|
||||
this.TotalWeights = 0;
|
||||
this.Options = ImmutableList<(T, int)>.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
int totalWeight = 0;
|
||||
var builder = new List<(T, int)>();
|
||||
foreach (var pair in items) {
|
||||
if (pair.Weight < 0) {
|
||||
throw new ArgumentException("Weights must be non-negative", $"items[{pair.Item}]");
|
||||
}
|
||||
|
||||
if (pair.Weight > 0) {
|
||||
builder.Add(pair);
|
||||
totalWeight += pair.Weight;
|
||||
}
|
||||
}
|
||||
|
||||
this.TotalWeights = totalWeight;
|
||||
this.Options = builder.ToImmutableList();
|
||||
}
|
||||
|
||||
public static RandomizableWeights<T> EmptyWeights => new();
|
||||
|
||||
public static RandomizableWeights<T> EqualAll {
|
||||
get => RandomizableWeights<T>.FromDictionary(Enum.GetValues<T>().ToDictionary(v => v, v => 1));
|
||||
}
|
||||
|
||||
public static RandomizableWeights<T> ConstantWeights(T value) {
|
||||
return new(new List<(T, int)> { (value, 100) });
|
||||
}
|
||||
|
||||
public static RandomizableWeights<T> FromDictionary(IDictionary<T, int> dict) {
|
||||
return new(dict.Select(kvp => (kvp.Key, kvp.Value)).ToList());
|
||||
}
|
||||
|
||||
public bool IsEmpty { get => this.Options.Count == 0; }
|
||||
|
||||
public T? Roll() {
|
||||
if (this.TotalWeights <= 0) {
|
||||
return default;
|
||||
}
|
||||
|
||||
int value = random.Next(this.TotalWeights);
|
||||
|
||||
foreach (var (item, weight) in this.Options) {
|
||||
value -= weight;
|
||||
|
||||
if (value < 0) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
// should never reach
|
||||
return default;
|
||||
}
|
||||
|
||||
object? IRandomizableWeights.Roll() => Roll();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace ALttPRandomizer.Model {
|
||||
using ALttPRandomizer.Settings;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using YamlDotNet.Serialization;
|
||||
using static ALttPRandomizer.Model.RandomizerInstance;
|
||||
|
||||
public class SeedSettings {
|
||||
@@ -541,7 +541,7 @@
|
||||
|
||||
public enum DamageChallengeMode {
|
||||
Normal,
|
||||
OHKO,
|
||||
[JsonStringEnumMemberName("ohko")] OHKO,
|
||||
Gloom,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
namespace ALttPRandomizer {
|
||||
using ALttPRandomizer.Azure;
|
||||
using ALttPRandomizer.Serialization;
|
||||
using ALttPRandomizer.Model;
|
||||
using ALttPRandomizer.Options;
|
||||
using ALttPRandomizer.Randomizers;
|
||||
using ALttPRandomizer.Service;
|
||||
@@ -13,7 +15,8 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Serilog;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
@@ -47,13 +50,30 @@
|
||||
});
|
||||
});
|
||||
|
||||
builder.Services.AddControllers()
|
||||
var yamlDeserializer =
|
||||
new DeserializerBuilder()
|
||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||
.WithTypeConverter(new YamlStringEnumConverter())
|
||||
.WithTypeConverter(new RandomizableWeightsYamlConverters())
|
||||
.Build();
|
||||
|
||||
var yamlFormatter =
|
||||
new YamlInputFormatter(
|
||||
yamlDeserializer,
|
||||
provider.GetRequiredService<ILogger<YamlInputFormatter>>());
|
||||
|
||||
builder.Services
|
||||
.AddControllers(options => {
|
||||
options.InputFormatters.Add(yamlFormatter);
|
||||
})
|
||||
.AddJsonOptions(x => {
|
||||
x.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
|
||||
x.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower, false));
|
||||
x.JsonSerializerOptions.WithStringEnum();
|
||||
});
|
||||
builder.Services.AddSwaggerGen(options => {
|
||||
options.OperationFilter<DefaultMysterySettingsFilter>();
|
||||
});
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var options = new DefaultAzureCredentialOptions();
|
||||
|
||||
@@ -72,6 +92,7 @@
|
||||
builder.Services.AddSingleton<ShutdownHandler>();
|
||||
|
||||
builder.Services.AddScoped<BaseRandomizer>();
|
||||
builder.Services.AddScoped<MysteryRandomizer>();
|
||||
builder.Services.AddScoped<RandomizeService>();
|
||||
builder.Services.AddScoped<SeedService>();
|
||||
builder.Services.AddScoped<IdGenerator>();
|
||||
|
||||
30
ALttPRandomizer/Randomizers/MysteryRandomizer.cs
Normal file
30
ALttPRandomizer/Randomizers/MysteryRandomizer.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace ALttPRandomizer.Randomizers {
|
||||
using System;
|
||||
using System.Linq;
|
||||
using ALttPRandomizer.Model;
|
||||
|
||||
public class MysteryRandomizer {
|
||||
private static readonly Random random = new();
|
||||
|
||||
public SeedSettings Roll(MysterySettings mystery, SeedSettings initialSettings) {
|
||||
var mysteryFields = typeof(MysterySettings).GetProperties();
|
||||
var settingsFields =
|
||||
typeof(SeedSettings).GetProperties()
|
||||
.ToDictionary(field => field.Name, field => field);
|
||||
|
||||
foreach (var field in mysteryFields) {
|
||||
if (!settingsFields.ContainsKey(field.Name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = field.GetValue(mystery);
|
||||
|
||||
if (value is IRandomizableWeights weights && !weights.IsEmpty) {
|
||||
settingsFields[field.Name].SetValue(initialSettings, weights.Roll());
|
||||
}
|
||||
}
|
||||
|
||||
return initialSettings;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,45 @@
|
||||
namespace ALttPRandomizer {
|
||||
using ALttPRandomizer.Serialization;
|
||||
using ALttPRandomizer.Model;
|
||||
using ALttPRandomizer.Randomizers;
|
||||
using ALttPRandomizer.Service;
|
||||
using ALttPRandomizer.Settings;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
public class SeedController : Controller {
|
||||
public SeedController(RandomizeService randomizeService, SeedService seedService, ILogger<SeedController> logger) {
|
||||
public SeedController(
|
||||
RandomizeService randomizeService,
|
||||
MysteryRandomizer mysteryRandomizer,
|
||||
SeedService seedService,
|
||||
ILogger<SeedController> logger) {
|
||||
this.RandomizeService = randomizeService;
|
||||
this.MysteryRandomizer = mysteryRandomizer;
|
||||
this.SeedService = seedService;
|
||||
this.Logger = logger;
|
||||
|
||||
this.YamlSerializer =
|
||||
new SerializerBuilder()
|
||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||
.WithTypeConverter(new YamlStringEnumConverter())
|
||||
.WithTypeConverter(new RandomizableWeightsYamlConverters())
|
||||
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull | DefaultValuesHandling.OmitEmptyCollections)
|
||||
.Build();
|
||||
}
|
||||
|
||||
private RandomizeService RandomizeService { get; }
|
||||
private MysteryRandomizer MysteryRandomizer { get; }
|
||||
private SeedService SeedService { get; }
|
||||
private ILogger<SeedController> Logger { get; }
|
||||
private ISerializer YamlSerializer { get; }
|
||||
|
||||
[Route("/generate")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> Generate([FromBody] SeedSettings settings) {
|
||||
public async Task<ObjectResult> Generate([FromBody] SeedSettings settings) {
|
||||
if (!ModelState.IsValid) {
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
@@ -33,9 +52,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
[Route("/mystery/{randomizer}")]
|
||||
[HttpPost]
|
||||
public ActionResult RollMystery([FromBody] MysterySettings mysterySettings, RandomizerInstance randomizer) {
|
||||
if (!ModelState.IsValid) {
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
var seedSettings = new SeedSettings() { Randomizer = randomizer };
|
||||
this.MysteryRandomizer.Roll(mysterySettings, seedSettings);
|
||||
|
||||
return StatusCode(200, this.YamlSerializer.Serialize(mysterySettings));
|
||||
}
|
||||
|
||||
[Route("/multiworld")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> GenerateMultiworld([FromBody] IList<SeedSettings> settings) {
|
||||
public async Task<ObjectResult> GenerateMultiworld([FromBody] IList<SeedSettings> settings) {
|
||||
if (!ModelState.IsValid) {
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
@@ -50,25 +82,25 @@
|
||||
|
||||
[Route("/seed/{id}")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> GetSeed(string id) {
|
||||
public async Task<ObjectResult> GetSeed(string id) {
|
||||
return ResolveResult(await this.SeedService.GetSeed(id));
|
||||
}
|
||||
|
||||
[Route("/seed/{id}")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> RetrySeed(string id) {
|
||||
public async Task<ObjectResult> RetrySeed(string id) {
|
||||
return ResolveResult(await this.RandomizeService.RetrySeed(id));
|
||||
}
|
||||
|
||||
[Route("/multi/{id}")]
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> GetMulti(string id) {
|
||||
public async Task<ObjectResult> GetMulti(string id) {
|
||||
return ResolveResult(await this.SeedService.GetMulti(id));
|
||||
}
|
||||
|
||||
[Route("/multi/{id}")]
|
||||
[HttpPost]
|
||||
public async Task<ActionResult> RetryMulti(string id) {
|
||||
public async Task<ObjectResult> RetryMulti(string id) {
|
||||
return ResolveResult(await this.RandomizeService.RetryMulti(id));
|
||||
}
|
||||
|
||||
@@ -85,7 +117,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
private ActionResult ResolveResult(IDictionary<string, object> result) {
|
||||
private ObjectResult ResolveResult(IDictionary<string, object> result) {
|
||||
if (result.TryGetValue("status", out var responseCode)) {
|
||||
if (responseCode is int code) {
|
||||
return StatusCode(code, result);
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
namespace ALttPRandomizer.Serialization {
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ALttPRandomizer.Model;
|
||||
|
||||
public class RandomizableWeightsJsonConverter : JsonConverterFactory {
|
||||
public override bool CanConvert(Type typeToConvert) {
|
||||
if (!typeToConvert.IsGenericType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeToConvert.GetGenericTypeDefinition() != typeof(RandomizableWeights<>)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) {
|
||||
Type itemType = typeToConvert.GetGenericArguments()[0];
|
||||
Type innerType = typeof(RandomizableWeightsConverterInner<>).MakeGenericType(itemType);
|
||||
|
||||
return (JsonConverter) Activator.CreateInstance(innerType, args: options)!;
|
||||
}
|
||||
|
||||
private class RandomizableWeightsConverterInner<T> : JsonConverter<RandomizableWeights<T>> where T : struct, Enum {
|
||||
private readonly JsonConverter<IDictionary<T, int>> dictionaryConverter;
|
||||
private readonly JsonConverter<T> valueConverter;
|
||||
private readonly Type valueType;
|
||||
private readonly Type dictionaryType;
|
||||
|
||||
public RandomizableWeightsConverterInner(JsonSerializerOptions options) {
|
||||
dictionaryConverter = (JsonConverter<IDictionary<T, int>>) options.GetConverter(typeof(IDictionary<T, int>));
|
||||
valueConverter = (JsonConverter<T>) options.GetConverter(typeof(T));
|
||||
|
||||
this.valueType = typeof(T);
|
||||
this.dictionaryType = typeof(IDictionary<T, int>);
|
||||
}
|
||||
|
||||
public override RandomizableWeights<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
|
||||
switch (reader.TokenType) {
|
||||
case JsonTokenType.Null:
|
||||
return RandomizableWeights<T>.EmptyWeights;
|
||||
case JsonTokenType.String:
|
||||
case JsonTokenType.Number:
|
||||
var value = valueConverter.Read(ref reader, this.valueType, options);
|
||||
return RandomizableWeights<T>.ConstantWeights(value);
|
||||
case JsonTokenType.StartObject:
|
||||
var dict = dictionaryConverter.Read(ref reader, this.dictionaryType, options);
|
||||
if (dict == null) {
|
||||
return RandomizableWeights<T>.EmptyWeights;
|
||||
} else {
|
||||
return RandomizableWeights<T>.FromDictionary(dict);
|
||||
}
|
||||
default:
|
||||
throw new JsonException();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, RandomizableWeights<T> value, JsonSerializerOptions options) {
|
||||
switch (value.Options.Count) {
|
||||
case 0:
|
||||
writer.WriteNullValue();
|
||||
break;
|
||||
case 1:
|
||||
this.valueConverter.Write(writer, value.Options[0].Item, options);
|
||||
break;
|
||||
default:
|
||||
writer.WriteStartObject();
|
||||
foreach (var (item, weight) in value.Options) {
|
||||
this.valueConverter.WriteAsPropertyName(writer, item, options);
|
||||
writer.WriteNumberValue(weight);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
namespace ALttPRandomizer.Serialization {
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ALttPRandomizer.Model;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Core.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
public class RandomizableWeightsYamlConverters : IYamlTypeConverter {
|
||||
private readonly Dictionary<Type, IYamlTypeConverter> converters = new();
|
||||
|
||||
public bool Accepts(Type type) {
|
||||
if (this.converters.ContainsKey(type)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!type.IsGenericType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (type.GetGenericTypeDefinition() != typeof(RandomizableWeights<>)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Type itemType = type.GetGenericArguments()[0];
|
||||
Type innerType = typeof(RandomizableWeightsYamlConverter<>).MakeGenericType(itemType)!;
|
||||
|
||||
this.converters[type] = (IYamlTypeConverter) Activator.CreateInstance(innerType)!;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) {
|
||||
return this.converters[type].ReadYaml(parser, type, rootDeserializer);
|
||||
}
|
||||
|
||||
public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) {
|
||||
this.converters[type].WriteYaml(emitter, value, type, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
internal class RandomizableWeightsYamlConverter<T> : IYamlTypeConverter where T : struct, Enum {
|
||||
private readonly Type valueType;
|
||||
private readonly Type dictionaryType;
|
||||
|
||||
public RandomizableWeightsYamlConverter() {
|
||||
this.valueType = typeof(T);
|
||||
this.dictionaryType = typeof(IDictionary<T, int>);
|
||||
}
|
||||
|
||||
public bool Accepts(Type type) {
|
||||
return type == typeof(RandomizableWeights<T>);
|
||||
}
|
||||
|
||||
public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) {
|
||||
if (parser.Accept<Scalar>(out var evt)) {
|
||||
if (evt.Value == "null") {
|
||||
parser.Consume<Scalar>();
|
||||
return RandomizableWeights<T>.EmptyWeights;
|
||||
}
|
||||
var value = rootDeserializer.Invoke(this.valueType);
|
||||
if (value is T t) {
|
||||
return RandomizableWeights<T>.ConstantWeights(t);
|
||||
} else {
|
||||
return RandomizableWeights<T>.EmptyWeights;
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.Accept<MappingStart>(out _)) {
|
||||
var value = rootDeserializer.Invoke(this.dictionaryType);
|
||||
if (value is IDictionary<T, int> dict) {
|
||||
return RandomizableWeights<T>.FromDictionary(dict);
|
||||
} else {
|
||||
return RandomizableWeights<T>.EmptyWeights;
|
||||
}
|
||||
}
|
||||
|
||||
throw new YamlException($"Error reading type RandomizableWeights<{typeof(T).Name}>.");
|
||||
}
|
||||
|
||||
public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) {
|
||||
if (value is RandomizableWeights<T> weights) {
|
||||
switch (weights.Options.Count) {
|
||||
case 0:
|
||||
emitter.Emit(new Scalar("null"));
|
||||
break;
|
||||
case 1:
|
||||
serializer.Invoke(weights.Options[0].Item, valueType);
|
||||
break;
|
||||
default:
|
||||
emitter.Emit(new MappingStart());
|
||||
foreach (var (item, weight) in weights.Options) {
|
||||
serializer.Invoke(item, valueType);
|
||||
emitter.Emit(new Scalar(weight.ToString()));
|
||||
}
|
||||
emitter.Emit(new MappingEnd());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
emitter.Emit(new MappingStart());
|
||||
emitter.Emit(new MappingEnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
ALttPRandomizer/Serialization/YamlInputFormatter.cs
Normal file
40
ALttPRandomizer/Serialization/YamlInputFormatter.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
namespace ALttPRandomizer.Serialization {
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
public class YamlInputFormatter : TextInputFormatter {
|
||||
private IDeserializer Deserializer { get; }
|
||||
private ILogger<YamlInputFormatter> Logger { get; }
|
||||
|
||||
public YamlInputFormatter(IDeserializer deserializer, ILogger<YamlInputFormatter> logger) {
|
||||
this.Deserializer = deserializer;
|
||||
this.Logger = logger;
|
||||
|
||||
this.SupportedEncodings.Add(Encoding.UTF8);
|
||||
this.SupportedEncodings.Add(Encoding.Unicode);
|
||||
this.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/yaml"));
|
||||
this.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/yaml"));
|
||||
}
|
||||
|
||||
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) {
|
||||
var request = context.HttpContext.Request;
|
||||
|
||||
using var reader = context.ReaderFactory(request.Body, encoding);
|
||||
var type = context.ModelType;
|
||||
|
||||
try {
|
||||
var content = await reader.ReadToEndAsync();
|
||||
var model = this.Deserializer.Deserialize(content, type);
|
||||
return await InputFormatterResult.SuccessAsync(model);
|
||||
} catch (Exception ex) {
|
||||
this.Logger.LogInformation(ex, "Error parsing YAML");
|
||||
return await InputFormatterResult.FailureAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
ALttPRandomizer/Serialization/YamlStringEnumConverter.cs
Normal file
72
ALttPRandomizer/Serialization/YamlStringEnumConverter.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
namespace ALttPRandomizer.Serialization {
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Core.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
public class YamlStringEnumConverter : IYamlTypeConverter {
|
||||
private readonly Dictionary<Type, Dictionary<string, object>> deserializationMap;
|
||||
private readonly Dictionary<Type, Dictionary<object, string>> serializationMap;
|
||||
|
||||
public YamlStringEnumConverter() {
|
||||
this.deserializationMap = new();
|
||||
this.serializationMap = new();
|
||||
}
|
||||
|
||||
public bool Accepts(Type type) => type.IsEnum;
|
||||
|
||||
private void RegisterType(Type type) {
|
||||
if (this.serializationMap.ContainsKey(type) && this.deserializationMap.ContainsKey(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.deserializationMap[type] = new();
|
||||
this.serializationMap[type] = new();
|
||||
|
||||
var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
|
||||
|
||||
foreach (var field in fields) {
|
||||
var alias = UnderscoredNamingConvention.Instance.Apply(field.Name);
|
||||
var value = Enum.Parse(type, field.Name);
|
||||
var att = field.GetCustomAttribute<JsonStringEnumMemberNameAttribute>();
|
||||
if (att != null) {
|
||||
alias = att.Name;
|
||||
}
|
||||
|
||||
this.deserializationMap[type][alias] = value;
|
||||
this.serializationMap[type][value] = alias;
|
||||
}
|
||||
}
|
||||
|
||||
public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) {
|
||||
this.RegisterType(type);
|
||||
|
||||
var stringValue = parser.Consume<Scalar>().Value;
|
||||
|
||||
if (this.deserializationMap[type].TryGetValue(stringValue, out var value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new YamlException($"Invalid value \"{stringValue}\" for type {type}");
|
||||
}
|
||||
|
||||
public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) {
|
||||
this.RegisterType(type);
|
||||
|
||||
if (value is null) {
|
||||
throw new YamlException($"Unexpected null value of type {type}");
|
||||
}
|
||||
|
||||
if (this.serializationMap[type].TryGetValue(value, out var stringValue)) {
|
||||
emitter.Emit(new Scalar(stringValue));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new YamlException($"Invalid value \"{value}\" for type {type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,17 +72,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
internal GeneratorSettingsAttribute GetGeneratorSettings(RandomizerInstance randomizer)
|
||||
{
|
||||
internal GeneratorSettingsAttribute GetGeneratorSettings(RandomizerInstance randomizer) {
|
||||
var fi = typeof(RandomizerInstance).GetField(randomizer.ToString(), BindingFlags.Static | BindingFlags.Public);
|
||||
|
||||
var settings = fi?.GetCustomAttribute<GeneratorSettingsAttribute>();
|
||||
|
||||
if (settings == null) {
|
||||
throw new InvalidSettingsException("Invalid randomizer: {0}", randomizer);
|
||||
}
|
||||
|
||||
return settings;
|
||||
return settings ?? throw new InvalidSettingsException("Invalid randomizer: {0}", randomizer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Submodule BetaRandomizer updated: d2a5bb56d8...cd5bc9a206
Reference in New Issue
Block a user