From fd2e8190c31a28579bf7bb030393521a41d3f02f Mon Sep 17 00:00:00 2001 From: Kara Alexandra Date: Thu, 13 Mar 2025 21:51:53 -0500 Subject: [PATCH] Multiworld --- ALttPRandomizer/Azure/AzureStorage.cs | 6 +- ALttPRandomizer/Model/SeedSettings.cs | 3 + ALttPRandomizer/Program.cs | 1 + ALttPRandomizer/Randomizers/BaseRandomizer.cs | 180 ++++++++++++++---- ALttPRandomizer/SeedController.cs | 35 ++++ ALttPRandomizer/Service/RandomizeService.cs | 15 +- ALttPRandomizer/Service/SeedService.cs | 56 ++++++ .../Settings/CommonSettingsProcessor.cs | 3 + BaseRandomizer | 2 +- 9 files changed, 262 insertions(+), 39 deletions(-) diff --git a/ALttPRandomizer/Azure/AzureStorage.cs b/ALttPRandomizer/Azure/AzureStorage.cs index 7d94e60..31ba8af 100644 --- a/ALttPRandomizer/Azure/AzureStorage.cs +++ b/ALttPRandomizer/Azure/AzureStorage.cs @@ -27,11 +27,15 @@ await BlobClient.UploadBlobAsync(name, data); } - public async Task UploadFileAndDelete(string name, string filepath) { + public async Task UploadFileFromSource(string name, string filepath) { using (var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read)) { this.Logger.LogDebug("Uploading file {filepath} -> {name}", filepath, name); await this.UploadFile(name, stream); } + } + + public async Task UploadFileAndDelete(string name, string filepath) { + await this.UploadFileFromSource(name, filepath); this.Logger.LogDebug("Deleting file {filepath}", filepath); File.Delete(filepath); diff --git a/ALttPRandomizer/Model/SeedSettings.cs b/ALttPRandomizer/Model/SeedSettings.cs index 909cc26..e8b1e5e 100644 --- a/ALttPRandomizer/Model/SeedSettings.cs +++ b/ALttPRandomizer/Model/SeedSettings.cs @@ -9,6 +9,9 @@ [NoSettingName] public RandomizerInstance Randomizer { get; set; } = RandomizerInstance.Base; + [NoSettingName] + public string PlayerName { get; set; } = string.Empty; + [NoSettingName] public RaceMode Race { get; set; } = RaceMode.Normal; diff --git a/ALttPRandomizer/Program.cs b/ALttPRandomizer/Program.cs index ebb7f82..25553d3 100644 --- a/ALttPRandomizer/Program.cs +++ b/ALttPRandomizer/Program.cs @@ -73,6 +73,7 @@ builder.Services.AddKeyedScoped(BaseRandomizer.Name); builder.Services.AddKeyedScoped(Apr2025Randomizer.Name); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/ALttPRandomizer/Randomizers/BaseRandomizer.cs b/ALttPRandomizer/Randomizers/BaseRandomizer.cs index 59be054..061ddde 100644 --- a/ALttPRandomizer/Randomizers/BaseRandomizer.cs +++ b/ALttPRandomizer/Randomizers/BaseRandomizer.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; + using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -20,16 +21,19 @@ public BaseRandomizer( AzureStorage azureStorage, CommonSettingsProcessor settingsProcessor, + IdGenerator idGenerator, IOptionsMonitor optionsMonitor, ILogger logger) { - AzureStorage = azureStorage; - SettingsProcessor = settingsProcessor; - OptionsMonitor = optionsMonitor; - Logger = logger; + this.AzureStorage = azureStorage; + this.SettingsProcessor = settingsProcessor; + this.IdGenerator = idGenerator; + this.OptionsMonitor = optionsMonitor; + this.Logger = logger; } private CommonSettingsProcessor SettingsProcessor { get; } private AzureStorage AzureStorage { get; } + private IdGenerator IdGenerator { get; } private IOptionsMonitor OptionsMonitor { get; } private ILogger Logger { get; } private ServiceOptions Configuration => OptionsMonitor.CurrentValue; @@ -38,9 +42,30 @@ 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); + public void ValidateAll(IList settings) { + foreach (var settingsItem in settings) { + this.Validate(settingsItem); + if (string.IsNullOrWhiteSpace(settingsItem.PlayerName)) { + throw new InvalidSettingsException("PlayerNames must be non-empty"); + } + } + } + private IList GetArgs(SeedSettings settings) { + var args = new List() { + "--reduce_flashing", + "--quickswap", + "--shufflelinks", + "--shuffletavern", + }; + + foreach (var arg in SettingsProcessor.GetSettings(Instance, settings)) { + args.Add(arg); + } + return args; + } + + private async Task StartProcess(string id, IEnumerable settings, Func completed) { var start = new ProcessStartInfo() { FileName = Configuration.PythonPath, WorkingDirectory = Configuration.RandomizerPaths[Name], @@ -62,13 +87,7 @@ args.Add("--spoiler=json"); - args.Add("--reduce_flashing"); - args.Add("--quickswap"); - - args.Add("--shufflelinks"); - args.Add("--shuffletavern"); - - foreach (var arg in SettingsProcessor.GetSettings(Instance, settings)) { + foreach (var arg in settings) { args.Add(arg); } @@ -87,51 +106,140 @@ process.BeginErrorReadLine(); process.Exited += async (sender, args) => { - var exitcode = process.ExitCode; + await completed.Invoke(process.ExitCode); + }; + } + public async Task Randomize(string id, SeedSettings settings) { + Logger.LogDebug("Recieved request for id {id} to randomize settings {@settings}", id, settings); + + await StartProcess(id, this.GetArgs(settings), async exitcode => { if (exitcode != 0) { await GenerationFailed(id, exitcode); } else { - await GenerationSucceeded(id, settings); + await SingleSucceeded(id); } - }; + }); 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("OR_{0}.sfc", id)); + public async Task RandomizeMultiworld(string id, IList settings) { + Logger.LogDebug("Recieved request for id {id} to randomize multiworld settings {@settings}", id, settings); - var bpsIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}.bps", id)); + var names = settings.Select(s => s.PlayerName).ToList(); + + var args = settings.Select((s, idx) => string.Format("--p{0}={1}", idx + 1, string.Join(" ", this.GetArgs(s)))) + .Append(string.Format("--names={0}", string.Join(",", names))) + .Append(string.Format("--multi={0}", settings.Count)); + + await StartProcess(id, args, async exitcode => { + if (exitcode != 0) { + await GenerationFailed(id, exitcode); + } else { + await MultiSucceeded(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 SingleSucceeded(string id) { + var basename = string.Format("OR_{0}", id); + await this.UploadFiles(id, basename, 1, null); + + var metaIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}_Meta.json", id)); + Logger.LogDebug("Deleting file {filepath}", metaIn); + File.Delete(metaIn); + + var spoilerIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}_Spoiler.json", id)); + Logger.LogDebug("Deleting file {filepath}", spoilerIn); + File.Delete(spoilerIn); + + var generating = string.Format("{0}/generating", id); + await AzureStorage.DeleteFile(generating); + + Logger.LogInformation("Finished uploading seed id {id}", id); + } + + private async Task UploadFiles(string id, string basename, int playerNum, string? parentId) { + var tasks = new List(); + + var rom = Path.Join(Path.GetTempPath(), string.Format("{0}.sfc", basename)); + Logger.LogDebug("Deleting file {filepath}", rom); + File.Delete(rom); + + var bpsIn = Path.Join(Path.GetTempPath(), string.Format("{0}.bps", basename)); var bpsOut = string.Format("{0}/patch.bps", id); - var uploadPatch = AzureStorage.UploadFileAndDelete(bpsOut, bpsIn); + tasks.Add(this.AzureStorage.UploadFileAndDelete(bpsOut, bpsIn)); + + var spoilerIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}_Spoiler.json", parentId ?? id)); + var spoilerOut = string.Format("{0}/spoiler.json", id); + tasks.Add(this.AzureStorage.UploadFileFromSource(spoilerOut, spoilerIn)); + + var metaIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}_Meta.json", parentId ?? id)); + var metaOut = string.Format("{0}/meta.json", id); + var meta = ProcessMetadata(metaIn, playerNum); + tasks.Add(this.AzureStorage.UploadFile(metaOut, new BinaryData(meta))); + + if (parentId != null) { + var parentOut = string.Format("{0}/parent", id); + tasks.Add(this.AzureStorage.UploadFile(parentOut, new BinaryData(parentId))); + } + + await Task.WhenAll(tasks); + } + + private async Task MultiSucceeded(string id, IList settings) { + var tasks = new List(); + var subIds = new List(); + var worlds = new List(); + + for (var i = 0; i < settings.Count; i++) { + var basename = string.Format("OR_{0}_P{1}_{2}", id, i + 1, settings[i].PlayerName); + var randomId = this.IdGenerator.GenerateId(); + subIds.Add(randomId); + tasks.Add(this.UploadFiles(randomId, basename, i + 1, id)); + + worlds.Add(new { Name = settings[i].PlayerName, Id = randomId }); + + var settingsJson = JsonSerializer.SerializeToDocument(settings[i], JsonOptions.Default); + var settingsOut = string.Format("{0}/settings.json", randomId); + tasks.Add(this.AzureStorage.UploadFile(settingsOut, new BinaryData(settingsJson))); + } + + var worldsJson = JsonSerializer.SerializeToDocument(worlds, JsonOptions.Default); + var worldsOut = string.Format("{0}/worlds.json", id); + + tasks.Add(this.AzureStorage.UploadFile(worldsOut, new BinaryData(worldsJson))); + + await Task.WhenAll(tasks); + + var metaIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}_Meta.json", id)); + var metaOut = string.Format("{0}/meta.json", id); + var uploadMeta = AzureStorage.UploadFileAndDelete(metaOut, metaIn); var spoilerIn = Path.Join(Path.GetTempPath(), string.Format("OR_{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("OR_{0}_Meta.json", id)); - var metaOut = string.Format("{0}/meta.json", id); - var meta = ProcessMetadata(metaIn); - var uploadMeta = AzureStorage.UploadFile(metaOut, new BinaryData(meta)); + var multidataIn = Path.Join(Path.GetTempPath(), string.Format("OR_{0}_multidata", id)); + var multidataOut = string.Format("{0}/multidata", id); + var uploadMultidata = AzureStorage.UploadFileAndDelete(multidataOut, multidataIn); + + await Task.WhenAll(uploadMeta, uploadSpoiler, uploadMultidata); var generating = string.Format("{0}/generating", id); var deleteGenerating = AzureStorage.DeleteFile(generating); - await Task.WhenAll(uploadPatch, uploadSpoiler, uploadMeta, deleteGenerating); - - Logger.LogDebug("Deleting file {filepath}", metaIn); - File.Delete(metaIn); - - Logger.LogDebug("Deleting file {filepath}", rom); - File.Delete(rom); - - Logger.LogInformation("Finished uploading seed id {id}", id); + Logger.LogInformation("Finished uploading multiworld id {id}", id); } - private JsonDocument ProcessMetadata(string path) { + private JsonDocument ProcessMetadata(string path, int playerNum) { JsonDocument orig; using (var file = File.OpenRead(path)) { orig = JsonDocument.Parse(file); @@ -140,8 +248,8 @@ var processed = new Dictionary(); foreach (var toplevel in orig.RootElement.EnumerateObject()) { var value = toplevel.Value; - if (value.ValueKind == JsonValueKind.Object && value.TryGetProperty("1", out var p1)) { - processed[toplevel.Name] = p1; + if (value.ValueKind == JsonValueKind.Object && value.TryGetProperty(playerNum.ToString(), out var p)) { + processed[toplevel.Name] = p; } else { processed[toplevel.Name] = toplevel.Value; } diff --git a/ALttPRandomizer/SeedController.cs b/ALttPRandomizer/SeedController.cs index 75f4585..c08bfa5 100644 --- a/ALttPRandomizer/SeedController.cs +++ b/ALttPRandomizer/SeedController.cs @@ -4,6 +4,7 @@ using ALttPRandomizer.Settings; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; + using System.Collections.Generic; using System.Threading.Tasks; public class SeedController : Controller { @@ -32,6 +33,21 @@ } } + [Route("/multiworld")] + [HttpPost] + public async Task GenerateMultiworld([FromBody] IList settings) { + if (!ModelState.IsValid) { + return BadRequest(ModelState); + } + try { + var id = await this.RandomizeService.RandomizeMultiworld(settings); + var url = string.Format("/multi/{0}", id); + return Accepted(url, id); + } catch (InvalidSettingsException ex) { + return BadRequest(ex.Message); + } + } + [Route("/seed/{id}")] [HttpGet] public async Task GetSeed(string id) { @@ -50,5 +66,24 @@ this.Logger.LogWarning("Unexpected result from SeedService: {@result}", result); return StatusCode(500); } + + [Route("/multi/{id}")] + [HttpGet] + public async Task GetMulti(string id) { + var result = await this.SeedService.GetMulti(id); + if (result.TryGetValue("status", out var responseCode)) { + switch (responseCode) { + case 200: + return Ok(result); + case 404: + return NotFound(result); + case 409: + return Conflict(result); + } + } + + this.Logger.LogWarning("Unexpected result from SeedService: {@result}", result); + return StatusCode(500); + } } } diff --git a/ALttPRandomizer/Service/RandomizeService.cs b/ALttPRandomizer/Service/RandomizeService.cs index 2876c3e..8c04870 100644 --- a/ALttPRandomizer/Service/RandomizeService.cs +++ b/ALttPRandomizer/Service/RandomizeService.cs @@ -5,19 +5,22 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; + using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; public class RandomizeService { - public RandomizeService(IdGenerator idGenerator, IServiceProvider serviceProvider, ILogger logger) { + public RandomizeService(IdGenerator idGenerator, IServiceProvider serviceProvider, BaseRandomizer baseRandomizer, ILogger logger) { this.IdGenerator = idGenerator; this.ServiceProvider = serviceProvider; + this.BaseRandomizer = baseRandomizer; this.Logger = logger; } private ILogger Logger { get; } private IdGenerator IdGenerator { get; } + private BaseRandomizer BaseRandomizer { get; } private IServiceProvider ServiceProvider { get; } public async Task RandomizeSeed(SeedSettings settings) { @@ -38,5 +41,15 @@ await randomizer.Randomize(id, settings); return id; } + + public async Task RandomizeMultiworld(IList settings) { + var id = this.IdGenerator.GenerateId(); + this.Logger.LogInformation("Generating multiworld {seedId} with settings {@settings}", id, settings); + + this.BaseRandomizer.ValidateAll(settings); + + await this.BaseRandomizer.RandomizeMultiworld(id, settings); + return id; + } } } diff --git a/ALttPRandomizer/Service/SeedService.cs b/ALttPRandomizer/Service/SeedService.cs index 5d0dc7e..be3412d 100644 --- a/ALttPRandomizer/Service/SeedService.cs +++ b/ALttPRandomizer/Service/SeedService.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; + using System.Linq; using System.Text.Json; using System.Threading.Tasks; @@ -52,6 +53,10 @@ result["meta"] = json; } + if (files.TryGetValue("parent", out var parent)) { + result["parent"] = parent.ToString(); + } + if (settings.Race != RaceMode.Race && files.TryGetValue("spoiler.json", out var spoilerData)) { var json = JsonDocument.Parse(spoilerData.ToString()); result["spoiler"] = json; @@ -61,5 +66,56 @@ return result; } + + public async Task> GetMulti(string multiId) { + var files = await this.AzureStorage.GetFiles(multiId); + + this.Logger.LogDebug("Found files: {@files}", files.Keys); + + var result = new Dictionary(); + + if (!files.TryGetValue("settings.json", out var settingsData)) { + result["status"] = 404; + result["error"] = "multi not found"; + return result; + } + + var settingsJson = JsonDocument.Parse(settingsData.ToString()); + result["settings"] = settingsJson; + + var settings = settingsJson.Deserialize>(JsonOptions.Default) ?? new List(); + + if (!files.TryGetValue("multidata", out var multidata)) { + if (files.ContainsKey("generating")) { + result["status"] = 409; + result["error"] = "generation still in progress"; + return result; + } else { + result["status"] = 404; + result["error"] = "generation failed"; + return result; + } + } + result["multidata"] = Convert.ToBase64String(multidata.ToArray()); + + if (files.TryGetValue("meta.json", out var metaData)) { + var json = JsonDocument.Parse(metaData.ToString()); + result["meta"] = json; + } + + if (files.TryGetValue("worlds.json", out var worlds)) { + var json = JsonDocument.Parse(worlds.ToString()); + result["worlds"] = json; + } + + if (!settings.Any(s => s.Race == RaceMode.Race) && files.TryGetValue("spoiler.json", out var spoilerData)) { + var json = JsonDocument.Parse(spoilerData.ToString()); + result["spoiler"] = json; + } + + result["status"] = 200; + + return result; + } } } diff --git a/ALttPRandomizer/Settings/CommonSettingsProcessor.cs b/ALttPRandomizer/Settings/CommonSettingsProcessor.cs index 1ab5a2c..c08b17b 100644 --- a/ALttPRandomizer/Settings/CommonSettingsProcessor.cs +++ b/ALttPRandomizer/Settings/CommonSettingsProcessor.cs @@ -10,6 +10,9 @@ var props = typeof(SeedSettings).GetProperties(BindingFlags.Instance | BindingFlags.Public); var starting = new List(); foreach (var prop in props) { + if (prop.Name == nameof(SeedSettings.PlayerName)) { + continue; + } 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) diff --git a/BaseRandomizer b/BaseRandomizer index 9ad70fb..7f41ba8 160000 --- a/BaseRandomizer +++ b/BaseRandomizer @@ -1 +1 @@ -Subproject commit 9ad70fb3d7f579221508f8fe0e706d3a23af0b04 +Subproject commit 7f41ba88d9aa4355a5efefe6a883294392cad232