From bceff701bf69e0512e797fd04288c954f5cde3af Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Sun, 24 May 2026 02:55:03 -0500 Subject: [PATCH] Preparation fo Mystery rolling --- ALttPRandomizer/ALttPRandomizer.csproj | 19 +-- ALttPRandomizer/Azure/AzureStorage.cs | 3 +- ALttPRandomizer/JsonOptions.cs | 2 + ALttPRandomizer/Model/MysterySettings.cs | 153 ++++++++++++++++++ ALttPRandomizer/Model/RandomizableWeights.cs | 80 +++++++++ ALttPRandomizer/Model/SeedSettings.cs | 4 +- ALttPRandomizer/Program.cs | 29 +++- .../Randomizers/MysteryRandomizer.cs | 30 ++++ ALttPRandomizer/SeedController.cs | 48 +++++- .../RandomizableWeightsJsonConverter.cs | 82 ++++++++++ .../RandomizableWeightsYamlConverter.cs | 105 ++++++++++++ .../Serialization/YamlInputFormatter.cs | 40 +++++ .../Serialization/YamlStringEnumConverter.cs | 72 +++++++++ .../Settings/CommonSettingsProcessor.cs | 9 +- BetaRandomizer | 2 +- 15 files changed, 646 insertions(+), 32 deletions(-) create mode 100644 ALttPRandomizer/Model/MysterySettings.cs create mode 100644 ALttPRandomizer/Model/RandomizableWeights.cs create mode 100644 ALttPRandomizer/Randomizers/MysteryRandomizer.cs create mode 100644 ALttPRandomizer/Serialization/RandomizableWeightsJsonConverter.cs create mode 100644 ALttPRandomizer/Serialization/RandomizableWeightsYamlConverter.cs create mode 100644 ALttPRandomizer/Serialization/YamlInputFormatter.cs create mode 100644 ALttPRandomizer/Serialization/YamlStringEnumConverter.cs diff --git a/ALttPRandomizer/ALttPRandomizer.csproj b/ALttPRandomizer/ALttPRandomizer.csproj index c76c1ea..640c1e7 100644 --- a/ALttPRandomizer/ALttPRandomizer.csproj +++ b/ALttPRandomizer/ALttPRandomizer.csproj @@ -9,15 +9,16 @@ - - - - - - - - - + + + + + + + + + + diff --git a/ALttPRandomizer/Azure/AzureStorage.cs b/ALttPRandomizer/Azure/AzureStorage.cs index 16ff340..459fb7c 100644 --- a/ALttPRandomizer/Azure/AzureStorage.cs +++ b/ALttPRandomizer/Azure/AzureStorage.cs @@ -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> 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(); diff --git a/ALttPRandomizer/JsonOptions.cs b/ALttPRandomizer/JsonOptions.cs index d7dc7c4..e44f394 100644 --- a/ALttPRandomizer/JsonOptions.cs +++ b/ALttPRandomizer/JsonOptions.cs @@ -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; } } diff --git a/ALttPRandomizer/Model/MysterySettings.cs b/ALttPRandomizer/Model/MysterySettings.cs new file mode 100644 index 0000000..ca88214 --- /dev/null +++ b/ALttPRandomizer/Model/MysterySettings.cs @@ -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 { get; set; } = new(); + + public RandomizableWeights Weapons { get; set; } = new(); + + public RandomizableWeights Goal { get; set; } = new(); + + public RandomizableWeights CrystalsGanon { get; set; } = new(); + + public RandomizableWeights BossesGanon { get; set; } = new(); + + public RandomizableWeights TriforcePieces { get; set; } = new(); + + [YamlMember(Alias = "crystals_gt")] + public RandomizableWeights CrystalsGT { get; set; } = new(); + + public RandomizableWeights GanonItem { get; set; } = new(); + + public RandomizableWeights EntranceShuffle { get; set; } = new(); + + public RandomizableWeights OverworldMapDungeons { get; set; } = new(); + + public RandomizableWeights LinksHouse { get; set; } = new(); + + public RandomizableWeights SkullWoods { get; set; } = new(); + + public RandomizableWeights LinkedDrops { get; set; } = new(); + + public RandomizableWeights BossShuffle { get; set; } = new(); + + public RandomizableWeights EnemyShuffle { get; set; } = new(); + + public RandomizableWeights DamageTableShuffle { get; set; } = new(); + + public RandomizableWeights SmallKeys { get; set; } = new(); + + public RandomizableWeights BigKeys { get; set; } = new(); + + public RandomizableWeights Maps { get; set; } = new(); + + public RandomizableWeights Compasses { get; set; } = new(); + + public RandomizableWeights ShowLoot { get; set; } = new(); + + public RandomizableWeights ShowLootHud { get; set; } = new(); + + public RandomizableWeights ShowMap { get; set; } = new(); + + public RandomizableWeights ShopShuffle { get; set; } = new(); + + public RandomizableWeights DropShuffle { get; set; } = new(); + + public RandomizableWeights PotShuffle { get; set; } = new(); + + public RandomizableWeights PrizeShuffle { get; set; } = new(); + + public RandomizableWeights Boots { get; set; } = new(); + + public RandomizableWeights Flute { get; set; } = new(); + + public RandomizableWeights DarkRooms { get; set; } = new(); + + public RandomizableWeights Bombs { get; set; } = new(); + + public RandomizableWeights Book { get; set; } = new(); + + public RandomizableWeights Mirror { get; set; } = new(); + + public RandomizableWeights DoorShuffle { get; set; } = new(); + + public RandomizableWeights Lobbies { get; set; } = new(); + + public RandomizableWeights DoorTypeMode { get; set; } = new(); + + public RandomizableWeights TrapDoorMode { get; set; } = new(); + + public RandomizableWeights ExtraKeys { get; set; } = new(); + + public RandomizableWeights FollowerShuffle { get; set; } = new(); + + public RandomizableWeights FluteShuffle { get; set; } = new(); + + public RandomizableWeights OverworldLayout { get; set; } = new(); + + public RandomizableWeights OverworldWorldLayouts { get; set; } = new(); + + public RandomizableWeights OverworldLayoutTerrain { get; set; } = new(); + + public RandomizableWeights OverworldLayoutEdges { get; set; } = new(); + + public RandomizableWeights OverworldMapFog { get; set; } = new(); + + public RandomizableWeights TileSwap { get; set; } = new(); + + public RandomizableWeights DamageChallenge { get; set; } = new(); + + public RandomizableWeights 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; + } + } + } + } + } + } +} diff --git a/ALttPRandomizer/Model/RandomizableWeights.cs b/ALttPRandomizer/Model/RandomizableWeights.cs new file mode 100644 index 0000000..5b746d8 --- /dev/null +++ b/ALttPRandomizer/Model/RandomizableWeights.cs @@ -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 : 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 EmptyWeights => new(); + + public static RandomizableWeights EqualAll { + get => RandomizableWeights.FromDictionary(Enum.GetValues().ToDictionary(v => v, v => 1)); + } + + public static RandomizableWeights ConstantWeights(T value) { + return new(new List<(T, int)> { (value, 100) }); + } + + public static RandomizableWeights FromDictionary(IDictionary 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(); + } +} diff --git a/ALttPRandomizer/Model/SeedSettings.cs b/ALttPRandomizer/Model/SeedSettings.cs index 0bf4a60..30b11cb 100644 --- a/ALttPRandomizer/Model/SeedSettings.cs +++ b/ALttPRandomizer/Model/SeedSettings.cs @@ -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, } diff --git a/ALttPRandomizer/Program.cs b/ALttPRandomizer/Program.cs index df1413d..31d4548 100644 --- a/ALttPRandomizer/Program.cs +++ b/ALttPRandomizer/Program.cs @@ -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>()); + + 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(); + builder.Services.AddSwaggerGen(options => { + options.OperationFilter(); + }); var options = new DefaultAzureCredentialOptions(); @@ -72,6 +92,7 @@ builder.Services.AddSingleton(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/ALttPRandomizer/Randomizers/MysteryRandomizer.cs b/ALttPRandomizer/Randomizers/MysteryRandomizer.cs new file mode 100644 index 0000000..2fc1c69 --- /dev/null +++ b/ALttPRandomizer/Randomizers/MysteryRandomizer.cs @@ -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; + } + } +} diff --git a/ALttPRandomizer/SeedController.cs b/ALttPRandomizer/SeedController.cs index 79fbc1d..b92cb75 100644 --- a/ALttPRandomizer/SeedController.cs +++ b/ALttPRandomizer/SeedController.cs @@ -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 logger) { + public SeedController( + RandomizeService randomizeService, + MysteryRandomizer mysteryRandomizer, + SeedService seedService, + ILogger 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 Logger { get; } + private ISerializer YamlSerializer { get; } [Route("/generate")] [HttpPost] - public async Task Generate([FromBody] SeedSettings settings) { + public async Task 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 GenerateMultiworld([FromBody] IList settings) { + public async Task GenerateMultiworld([FromBody] IList settings) { if (!ModelState.IsValid) { return BadRequest(ModelState); } @@ -50,25 +82,25 @@ [Route("/seed/{id}")] [HttpGet] - public async Task GetSeed(string id) { + public async Task GetSeed(string id) { return ResolveResult(await this.SeedService.GetSeed(id)); } [Route("/seed/{id}")] [HttpPost] - public async Task RetrySeed(string id) { + public async Task RetrySeed(string id) { return ResolveResult(await this.RandomizeService.RetrySeed(id)); } [Route("/multi/{id}")] [HttpGet] - public async Task GetMulti(string id) { + public async Task GetMulti(string id) { return ResolveResult(await this.SeedService.GetMulti(id)); } [Route("/multi/{id}")] [HttpPost] - public async Task RetryMulti(string id) { + public async Task RetryMulti(string id) { return ResolveResult(await this.RandomizeService.RetryMulti(id)); } @@ -85,7 +117,7 @@ } } - private ActionResult ResolveResult(IDictionary result) { + private ObjectResult ResolveResult(IDictionary result) { if (result.TryGetValue("status", out var responseCode)) { if (responseCode is int code) { return StatusCode(code, result); diff --git a/ALttPRandomizer/Serialization/RandomizableWeightsJsonConverter.cs b/ALttPRandomizer/Serialization/RandomizableWeightsJsonConverter.cs new file mode 100644 index 0000000..976c398 --- /dev/null +++ b/ALttPRandomizer/Serialization/RandomizableWeightsJsonConverter.cs @@ -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 : JsonConverter> where T : struct, Enum { + private readonly JsonConverter> dictionaryConverter; + private readonly JsonConverter valueConverter; + private readonly Type valueType; + private readonly Type dictionaryType; + + public RandomizableWeightsConverterInner(JsonSerializerOptions options) { + dictionaryConverter = (JsonConverter>) options.GetConverter(typeof(IDictionary)); + valueConverter = (JsonConverter) options.GetConverter(typeof(T)); + + this.valueType = typeof(T); + this.dictionaryType = typeof(IDictionary); + } + + public override RandomizableWeights? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + switch (reader.TokenType) { + case JsonTokenType.Null: + return RandomizableWeights.EmptyWeights; + case JsonTokenType.String: + case JsonTokenType.Number: + var value = valueConverter.Read(ref reader, this.valueType, options); + return RandomizableWeights.ConstantWeights(value); + case JsonTokenType.StartObject: + var dict = dictionaryConverter.Read(ref reader, this.dictionaryType, options); + if (dict == null) { + return RandomizableWeights.EmptyWeights; + } else { + return RandomizableWeights.FromDictionary(dict); + } + default: + throw new JsonException(); + } + } + + public override void Write(Utf8JsonWriter writer, RandomizableWeights 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; + } + } + } + } +} diff --git a/ALttPRandomizer/Serialization/RandomizableWeightsYamlConverter.cs b/ALttPRandomizer/Serialization/RandomizableWeightsYamlConverter.cs new file mode 100644 index 0000000..7a730b1 --- /dev/null +++ b/ALttPRandomizer/Serialization/RandomizableWeightsYamlConverter.cs @@ -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 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 : IYamlTypeConverter where T : struct, Enum { + private readonly Type valueType; + private readonly Type dictionaryType; + + public RandomizableWeightsYamlConverter() { + this.valueType = typeof(T); + this.dictionaryType = typeof(IDictionary); + } + + public bool Accepts(Type type) { + return type == typeof(RandomizableWeights); + } + + public object? ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeserializer) { + if (parser.Accept(out var evt)) { + if (evt.Value == "null") { + parser.Consume(); + return RandomizableWeights.EmptyWeights; + } + var value = rootDeserializer.Invoke(this.valueType); + if (value is T t) { + return RandomizableWeights.ConstantWeights(t); + } else { + return RandomizableWeights.EmptyWeights; + } + } + + if (parser.Accept(out _)) { + var value = rootDeserializer.Invoke(this.dictionaryType); + if (value is IDictionary dict) { + return RandomizableWeights.FromDictionary(dict); + } else { + return RandomizableWeights.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 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()); + } + } + } +} diff --git a/ALttPRandomizer/Serialization/YamlInputFormatter.cs b/ALttPRandomizer/Serialization/YamlInputFormatter.cs new file mode 100644 index 0000000..a1cc876 --- /dev/null +++ b/ALttPRandomizer/Serialization/YamlInputFormatter.cs @@ -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 Logger { get; } + + public YamlInputFormatter(IDeserializer deserializer, ILogger 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 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(); + } + } + } +} diff --git a/ALttPRandomizer/Serialization/YamlStringEnumConverter.cs b/ALttPRandomizer/Serialization/YamlStringEnumConverter.cs new file mode 100644 index 0000000..8316a7f --- /dev/null +++ b/ALttPRandomizer/Serialization/YamlStringEnumConverter.cs @@ -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> deserializationMap; + private readonly Dictionary> 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(); + 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().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}"); + } + } +} diff --git a/ALttPRandomizer/Settings/CommonSettingsProcessor.cs b/ALttPRandomizer/Settings/CommonSettingsProcessor.cs index cbf348f..1d58773 100644 --- a/ALttPRandomizer/Settings/CommonSettingsProcessor.cs +++ b/ALttPRandomizer/Settings/CommonSettingsProcessor.cs @@ -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(); - if (settings == null) { - throw new InvalidSettingsException("Invalid randomizer: {0}", randomizer); - } - - return settings; + return settings ?? throw new InvalidSettingsException("Invalid randomizer: {0}", randomizer); } } diff --git a/BetaRandomizer b/BetaRandomizer index d2a5bb5..cd5bc9a 160000 --- a/BetaRandomizer +++ b/BetaRandomizer @@ -1 +1 @@ -Subproject commit d2a5bb56d8dcc89064e3ef356925d0a9bc6a2584 +Subproject commit cd5bc9a2061694513d327484d2cf07971a00116e