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