diff --git a/.dockerignore b/.dockerignore index 39f8c9f..ba29cf6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,4 +11,4 @@ ALttPRandomizer/[Bb]in */DR_* */ER_* */OR_* -*/data/base2current.json +BaseRandomizer/data/base2current.json diff --git a/.gitmodules b/.gitmodules index 821fa2e..ce65ba4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "BaseRandomizer"] path = BaseRandomizer url = https://github.com/ardnaxelarak/ALttPDoorRandomizer +[submodule "Apr2025Randomizer"] + path = Apr2025Randomizer + url = https://github.com/ardnaxelarak/ALttPDoorRandomizer diff --git a/ALttPRandomizer/Model/SeedSettings.cs b/ALttPRandomizer/Model/SeedSettings.cs index 78de692..8220d83 100644 --- a/ALttPRandomizer/Model/SeedSettings.cs +++ b/ALttPRandomizer/Model/SeedSettings.cs @@ -1,12 +1,18 @@ namespace ALttPRandomizer.Model { + using ALttPRandomizer.Randomizers; using ALttPRandomizer.Settings; - using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; + using static ALttPRandomizer.Model.RandomizerInstance; + public class SeedSettings { + [NoSettingName] + public RandomizerInstance Randomizer { get; set; } = RandomizerInstance.Base; + [NoSettingName] public RaceMode Race { get; set; } = RaceMode.Normal; + [ForbiddenSetting([Apr2025], Mode.Inverted)] public Mode Mode { get; set; } = Mode.Open; [SettingName("swords")] @@ -19,44 +25,75 @@ [SettingName("crystals_gt")] [JsonPropertyName("crystals_gt")] + [NoSettingName([Apr2025])] public EntryRequirement CrystalsGT { get; set; } = EntryRequirement.Crystals7; [SettingName("shuffle")] + [ForbiddenSetting([Apr2025], EntranceShuffle.Swapped)] public EntranceShuffle EntranceShuffle { get; set; } = EntranceShuffle.Vanilla; + [SettingName("skullwoods")] + [RequiredSetting([Apr2025], SkullWoodsShuffle.Original)] + [NoSettingName([Apr2025])] public SkullWoodsShuffle SkullWoods { get; set; } = SkullWoodsShuffle.Original; + [SettingName("linked_drops")] + [RequiredSetting([Apr2025], LinkedDrops.Unset)] + [NoSettingName([Apr2025])] public LinkedDrops LinkedDrops { get; set; } = LinkedDrops.Unset; [SettingName("shufflebosses")] + [RequiredSetting([Apr2025], BossShuffle.Vanilla)] + [NoSettingName([Apr2025])] public BossShuffle BossShuffle { get; set; } = BossShuffle.Vanilla; [SettingName("shuffleenemies")] + [RequiredSetting([Apr2025], EnemyShuffle.Vanilla)] + [NoSettingName([Apr2025])] public EnemyShuffle EnemyShuffle { get; set; } = EnemyShuffle.Vanilla; [SettingName("keyshuffle")] - public DungeonItemLocations SmallKeys { get; set; } = DungeonItemLocations.Dungeon; + [RequiredSetting([Apr2025], KeyLocations.Dungeon, KeyLocations.Wild)] + [NoSettingName([Apr2025])] + public KeyLocations SmallKeys { get; set; } = KeyLocations.Dungeon; [SettingName("bigkeyshuffle")] - [DeniedValues(DungeonItemLocations.Universal)] + [RequiredSetting([Apr2025], DungeonItemLocations.Dungeon)] + [NoSettingName([Apr2025])] public DungeonItemLocations BigKeys { get; set; } = DungeonItemLocations.Dungeon; [SettingName("mapshuffle")] - [DeniedValues(DungeonItemLocations.Universal)] + [RequiredSetting([Apr2025], DungeonItemLocations.Dungeon)] + [NoSettingName([Apr2025])] public DungeonItemLocations Maps { get; set; } = DungeonItemLocations.Dungeon; [SettingName("compassshuffle")] - [DeniedValues(DungeonItemLocations.Universal)] + [RequiredSetting([Apr2025], DungeonItemLocations.Dungeon)] + [NoSettingName([Apr2025])] public DungeonItemLocations Compasses { get; set; } = DungeonItemLocations.Dungeon; [NoSettingName] + [RequiredSetting([Apr2025], ShopShuffle.Vanilla)] public ShopShuffle ShopShuffle { get; set; } = ShopShuffle.Vanilla; + + [RequiredSetting([Apr2025], DropShuffle.Vanilla)] + [NoSettingName([Apr2025])] public DropShuffle DropShuffle { get; set; } = DropShuffle.Vanilla; + + [RequiredSetting([Apr2025], Pottery.Vanilla)] + [NoSettingName([Apr2025])] public Pottery Pottery { get; set; } = Pottery.Vanilla; + [RequiredSetting([Apr2025], PrizeShuffle.Vanilla)] + [NoSettingName([Apr2025])] public PrizeShuffle PrizeShuffle { get; set; } = PrizeShuffle.Vanilla; } + public enum RandomizerInstance { + [RandomizerName(BaseRandomizer.Name)] Base, + [RandomizerName(Apr2025Randomizer.Name)] Apr2025, + } + public enum RaceMode { Normal, [AdditionalSetting("--securerandom")] Race, @@ -132,11 +169,17 @@ Mimics, } + public enum KeyLocations { + [SettingName("none")] Dungeon, + [AdditionalSetting([Apr2025], "--keysanity")] Wild, + Nearby, + Universal, + } + public enum DungeonItemLocations { [SettingName("none")] Dungeon, Wild, Nearby, - Universal, } public enum ShopShuffle { @@ -168,5 +211,4 @@ Nearby, Wild, } - } diff --git a/ALttPRandomizer/Options/ServiceOptions.cs b/ALttPRandomizer/Options/ServiceOptions.cs index 7d39740..2e72ee3 100644 --- a/ALttPRandomizer/Options/ServiceOptions.cs +++ b/ALttPRandomizer/Options/ServiceOptions.cs @@ -5,9 +5,10 @@ public class ServiceOptions { public string Baserom { get; set; } = null!; public string PythonPath { get; set; } = null!; - public string RandomizerPath { get; set; } = null!; + public string FlipsPath { get; set; } = null!; public IList AllowedCors { get; set; } = new List(); public AzureSettings AzureSettings { get; set; } = new AzureSettings(); + public IDictionary RandomizerPaths { get; set; } = new Dictionary(); } public class AzureSettings { diff --git a/ALttPRandomizer/Program.cs b/ALttPRandomizer/Program.cs index 0b5bc93..b23d757 100644 --- a/ALttPRandomizer/Program.cs +++ b/ALttPRandomizer/Program.cs @@ -1,7 +1,7 @@ -namespace ALttPRandomizer -{ +namespace ALttPRandomizer { using ALttPRandomizer.Azure; using ALttPRandomizer.Options; + using ALttPRandomizer.Randomizers; using ALttPRandomizer.Service; using ALttPRandomizer.Settings; using global::Azure.Identity; @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Serilog; + using System; internal class Program { @@ -60,9 +61,13 @@ var seedClient = new BlobContainerClient(settings.AzureSettings.BlobstoreEndpoint, token); builder.Services.AddSingleton(seedClient); + builder.Services.AddSingleton(sp => sp); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddScoped(); + + builder.Services.AddKeyedScoped(BaseRandomizer.Name); + builder.Services.AddKeyedScoped(Apr2025Randomizer.Name); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/ALttPRandomizer/Randomizers/Apr2025Randomizer.cs b/ALttPRandomizer/Randomizers/Apr2025Randomizer.cs new file mode 100644 index 0000000..376a564 --- /dev/null +++ b/ALttPRandomizer/Randomizers/Apr2025Randomizer.cs @@ -0,0 +1,159 @@ +namespace ALttPRandomizer.Randomizers { + using ALttPRandomizer; + using ALttPRandomizer.Azure; + using ALttPRandomizer.Model; + using ALttPRandomizer.Options; + using ALttPRandomizer.Settings; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using System; + using System.Diagnostics; + using System.IO; + using System.Text.Json; + using System.Threading.Tasks; + + public class Apr2025Randomizer : IRandomizer { + public const string Name = "apr2025"; + public const RandomizerInstance Instance = RandomizerInstance.Apr2025; + + public Apr2025Randomizer( + AzureStorage azureStorage, + CommonSettingsProcessor settingsProcessor, + IOptionsMonitor optionsMonitor, + ILogger logger) { + AzureStorage = azureStorage; + SettingsProcessor = settingsProcessor; + OptionsMonitor = optionsMonitor; + Logger = logger; + } + + private CommonSettingsProcessor SettingsProcessor { get; } + private AzureStorage AzureStorage { get; } + private IOptionsMonitor OptionsMonitor { get; } + private ILogger Logger { get; } + private ServiceOptions Configuration => OptionsMonitor.CurrentValue; + + public void Validate(SeedSettings settings) { + this.SettingsProcessor.ValidateSettings(Instance, settings); + } + + public async Task Randomize(string id, SeedSettings settings) { + Logger.LogDebug("Recieved request for id {id} to randomize settings {@settings}", id, settings); + + var start = new ProcessStartInfo() { + FileName = Configuration.PythonPath, + WorkingDirectory = Configuration.RandomizerPaths[Name], + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + + var args = start.ArgumentList; + args.Add("EntranceRandomizer.py"); + args.Add("--rom"); + args.Add(Configuration.Baserom); + + args.Add("--outputpath"); + args.Add(Path.GetTempPath()); + + args.Add("--outputname"); + args.Add(id); + + args.Add("--json_spoiler"); + + args.Add("--quickswap"); + + foreach (var arg in SettingsProcessor.GetSettings(Instance, settings)) { + args.Add(arg); + } + + Logger.LogInformation("Randomizing with args: {args}", string.Join(" ", args)); + + var generating = string.Format("{0}/generating", id); + await AzureStorage.UploadFile(generating, BinaryData.Empty); + + var process = Process.Start(start) ?? throw new GenerationFailedException("Process failed to start."); + process.EnableRaisingEvents = true; + + process.OutputDataReceived += (_, args) => Logger.LogInformation("Randomizer STDOUT: {output}", args.Data); + process.ErrorDataReceived += (_, args) => Logger.LogInformation("Randomizer STDERR: {output}", args.Data); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + process.Exited += async (sender, args) => { + var exitcode = process.ExitCode; + + if (exitcode != 0) { + await GenerationFailed(id, exitcode); + } else { + await GenerationSucceeded(id, settings); + } + }; + + var settingsJson = JsonSerializer.SerializeToDocument(settings, JsonOptions.Default); + var settingsOut = string.Format("{0}/settings.json", id); + await AzureStorage.UploadFile(settingsOut, new BinaryData(settingsJson)); + } + + private async Task GenerationSucceeded(string id, SeedSettings settings) { + var rom = Path.Join(Path.GetTempPath(), string.Format("ER_{0}.sfc", id)); + + var spoilerIn = Path.Join(Path.GetTempPath(), string.Format("ER_{0}_Spoiler.json", id)); + var spoilerOut = string.Format("{0}/spoiler.json", id); + var uploadSpoiler = AzureStorage.UploadFileAndDelete(spoilerOut, spoilerIn); + + var metaIn = Path.Join(Path.GetTempPath(), string.Format("ER_{0}_Meta.json", id)); + var metaOut = string.Format("{0}/meta.json", id); + var uploadMeta = AzureStorage.UploadFileAndDelete(metaOut, metaIn); + + var bpsIn = Path.Join(Path.GetTempPath(), string.Format("ER_{0}.bps", id)); + + var flips = new ProcessStartInfo() { + FileName = Configuration.FlipsPath, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + var args = flips.ArgumentList; + args.Add("--create"); + args.Add(Configuration.Baserom); + args.Add(rom); + args.Add(bpsIn); + + var process = Process.Start(flips) ?? throw new GenerationFailedException("Process failed to start."); + process.EnableRaisingEvents = true; + + process.OutputDataReceived += (_, args) => Logger.LogInformation("flips STDOUT: {output}", args.Data); + process.ErrorDataReceived += (_, args) => Logger.LogInformation("flips STDERR: {output}", args.Data); + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + + await process.WaitForExitAsync(); + + if (process.ExitCode != 0) { + await this.GenerationFailed(id, process.ExitCode); + return; + } + + var bpsOut = string.Format("{0}/patch.bps", id); + var uploadPatch = AzureStorage.UploadFileAndDelete(bpsOut, bpsIn); + + var generating = string.Format("{0}/generating", id); + var deleteGenerating = AzureStorage.DeleteFile(generating); + + await Task.WhenAll(uploadPatch, uploadSpoiler, uploadMeta, deleteGenerating); + + Logger.LogDebug("Deleting file {filepath}", rom); + File.Delete(rom); + + Logger.LogInformation("Finished uploading seed id {id}", id); + } + + private async Task GenerationFailed(string id, int exitcode) { + var generating = string.Format("{0}/generating", id); + var deleteGenerating = AzureStorage.DeleteFile(generating); + + await Task.WhenAll(deleteGenerating); + } + } +} diff --git a/ALttPRandomizer/Randomizer.cs b/ALttPRandomizer/Randomizers/BaseRandomizer.cs similarity index 64% rename from ALttPRandomizer/Randomizer.cs rename to ALttPRandomizer/Randomizers/BaseRandomizer.cs index 67bcf3e..4d70775 100644 --- a/ALttPRandomizer/Randomizer.cs +++ b/ALttPRandomizer/Randomizers/BaseRandomizer.cs @@ -1,4 +1,5 @@ -namespace ALttPRandomizer { +namespace ALttPRandomizer.Randomizers { + using ALttPRandomizer; using ALttPRandomizer.Azure; using ALttPRandomizer.Model; using ALttPRandomizer.Options; @@ -12,30 +13,37 @@ using System.Text.Json; using System.Threading.Tasks; - public class Randomizer { - public Randomizer( + public class BaseRandomizer : IRandomizer { + public const string Name = "base"; + public const RandomizerInstance Instance = RandomizerInstance.Base; + + public BaseRandomizer( AzureStorage azureStorage, CommonSettingsProcessor settingsProcessor, IOptionsMonitor optionsMonitor, - ILogger logger) { - this.AzureStorage = azureStorage; - this.SettingsProcessor = settingsProcessor; - this.OptionsMonitor = optionsMonitor; - this.Logger = logger; + ILogger logger) { + AzureStorage = azureStorage; + SettingsProcessor = settingsProcessor; + OptionsMonitor = optionsMonitor; + Logger = logger; } private CommonSettingsProcessor SettingsProcessor { get; } private AzureStorage AzureStorage { get; } private IOptionsMonitor OptionsMonitor { get; } - private ILogger Logger { get; } - private ServiceOptions Configuration => this.OptionsMonitor.CurrentValue; + private ILogger Logger { get; } + private ServiceOptions Configuration => OptionsMonitor.CurrentValue; + + public void Validate(SeedSettings settings) { + this.SettingsProcessor.ValidateSettings(Instance, settings); + } public async Task Randomize(string id, SeedSettings settings) { - this.Logger.LogDebug("Recieved request for id {id} to randomize settings {@settings}", id, settings); + Logger.LogDebug("Recieved request for id {id} to randomize settings {@settings}", id, settings); var start = new ProcessStartInfo() { FileName = Configuration.PythonPath, - WorkingDirectory = Configuration.RandomizerPath, + WorkingDirectory = Configuration.RandomizerPaths[Name], RedirectStandardOutput = true, RedirectStandardError = true, }; @@ -43,7 +51,7 @@ var args = start.ArgumentList; args.Add("DungeonRandomizer.py"); args.Add("--rom"); - args.Add(this.Configuration.Baserom); + args.Add(Configuration.Baserom); args.Add("--bps"); args.Add("--outputpath"); @@ -60,17 +68,17 @@ args.Add("--shufflelinks"); args.Add("--shuffletavern"); - foreach (var arg in this.SettingsProcessor.GetSettings(settings)) { + foreach (var arg in SettingsProcessor.GetSettings(Instance, settings)) { args.Add(arg); } - this.Logger.LogInformation("Randomizing with args: {args}", string.Join(" ", args)); + Logger.LogInformation("Randomizing with args: {args}", string.Join(" ", args)); var process = Process.Start(start) ?? throw new GenerationFailedException("Process failed to start."); process.EnableRaisingEvents = true; - process.OutputDataReceived += (_, args) => this.Logger.LogInformation("Randomizer STDOUT: {output}", args.Data); - process.ErrorDataReceived += (_, args) => this.Logger.LogInformation("Randomizer STDERR: {output}", args.Data); + process.OutputDataReceived += (_, args) => Logger.LogInformation("Randomizer STDOUT: {output}", args.Data); + process.ErrorDataReceived += (_, args) => Logger.LogInformation("Randomizer STDERR: {output}", args.Data); process.BeginOutputReadLine(); process.BeginErrorReadLine(); @@ -79,18 +87,18 @@ var exitcode = process.ExitCode; if (exitcode != 0) { - await this.GenerationFailed(id, exitcode); + await GenerationFailed(id, exitcode); } else { - await this.GenerationSucceeded(id, settings); + await GenerationSucceeded(id, settings); } }; var settingsJson = JsonSerializer.SerializeToDocument(settings, JsonOptions.Default); var settingsOut = string.Format("{0}/settings.json", id); - var uploadSettings = this.AzureStorage.UploadFile(settingsOut, new BinaryData(settingsJson)); + var uploadSettings = AzureStorage.UploadFile(settingsOut, new BinaryData(settingsJson)); var generating = string.Format("{0}/generating", id); - var uploadGenerating = this.AzureStorage.UploadFile(generating, BinaryData.Empty); + var uploadGenerating = AzureStorage.UploadFile(generating, BinaryData.Empty); await Task.WhenAll(uploadSettings, uploadGenerating); } @@ -100,29 +108,29 @@ var bpsIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}.bps", id)); var bpsOut = string.Format("{0}/patch.bps", id); - var uploadPatch = this.AzureStorage.UploadFileAndDelete(bpsOut, bpsIn); + var uploadPatch = AzureStorage.UploadFileAndDelete(bpsOut, bpsIn); var spoilerIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}_Spoiler.json", id)); var spoilerOut = string.Format("{0}/spoiler.json", id); - var uploadSpoiler = this.AzureStorage.UploadFileAndDelete(spoilerOut, spoilerIn); + var uploadSpoiler = AzureStorage.UploadFileAndDelete(spoilerOut, spoilerIn); var metaIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}_Meta.json", id)); var metaOut = string.Format("{0}/meta.json", id); - var meta = this.ProcessMetadata(metaIn); - var uploadMeta = this.AzureStorage.UploadFile(metaOut, new BinaryData(meta)); + var meta = ProcessMetadata(metaIn); + var uploadMeta = AzureStorage.UploadFile(metaOut, new BinaryData(meta)); var generating = string.Format("{0}/generating", id); - var deleteGenerating = this.AzureStorage.DeleteFile(generating); + var deleteGenerating = AzureStorage.DeleteFile(generating); await Task.WhenAll(uploadPatch, uploadSpoiler, uploadMeta, deleteGenerating); - this.Logger.LogDebug("Deleting file {filepath}", metaIn); + Logger.LogDebug("Deleting file {filepath}", metaIn); File.Delete(metaIn); - this.Logger.LogDebug("Deleting file {filepath}", rom); + Logger.LogDebug("Deleting file {filepath}", rom); File.Delete(rom); - this.Logger.LogInformation("Finished uploading seed id {id}", id); + Logger.LogInformation("Finished uploading seed id {id}", id); } private JsonDocument ProcessMetadata(string path) { @@ -146,7 +154,7 @@ private async Task GenerationFailed(string id, int exitcode) { var generating = string.Format("{0}/generating", id); - var deleteGenerating = this.AzureStorage.DeleteFile(generating); + var deleteGenerating = AzureStorage.DeleteFile(generating); await Task.WhenAll(deleteGenerating); } diff --git a/ALttPRandomizer/Randomizers/IRandomizer.cs b/ALttPRandomizer/Randomizers/IRandomizer.cs new file mode 100644 index 0000000..410b06c --- /dev/null +++ b/ALttPRandomizer/Randomizers/IRandomizer.cs @@ -0,0 +1,10 @@ +namespace ALttPRandomizer.Randomizers { + using ALttPRandomizer.Model; + using System.Threading.Tasks; + + public interface IRandomizer { + public void Validate(SeedSettings settings); + + public Task Randomize(string id, SeedSettings settings); + } +} diff --git a/ALttPRandomizer/SeedController.cs b/ALttPRandomizer/SeedController.cs index 32de1f0..53ccd96 100644 --- a/ALttPRandomizer/SeedController.cs +++ b/ALttPRandomizer/SeedController.cs @@ -1,6 +1,7 @@ namespace ALttPRandomizer { using ALttPRandomizer.Model; using ALttPRandomizer.Service; + using ALttPRandomizer.Settings; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.Threading.Tasks; @@ -19,9 +20,13 @@ [Route("/generate")] [HttpPost] public async Task Generate(SeedSettings settings) { - var id = await this.RandomizeService.RandomizeSeed(settings); - var url = string.Format("/seed/{0}", id); - return Accepted(url, id); + try { + var id = await this.RandomizeService.RandomizeSeed(settings); + var url = string.Format("/seed/{0}", id); + return Accepted(url, id); + } catch (InvalidSettingsException ex) { + return BadRequest(ex.Message); + } } [Route("/seed/{id}")] diff --git a/ALttPRandomizer/Service/RandomizeService.cs b/ALttPRandomizer/Service/RandomizeService.cs index 7ea4fbe..2876c3e 100644 --- a/ALttPRandomizer/Service/RandomizeService.cs +++ b/ALttPRandomizer/Service/RandomizeService.cs @@ -1,24 +1,41 @@ -using ALttPRandomizer.Model; -using Microsoft.Extensions.Logging; -using System.Threading.Tasks; +namespace ALttPRandomizer.Service { + using ALttPRandomizer.Model; + using ALttPRandomizer.Randomizers; + using ALttPRandomizer.Settings; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using System; + using System.Reflection; + using System.Threading.Tasks; -namespace ALttPRandomizer.Service { public class RandomizeService { - public RandomizeService(IdGenerator idGenerator, Randomizer randomizer, ILogger logger) { + public RandomizeService(IdGenerator idGenerator, IServiceProvider serviceProvider, ILogger logger) { this.IdGenerator = idGenerator; - this.Randomizer = randomizer; + this.ServiceProvider = serviceProvider; this.Logger = logger; } private ILogger Logger { get; } private IdGenerator IdGenerator { get; } - private Randomizer Randomizer { get; } + private IServiceProvider ServiceProvider { get; } public async Task RandomizeSeed(SeedSettings settings) { var id = this.IdGenerator.GenerateId(); this.Logger.LogInformation("Generating seed {seedId} with settings {@settings}", id, settings); - await this.Randomizer.Randomize(id, settings); + + var fi = typeof(RandomizerInstance).GetField(settings.Randomizer.ToString(), BindingFlags.Static | BindingFlags.Public); + + var randomizerKey = fi?.GetCustomAttribute()?.Name; + + if (randomizerKey == null) { + throw new InvalidSettingsException("Invalid randomizer: {0}", settings.Randomizer); + } + + var randomizer = this.ServiceProvider.GetRequiredKeyedService(randomizerKey); + randomizer.Validate(settings); + + await randomizer.Randomize(id, settings); return id; } } diff --git a/ALttPRandomizer/Settings/Attributes.cs b/ALttPRandomizer/Settings/Attributes.cs index 6da3fb8..6d66af5 100644 --- a/ALttPRandomizer/Settings/Attributes.cs +++ b/ALttPRandomizer/Settings/Attributes.cs @@ -1,21 +1,83 @@ namespace ALttPRandomizer.Settings { + using ALttPRandomizer.Model; using System; + using System.Linq; - internal class SettingNameAttribute : Attribute { - public SettingNameAttribute(string name) { + internal class RandomizerNameAttribute : Attribute { + public RandomizerNameAttribute(string name) { this.Name = name; } public string Name { get; } } - internal class NoSettingNameAttribute : Attribute { } + internal abstract class RandomizerSpecificAttribute : Attribute { + public RandomizerSpecificAttribute(RandomizerInstance[]? randomizers) { + this.Randomizers = randomizers; + } - internal class AdditionalSettingAttribute : Attribute { - public AdditionalSettingAttribute(string setting) { + protected RandomizerInstance[]? Randomizers { get; } + + public bool HasRandomizer(RandomizerInstance name) { + if (this.Randomizers == null) { + return true; + } + + return this.Randomizers.Contains(name); + } + } + + internal class SettingNameAttribute : RandomizerSpecificAttribute { + public SettingNameAttribute(string name) : base(null) { + this.Name = name; + } + + public SettingNameAttribute(RandomizerInstance[] randomizers, string name) : base(randomizers) { + this.Name = name; + } + + public string Name { get; } + } + + internal class NoSettingNameAttribute : RandomizerSpecificAttribute { + public NoSettingNameAttribute() : base(null) { } + + public NoSettingNameAttribute(RandomizerInstance[] randomizers) : base(randomizers) { } + } + + internal class AdditionalSettingAttribute : RandomizerSpecificAttribute { + public AdditionalSettingAttribute(string setting) : base(null) { + this.Setting = setting; + } + + public AdditionalSettingAttribute(RandomizerInstance[] randomizers, string setting) : base(randomizers) { this.Setting = setting; } public string Setting { get; } } + + internal class RequiredSettingAttribute : RandomizerSpecificAttribute { + public RequiredSettingAttribute(params object[] values) : base(null) { + this.Values = values; + } + + public RequiredSettingAttribute(RandomizerInstance[] randomizers, params object[] values) : base(randomizers) { + this.Values = values; + } + + public object[] Values { get; } + } + + internal class ForbiddenSettingAttribute : RandomizerSpecificAttribute { + public ForbiddenSettingAttribute(params object[] values) : base(null) { + this.Values = values; + } + + public ForbiddenSettingAttribute(RandomizerInstance[] randomizers, params object[] values) : base(randomizers) { + this.Values = values; + } + + public object[] Values { get; } + } } diff --git a/ALttPRandomizer/Settings/CommonSettingsProcessor.cs b/ALttPRandomizer/Settings/CommonSettingsProcessor.cs index b948420..18eed32 100644 --- a/ALttPRandomizer/Settings/CommonSettingsProcessor.cs +++ b/ALttPRandomizer/Settings/CommonSettingsProcessor.cs @@ -1,38 +1,61 @@ -using System; - -namespace ALttPRandomizer.Settings { +namespace ALttPRandomizer.Settings { using ALttPRandomizer.Model; + using System; using System.Collections.Generic; + using System.Linq; using System.Reflection; public class CommonSettingsProcessor { - public IList GetSettings(SeedSettings settings) { - var args = new List(); - + public IEnumerable GetSettings(RandomizerInstance randomizer, SeedSettings settings) { var props = typeof(SeedSettings).GetProperties(BindingFlags.Instance | BindingFlags.Public); foreach (var prop in props) { - var value = prop.GetValue(settings) ?? throw new SettingsLookupException("settings.{} not found", prop.Name); - var valueFieldName = value.ToString() ?? throw new SettingsLookupException("settings.{}.ToString() returned null", prop.Name); + var value = prop.GetValue(settings) ?? throw new SettingsLookupException("settings.{0} not found", prop.Name); + var valueFieldName = value.ToString() ?? throw new SettingsLookupException("settings.{0}.ToString() returned null", prop.Name); var fi = prop.PropertyType.GetField(valueFieldName, BindingFlags.Static | BindingFlags.Public) - ?? throw new SettingsLookupException("Could not get field info for value {}.{}", prop.PropertyType, valueFieldName); + ?? throw new SettingsLookupException("Could not get field info for value {0}.{1}", prop.PropertyType, valueFieldName); - if (prop.GetCustomAttribute() == null) { - var settingName = prop.GetCustomAttribute()?.Name ?? prop.Name.ToLower(); - var valueName = fi.GetCustomAttribute()?.Name ?? valueFieldName.ToLower(); + if (!prop.GetCustomAttributes().Any(att => att.HasRandomizer(randomizer))) { + var settingName = + prop.GetCustomAttributes() + .FirstOrDefault(att => att.HasRandomizer(randomizer))?.Name ?? prop.Name.ToLower(); + var valueName = + fi.GetCustomAttributes() + .FirstOrDefault(att => att.HasRandomizer(randomizer))?.Name ?? valueFieldName.ToLower(); - args.Add(string.Format("--{0}={1}", settingName, valueName)); + yield return string.Format("--{0}={1}", settingName, valueName); } - foreach (var att in fi.GetCustomAttributes()) { - args.Add(att.Setting); + foreach (var att in fi.GetCustomAttributes().Where(att => att.HasRandomizer(randomizer))) { + yield return att.Setting; } } + } - return args; + public void ValidateSettings(RandomizerInstance randomizer, SeedSettings settings) { + var props = typeof(SeedSettings).GetProperties(BindingFlags.Instance | BindingFlags.Public); + foreach (var prop in props) { + var value = prop.GetValue(settings) ?? throw new SettingsLookupException("settings.{0} not found", prop.Name); + + foreach (var att in prop.GetCustomAttributes().Where(att => att.HasRandomizer(randomizer))) { + if (!att.Values.Contains(value)) { + throw new InvalidSettingsException("{0} contains value {1} not in required set [{2}]", prop.Name, value, string.Join(", ", att.Values)); + } + } + + foreach (var att in prop.GetCustomAttributes().Where(att => att.HasRandomizer(randomizer))) { + if (att.Values.Contains(value)) { + throw new InvalidSettingsException("{0} contains forbidden value {1}", prop.Name, value); + } + } + } } } public class SettingsLookupException : Exception { public SettingsLookupException(string message, params object?[] args) : base(string.Format(message, args)) { } } + + public class InvalidSettingsException : Exception { + public InvalidSettingsException(string message, params object?[] args) : base(string.Format(message, args)) { } + } } diff --git a/ALttPRandomizer/appsettings.Docker.json b/ALttPRandomizer/appsettings.Docker.json index 40f3fd9..da3f10e 100644 --- a/ALttPRandomizer/appsettings.Docker.json +++ b/ALttPRandomizer/appsettings.Docker.json @@ -2,7 +2,7 @@ "ALttPRandomizer": { "baserom": "/randomizer/alttp.sfc", "pythonPath": "/usr/bin/python3", - "randomizerPath": "/randomizer", + "flipsPath": "/flips/flips", "allowedCors": [ "https://new.alttpr.gwaa.kiwi", "http://localhost:8082" @@ -10,6 +10,10 @@ "azureSettings": { "clientId": "a48d5ae1-fa0a-4e33-9586-18c0eca0a28c", "blobstoreEndpoint": "https://alttprstorage.blob.core.windows.net/seeds" + }, + "randomizerPaths": { + "base": "/randomizer", + "apr2025": "/apr2025_randomizer" } }, "Serilog": { diff --git a/Apr2025Randomizer b/Apr2025Randomizer new file mode 160000 index 0000000..1d84a6b --- /dev/null +++ b/Apr2025Randomizer @@ -0,0 +1 @@ +Subproject commit 1d84a6b348268c26cf0c4779710f8b1fe866d873 diff --git a/Dockerfile b/Dockerfile index 2fbf9c9..a74cc02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,10 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0-azurelinux3.0 AS final EXPOSE 8080 EXPOSE 8081 -RUN tdnf install -y python3 +RUN tdnf install -y python3 wget unzip +RUN wget https://github.com/Alcaro/Flips/releases/download/v198/flips-linux.zip +RUN unzip flips-linux.zip -d /flips +RUN rm flips-linux.zip RUN mkdir -p /randomizer/data RUN touch /randomizer/data/base2current.json @@ -30,6 +33,9 @@ RUN python3 -m pip install -r requirements.txt COPY BaseRandomizer/ . +WORKDIR /apr2025_randomizer +COPY Apr2025Randomizer/ . + WORKDIR /app COPY --from=build /app/publish . COPY ALttPRandomizer/appsettings.Docker.json appsettings.json