From b6731ff7738ede0985297fd69d5b32a82c66bdfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:34:13 +0100 Subject: [PATCH 01/19] Add completion flag to `WizardOverlay` --- osu.Game/Overlays/WizardOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/WizardOverlay.cs b/osu.Game/Overlays/WizardOverlay.cs index 34ffa7bd77c9..2a881045fd2f 100644 --- a/osu.Game/Overlays/WizardOverlay.cs +++ b/osu.Game/Overlays/WizardOverlay.cs @@ -45,6 +45,8 @@ public partial class WizardOverlay : ShearedOverlayContainer private LoadingSpinner loading = null!; private ScheduledDelegate? loadingShowDelegate; + public bool Completed { get; private set; } + protected WizardOverlay(OverlayColourScheme scheme) : base(scheme) { @@ -221,6 +223,7 @@ protected virtual void ShowNextStep() else { CurrentStepIndex = null; + Completed = true; Hide(); } From fff99a8b4008800ce5a870ac600618e84d8ffdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 11:54:26 +0100 Subject: [PATCH 02/19] Implement special exporter intended specifically for submission flows --- osu.Game/Database/LegacyBeatmapExporter.cs | 23 +++++--- .../Submission/SubmissionBeatmapExporter.cs | 58 +++++++++++++++++++ 2 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs diff --git a/osu.Game/Database/LegacyBeatmapExporter.cs b/osu.Game/Database/LegacyBeatmapExporter.cs index 8f94fc9e63d0..e7e5ddb4d28a 100644 --- a/osu.Game/Database/LegacyBeatmapExporter.cs +++ b/osu.Game/Database/LegacyBeatmapExporter.cs @@ -61,6 +61,20 @@ public LegacyBeatmapExporter(Storage storage) Configuration = new LegacySkinDecoder().Decode(skinStreamReader) }; + MutateBeatmap(model, playableBeatmap); + + // Encode to legacy format + var stream = new MemoryStream(); + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + return stream; + } + + protected virtual void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { // Convert beatmap elements to be compatible with legacy format // So we truncate time and position values to integers, and convert paths with multiple segments to Bézier curves @@ -145,15 +159,6 @@ public LegacyBeatmapExporter(Storage storage) hasPath.Path.ControlPoints.Add(new PathControlPoint(position)); } } - - // Encode to legacy format - var stream = new MemoryStream(); - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(playableBeatmap, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - - return stream; } protected override string FileExtension => @".osz"; diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs new file mode 100644 index 000000000000..3c50a1bf80d0 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -0,0 +1,58 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Screens.Edit.Submission +{ + public class SubmissionBeatmapExporter : LegacyBeatmapExporter + { + private readonly uint? beatmapSetId; + private readonly HashSet? beatmapIds; + + public SubmissionBeatmapExporter(Storage storage) + : base(storage) + { + } + + public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) + : base(storage) + { + beatmapSetId = putBeatmapSetResponse.BeatmapSetId; + beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + } + + protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) + { + base.MutateBeatmap(beatmapSet, playableBeatmap); + + if (beatmapSetId != null && beatmapIds != null) + { + playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; + playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; + + if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + { + beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + return; + } + + if (playableBeatmap.BeatmapInfo.OnlineID > 0) + throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); + + if (beatmapIds.Count == 0) + throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); + + int newId = beatmapIds.First(); + beatmapIds.Remove(newId); + playableBeatmap.BeatmapInfo.OnlineID = newId; + } + } + } +} From 78e85dc2c7f773ac8cbde2b226ec6ba9b8791672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 5 Feb 2025 12:22:33 +0100 Subject: [PATCH 03/19] Add beatmap submission support --- .../Localisation/BeatmapSubmissionStrings.cs | 40 ++ osu.Game/Localisation/EditorStrings.cs | 10 + osu.Game/Screens/Edit/Editor.cs | 55 ++- .../Submission/BeatmapSubmissionScreen.cs | 422 ++++++++++++++++++ .../Submission/BeatmapSubmissionSettings.cs | 13 + .../Submission/ScreenSubmissionSettings.cs | 15 +- 6 files changed, 544 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs create mode 100644 osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index a4c2b3689422..50b65ab57275 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -39,6 +39,31 @@ public static class BeatmapSubmissionStrings /// public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings"); + /// + /// "Submit beatmap!" + /// + public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); + + /// + /// "Exporting beatmap set in compatibility mode..." + /// + public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + + /// + /// "Preparing beatmap set online..." + /// + public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + + /// + /// "Uploading beatmap set contents..." + /// + public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + + /// + /// "Updating local beatmap with relevant changes..." + /// + public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" /// @@ -119,6 +144,21 @@ public static class BeatmapSubmissionStrings /// public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + /// + /// "Empty beatmaps cannot be submitted." + /// + public static LocalisableString EmptyBeatmapsCannotBeSubmitted => new TranslatableString(getKey(@"empty_beatmaps_cannot_be_submitted"), @"Empty beatmaps cannot be submitted."); + + /// + /// "Update beatmap!" + /// + public static LocalisableString UpdateBeatmap => new TranslatableString(getKey(@"update_beatmap"), @"Update beatmap!"); + + /// + /// "Upload NEW beatmap!" + /// + public static LocalisableString UploadNewBeatmap => new TranslatableString(getKey(@"upload_new_beatmap"), @"Upload NEW beatmap!"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs index 3b4026be11d7..2c834c38bbec 100644 --- a/osu.Game/Localisation/EditorStrings.cs +++ b/osu.Game/Localisation/EditorStrings.cs @@ -69,6 +69,16 @@ public static class EditorStrings /// public static LocalisableString DeleteDifficulty => new TranslatableString(getKey(@"delete_difficulty"), @"Delete difficulty"); + /// + /// "Edit externally" + /// + public static LocalisableString EditExternally => new TranslatableString(getKey(@"edit_externally"), @"Edit externally"); + + /// + /// "Submit beatmap" + /// + public static LocalisableString SubmitBeatmap => new TranslatableString(getKey(@"submit_beatmap"), @"Submit beatmap"); + /// /// "setup" /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3302fafbb8f0..c2a726424356 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -32,6 +32,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; @@ -52,6 +53,7 @@ using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Edit.Setup; +using osu.Game.Screens.Edit.Submission; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Edit.Verify; using osu.Game.Screens.OnlinePlay; @@ -111,6 +113,10 @@ protected bool HasUnsavedChanges [Resolved(canBeNull: true)] private INotificationOverlay notifications { get; set; } + [Resolved(canBeNull: true)] + [CanBeNull] + private LoginOverlay loginOverlay { get; set; } + [Resolved] private RealmAccess realm { get; set; } @@ -1309,11 +1315,22 @@ private IEnumerable createFileMenuItems() if (RuntimeInfo.IsDesktop) { - var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally); + var externalEdit = new EditorMenuItem(EditorStrings.EditExternally, MenuItemType.Standard, editExternally); saveRelatedMenuItems.Add(externalEdit); yield return externalEdit; } + bool isSetMadeOfLegacyRulesetBeatmaps = (isNewBeatmap && Ruleset.Value.IsLegacyRuleset()) + || (!isNewBeatmap && Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Ruleset.IsLegacyRuleset())); + bool submissionAvailable = api.EndpointConfiguration.BeatmapSubmissionServiceUrl != null; + + if (isSetMadeOfLegacyRulesetBeatmaps && submissionAvailable) + { + var upload = new EditorMenuItem(EditorStrings.SubmitBeatmap, MenuItemType.Standard, submitBeatmap); + saveRelatedMenuItems.Add(upload); + yield return upload; + } + yield return new OsuMenuItemSpacer(); yield return new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit); } @@ -1353,6 +1370,42 @@ void startEdit() } } + private void submitBeatmap() + { + if (api.State.Value != APIState.Online) + { + loginOverlay?.Show(); + return; + } + + if (!editorBeatmap.HitObjects.Any()) + { + notifications?.Post(new SimpleNotification + { + Text = BeatmapSubmissionStrings.EmptyBeatmapsCannotBeSubmitted, + }); + return; + } + + if (HasUnsavedChanges) + { + dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() => + { + if (!Save()) + return false; + + startSubmission(); + return true; + }))); + } + else + { + startSubmission(); + } + + void startSubmission() => this.Push(new BeatmapSubmissionScreen()); + } + private void exportBeatmap(bool legacy) { if (HasUnsavedChanges) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs new file mode 100644 index 000000000000..796d975e4f25 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -0,0 +1,422 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Development; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables.Cards; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Extensions; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.IO.Archives; +using osu.Game.Localisation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Menu; +using osuTK; + +namespace osu.Game.Screens.Edit.Submission +{ + public partial class BeatmapSubmissionScreen : OsuScreen + { + private BeatmapSubmissionOverlay overlay = null!; + + public override bool AllowUserExit => false; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); + + [Resolved] + private RealmAccess realmAccess { get; set; } = null!; + + [Resolved] + private Storage storage { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private OsuConfigManager configManager { get; set; } = null!; + + [Resolved] + private OsuGame? game { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + + [Cached] + private BeatmapSubmissionSettings settings { get; } = new BeatmapSubmissionSettings(); + + private Container submissionProgress = null!; + private SubmissionStageProgress exportStep = null!; + private SubmissionStageProgress createSetStep = null!; + private SubmissionStageProgress uploadStep = null!; + private SubmissionStageProgress updateStep = null!; + private Container successContainer = null!; + private Container flashLayer = null!; + private RoundedButton backButton = null!; + + private uint? beatmapSetId; + + private SubmissionBeatmapExporter legacyBeatmapExporter = null!; + private ProgressNotification? exportProgressNotification; + private MemoryStream beatmapPackageStream = null!; + private ProgressNotification? updateProgressNotification; + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + overlay = new BeatmapSubmissionOverlay(), + submissionProgress = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.6f, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background5, + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(20), + Spacing = new Vector2(5), + Children = new Drawable[] + { + createSetStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + exportStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + uploadStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + updateStep = new SubmissionStageProgress + { + StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + successContainer = new Container + { + Padding = new MarginPadding(20), + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Child = flashLayer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = BeatmapCard.CORNER_RADIUS, + Depth = float.MinValue, + Alpha = 0, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + } + } + }, + backButton = new RoundedButton + { + Text = CommonStrings.Back, + Width = 150, + Action = this.Exit, + Enabled = { Value = false }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + } + } + } + } + } + }); + + overlay.State.BindValueChanged(_ => + { + if (overlay.State.Value == Visibility.Hidden) + { + if (!overlay.Completed) + this.Exit(); + else + { + submissionProgress.FadeIn(200, Easing.OutQuint); + createBeatmapSet(); + } + } + }); + beatmapPackageStream = new MemoryStream(); + } + + private void createBeatmapSet() + { + bool beatmapHasOnlineId = Beatmap.Value.BeatmapSetInfo.OnlineID > 0; + + var createRequest = beatmapHasOnlineId + ? PutBeatmapSetRequest.UpdateExisting( + (uint)Beatmap.Value.BeatmapSetInfo.OnlineID, + Beatmap.Value.BeatmapSetInfo.Beatmaps.Where(b => b.OnlineID > 0).Select(b => (uint)b.OnlineID).ToArray(), + (uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count(b => b.OnlineID <= 0), + settings.Target.Value) + : PutBeatmapSetRequest.CreateNew((uint)Beatmap.Value.BeatmapSetInfo.Beatmaps.Count, settings.Target.Value); + + createRequest.Success += async response => + { + createSetStep.SetCompleted(); + beatmapSetId = response.BeatmapSetId; + + // at this point the set has an assigned online ID. + // it's important to proactively store it to the realm database, + // so that in the event in further failures in the process, the online ID is not lost. + // losing it can incur creation of redundant new sets server-side, or even cause online ID confusion. + if (!beatmapHasOnlineId) + { + await realmAccess.WriteAsync(r => + { + var refetchedSet = r.Find(Beatmap.Value.BeatmapSetInfo.ID); + refetchedSet!.OnlineID = (int)beatmapSetId.Value; + }).ConfigureAwait(true); + } + + legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + await createBeatmapPackage(response.Files).ConfigureAwait(true); + }; + createRequest.Failure += ex => + { + createSetStep.SetFailed(ex.Message); + backButton.Enabled.Value = true; + Logger.Log($"Beatmap set submission failed on creation: {ex}"); + }; + + createSetStep.SetInProgress(); + api.Queue(createRequest); + } + + private async Task createBeatmapPackage(ICollection onlineFiles) + { + Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); + + try + { + await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) + .ConfigureAwait(true); + } + catch (Exception ex) + { + exportStep.SetFailed(ex.Message); + Logger.Log($"Beatmap set submission failed on export: {ex}"); + backButton.Enabled.Value = true; + exportProgressNotification = null; + } + + exportStep.SetCompleted(); + exportProgressNotification = null; + + if (onlineFiles.Count > 0) + await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + else + replaceBeatmapSet(); + } + + private async Task patchBeatmapSet(ICollection onlineFiles) + { + Debug.Assert(beatmapSetId != null); + + var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); + + // disposing the `ArchiveReader` makes the underlying stream no longer readable which we don't want. + // make a local copy to defend against it. + using var archiveReader = new ZipArchiveReader(new MemoryStream(beatmapPackageStream.ToArray())); + var filesToUpdate = new HashSet(); + + foreach (string filename in archiveReader.Filenames) + { + string localHash = archiveReader.GetStream(filename).ComputeSHA2Hash(); + + if (!onlineFilesByFilename.Remove(filename, out string? onlineHash)) + { + filesToUpdate.Add(filename); + continue; + } + + if (localHash != onlineHash) + filesToUpdate.Add(filename); + } + + var changedFiles = new Dictionary(); + + foreach (string file in filesToUpdate) + changedFiles.Add(file, await archiveReader.GetStream(file).ReadAllBytesToArrayAsync().ConfigureAwait(true)); + + var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); + patchRequest.FilesChanged.AddRange(changedFiles); + patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); + patchRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + patchRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); + + api.Queue(patchRequest); + uploadStep.SetInProgress(); + } + + private void replaceBeatmapSet() + { + Debug.Assert(beatmapSetId != null); + + var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); + + uploadRequest.Success += async () => + { + uploadStep.SetCompleted(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + game?.OpenUrlExternally($"{api.EndpointConfiguration.WebsiteRootUrl}/beatmapsets/{beatmapSetId}"); + + await updateLocalBeatmap().ConfigureAwait(true); + }; + uploadRequest.Failure += ex => + { + uploadStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on upload: {ex}"); + backButton.Enabled.Value = true; + }; + uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); + + api.Queue(uploadRequest); + uploadStep.SetInProgress(); + } + + private async Task updateLocalBeatmap() + { + Debug.Assert(beatmapSetId != null); + updateStep.SetInProgress(); + + Live? importedSet; + + try + { + importedSet = await beatmaps.ImportAsUpdate( + updateProgressNotification = new ProgressNotification(), + new ImportTask(beatmapPackageStream, $"{beatmapSetId}.osz"), + Beatmap.Value.BeatmapSetInfo).ConfigureAwait(true); + } + catch (Exception ex) + { + updateStep.SetFailed(ex.Message); + Logger.Log($"Beatmap submission failed on local update: {ex}"); + Schedule(() => backButton.Enabled.Value = true); + return; + } + + updateStep.SetCompleted(); + backButton.Enabled.Value = true; + backButton.Action = () => + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + }; + showBeatmapCard(); + } + + private void showBeatmapCard() + { + Debug.Assert(beatmapSetId != null); + + var getBeatmapSetRequest = new GetBeatmapSetRequest((int)beatmapSetId.Value); + getBeatmapSetRequest.Success += beatmapSet => + { + LoadComponentAsync(new BeatmapCardExtra(beatmapSet, false), loaded => + { + successContainer.Add(loaded); + flashLayer.FadeOutFromOne(2000, Easing.OutQuint); + }); + }; + + api.Queue(getBeatmapSetRequest); + } + + protected override void Update() + { + base.Update(); + + if (exportProgressNotification != null && exportProgressNotification.Ongoing) + exportStep.SetInProgress(exportProgressNotification.Progress); + + if (updateProgressNotification != null && updateProgressNotification.Ongoing) + updateStep.SetInProgress(updateProgressNotification.Progress); + } + + public override void OnEntering(ScreenTransitionEvent e) + { + base.OnEntering(e); + + overlay.Show(); + } + } +} diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs new file mode 100644 index 000000000000..359dc11f39e8 --- /dev/null +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionSettings.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Screens.Edit.Submission +{ + public class BeatmapSubmissionSettings + { + public Bindable Target { get; } = new Bindable(); + } +} diff --git a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs index 72da94afa1db..08b4d9f71274 100644 --- a/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs +++ b/osu.Game/Screens/Edit/Submission/ScreenSubmissionSettings.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Localisation; +using osu.Game.Online.API.Requests; using osu.Game.Overlays; using osuTK; @@ -22,8 +23,10 @@ public partial class ScreenSubmissionSettings : WizardScreen private readonly BindableBool notifyOnDiscussionReplies = new BindableBool(); private readonly BindableBool loadInBrowserAfterSubmission = new BindableBool(); + public override LocalisableString? NextStepText => BeatmapSubmissionStrings.ConfirmSubmission; + [BackgroundDependencyLoader] - private void load(OsuConfigManager configManager, OsuColour colours) + private void load(OsuConfigManager configManager, OsuColour colours, BeatmapSubmissionSettings settings) { configManager.BindWith(OsuSetting.EditorSubmissionNotifyOnDiscussionReplies, notifyOnDiscussionReplies); configManager.BindWith(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission, loadInBrowserAfterSubmission); @@ -39,6 +42,7 @@ private void load(OsuConfigManager configManager, OsuColour colours) { RelativeSizeAxes = Axes.X, Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption, + Current = settings.Target, }, new FormCheckBox { @@ -60,14 +64,5 @@ private void load(OsuConfigManager configManager, OsuColour colours) } }); } - - private enum BeatmapSubmissionTarget - { - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))] - WIP, - - [LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))] - Pending, - } } } From e1a146d487300feb616adcf100563945aa3d17e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 6 Feb 2025 08:38:28 +0100 Subject: [PATCH 04/19] Remove unnecessary suppressions --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index bb9d32f77b0b..a59a70807956 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -26,10 +26,8 @@ protected override string Uri public uint BeatmapSetID { get; } - // ReSharper disable once CollectionNeverUpdated.Global public Dictionary FilesChanged { get; } = new Dictionary(); - // ReSharper disable once CollectionNeverUpdated.Global public HashSet FilesDeleted { get; } = new HashSet(); public PatchBeatmapPackageRequest(uint beatmapSetId) From bf57fef4125bba86595850a6ec13f5f1fcb3f980 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 17:50:32 +0900 Subject: [PATCH 05/19] Fix missing cached settings in `BetamapSubmissionOverlay` test --- .../Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs index e3e8c0de39c1..f83d424d566f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatmapSubmissionOverlay.cs @@ -24,7 +24,11 @@ public void SetUpSteps() Child = new DependencyProvidingContainer { RelativeSizeAxes = Axes.Both, - CachedDependencies = new[] { (typeof(ScreenFooter), (object)footer) }, + CachedDependencies = new[] + { + (typeof(ScreenFooter), (object)footer), + (typeof(BeatmapSubmissionSettings), new BeatmapSubmissionSettings()), + }, Children = new Drawable[] { receptor, From 46290ae76b81d953253b670c752968906ced6e5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:05:47 +0900 Subject: [PATCH 06/19] Disallow changing beatmap / ruleset while submitting beatmap --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9794402061a0..4c7ea39c3574 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -40,6 +40,8 @@ public partial class BeatmapSubmissionScreen : OsuScreen public override bool AllowUserExit => false; + public override bool DisallowExternalBeatmapRulesetChanges => true; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From 12881f3f366625ecdd861c66e24120541c428995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:06:31 +0900 Subject: [PATCH 07/19] Don't show informational screens for subsequent submissions These are historically only presented to the user when uploading a new beatmap for the first time. --- .../Edit/Submission/BeatmapSubmissionOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs index da2abd8c2385..cf2fef25d5d7 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionOverlay.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Localisation; @@ -15,10 +17,14 @@ public BeatmapSubmissionOverlay() } [BackgroundDependencyLoader] - private void load() + private void load(IBindable beatmap) { - AddStep(); - AddStep(); + if (beatmap.Value.BeatmapSetInfo.OnlineID <= 0) + { + AddStep(); + AddStep(); + } + AddStep(); Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle; From 95967a2fde5ae2015c206d35f3edc86eff318388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:17:49 +0900 Subject: [PATCH 08/19] Adjust beatmap stream creation to make a bit more sense --- .../Edit/Submission/BeatmapSubmissionScreen.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 4c7ea39c3574..44b277886980 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,10 +76,10 @@ public partial class BeatmapSubmissionScreen : OsuScreen private RoundedButton backButton = null!; private uint? beatmapSetId; + private MemoryStream? beatmapPackageStream; private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; - private MemoryStream beatmapPackageStream = null!; private ProgressNotification? updateProgressNotification; [BackgroundDependencyLoader] @@ -189,7 +189,6 @@ private void load() } } }); - beatmapPackageStream = new MemoryStream(); } private void createBeatmapSet() @@ -239,10 +238,12 @@ await realmAccess.WriteAsync(r => private async Task createBeatmapPackage(ICollection onlineFiles) { Debug.Assert(ThreadSafety.IsUpdateThread); + exportStep.SetInProgress(); try { + beatmapPackageStream = new MemoryStream(); await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) .ConfigureAwait(true); } @@ -266,6 +267,7 @@ private async Task createBeatmapPackage(ICollection onlineFiles) private async Task patchBeatmapSet(ICollection onlineFiles) { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var onlineFilesByFilename = onlineFiles.ToDictionary(f => f.Filename, f => f.SHA2Hash); @@ -320,6 +322,7 @@ private async Task patchBeatmapSet(ICollection onlineFiles) private void replaceBeatmapSet() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); @@ -347,6 +350,8 @@ private void replaceBeatmapSet() private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); + Debug.Assert(beatmapPackageStream != null); + updateStep.SetInProgress(); Live? importedSet; @@ -420,5 +425,12 @@ public override void OnEntering(ScreenTransitionEvent e) overlay.Show(); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + beatmapPackageStream?.Dispose(); + } } } From 783ef0078533c7bf90f13675861a88c03c4242e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:34:48 +0900 Subject: [PATCH 09/19] Change `BeatmapSubmissionScreen` to use global back button instead of custom implementation --- .../Submission/BeatmapSubmissionScreen.cs | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 44b277886980..8536ba5f02d7 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -21,7 +21,6 @@ using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Extensions; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.IO.Archives; using osu.Game.Localisation; using osu.Game.Online.API; @@ -38,8 +37,6 @@ public partial class BeatmapSubmissionScreen : OsuScreen { private BeatmapSubmissionOverlay overlay = null!; - public override bool AllowUserExit => false; - public override bool DisallowExternalBeatmapRulesetChanges => true; [Cached] @@ -73,7 +70,6 @@ public partial class BeatmapSubmissionScreen : OsuScreen private SubmissionStageProgress updateStep = null!; private Container successContainer = null!; private Container flashLayer = null!; - private RoundedButton backButton = null!; private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; @@ -82,6 +78,8 @@ public partial class BeatmapSubmissionScreen : OsuScreen private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; + private Live? importedSet; + [BackgroundDependencyLoader] private void load() { @@ -161,15 +159,6 @@ private void load() } } }, - backButton = new RoundedButton - { - Text = CommonStrings.Back, - Width = 150, - Action = this.Exit, - Enabled = { Value = false }, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - } } } } @@ -181,7 +170,10 @@ private void load() if (overlay.State.Value == Visibility.Hidden) { if (!overlay.Completed) + { + allowExit(); this.Exit(); + } else { submissionProgress.FadeIn(200, Easing.OutQuint); @@ -227,8 +219,8 @@ await realmAccess.WriteAsync(r => createRequest.Failure += ex => { createSetStep.SetFailed(ex.Message); - backButton.Enabled.Value = true; Logger.Log($"Beatmap set submission failed on creation: {ex}"); + allowExit(); }; createSetStep.SetInProgress(); @@ -250,9 +242,9 @@ private async Task createBeatmapPackage(ICollection onlineFiles) catch (Exception ex) { exportStep.SetFailed(ex.Message); - Logger.Log($"Beatmap set submission failed on export: {ex}"); - backButton.Enabled.Value = true; exportProgressNotification = null; + Logger.Log($"Beatmap set submission failed on export: {ex}"); + allowExit(); } exportStep.SetCompleted(); @@ -311,7 +303,7 @@ private async Task patchBeatmapSet(ICollection onlineFiles) { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total); @@ -339,7 +331,7 @@ private void replaceBeatmapSet() { uploadStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on upload: {ex}"); - backButton.Enabled.Value = true; + allowExit(); }; uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1)); @@ -354,8 +346,6 @@ private async Task updateLocalBeatmap() updateStep.SetInProgress(); - Live? importedSet; - try { importedSet = await beatmaps.ImportAsUpdate( @@ -367,28 +357,13 @@ private async Task updateLocalBeatmap() { updateStep.SetFailed(ex.Message); Logger.Log($"Beatmap submission failed on local update: {ex}"); - Schedule(() => backButton.Enabled.Value = true); + allowExit(); return; } updateStep.SetCompleted(); - backButton.Enabled.Value = true; - backButton.Action = () => - { - game?.PerformFromScreen(s => - { - if (s is OsuScreen osuScreen) - { - Debug.Assert(importedSet != null); - var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) - ?? importedSet.Value.Beatmaps.First(); - osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); - } - - s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); - }; showBeatmapCard(); + allowExit(); } private void showBeatmapCard() @@ -408,6 +383,11 @@ private void showBeatmapCard() api.Queue(getBeatmapSetRequest); } + private void allowExit() + { + BackButtonVisibility.Value = true; + } + protected override void Update() { base.Update(); @@ -419,6 +399,33 @@ protected override void Update() updateStep.SetInProgress(updateProgressNotification.Progress); } + public override bool OnExiting(ScreenExitEvent e) + { + // We probably want a method of cancelling in the future… + if (!BackButtonVisibility.Value) + return true; + + if (importedSet != null) + { + game?.PerformFromScreen(s => + { + if (s is OsuScreen osuScreen) + { + Debug.Assert(importedSet != null); + var targetBeatmap = importedSet.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == Beatmap.Value.BeatmapInfo.DifficultyName) + ?? importedSet.Value.Beatmaps.First(); + osuScreen.Beatmap.Value = beatmaps.GetWorkingBeatmap(targetBeatmap); + } + + s.Push(new EditorLoader()); + }, [typeof(MainMenu)]); + + return true; + } + + return base.OnExiting(e); + } + public override void OnEntering(ScreenTransitionEvent e) { base.OnEntering(e); From ce88ecfb3cbfb2df90663b6f7ac1d3b8021da22e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:39:01 +0900 Subject: [PATCH 10/19] Adjust timeouts to be much higher for upload requests It seems that right now these timeouts do not check for actual data movement, which is to say if a user with a very slow connection is uploading and it takes more than `Timeout`, their upload will fail. For now let's set these values high enough that most users will not be affected. --- osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs | 2 +- osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs index 5728dbe3faf4..df3c9d071ccd 100644 --- a/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/PatchBeatmapPackageRequest.cs @@ -46,7 +46,7 @@ protected override WebRequest CreateWebRequest() foreach (string filename in FilesDeleted) request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form); - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } diff --git a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs index 2e224ce6024f..de8af6a6238a 100644 --- a/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs +++ b/osu.Game/Online/API/Requests/ReplaceBeatmapPackageRequest.cs @@ -38,7 +38,7 @@ protected override WebRequest CreateWebRequest() var request = base.CreateWebRequest(); request.AddFile(@"beatmapArchive", oszPackage); request.Method = HttpMethod.Put; - request.Timeout = 60_000; + request.Timeout = 600_000; return request; } } From 753eae426d7c33978621025424b8dd43081a31fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 7 Feb 2025 18:42:36 +0900 Subject: [PATCH 11/19] Update strings --- .../Localisation/BeatmapSubmissionStrings.cs | 20 +++++++++---------- .../Submission/BeatmapSubmissionScreen.cs | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game/Localisation/BeatmapSubmissionStrings.cs b/osu.Game/Localisation/BeatmapSubmissionStrings.cs index 50b65ab57275..3abe8cc515d9 100644 --- a/osu.Game/Localisation/BeatmapSubmissionStrings.cs +++ b/osu.Game/Localisation/BeatmapSubmissionStrings.cs @@ -45,24 +45,24 @@ public static class BeatmapSubmissionStrings public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!"); /// - /// "Exporting beatmap set in compatibility mode..." + /// "Exporting beatmap for compatibility..." /// - public static LocalisableString ExportingBeatmapSet => new TranslatableString(getKey(@"exporting_beatmap_set"), @"Exporting beatmap set in compatibility mode..."); + public static LocalisableString Exporting => new TranslatableString(getKey(@"exporting"), @"Exporting beatmap for compatibility..."); /// - /// "Preparing beatmap set online..." + /// "Preparing for upload..." /// - public static LocalisableString PreparingBeatmapSet => new TranslatableString(getKey(@"preparing_beatmap_set"), @"Preparing beatmap set online..."); + public static LocalisableString Preparing => new TranslatableString(getKey(@"preparing"), @"Preparing for upload..."); /// - /// "Uploading beatmap set contents..." + /// "Uploading beatmap contents..." /// - public static LocalisableString UploadingBeatmapSetContents => new TranslatableString(getKey(@"uploading_beatmap_set_contents"), @"Uploading beatmap set contents..."); + public static LocalisableString Uploading => new TranslatableString(getKey(@"uploading"), @"Uploading beatmap contents..."); /// - /// "Updating local beatmap with relevant changes..." + /// "Finishing up..." /// - public static LocalisableString UpdatingLocalBeatmap => new TranslatableString(getKey(@"updating_local_beatmap"), @"Updating local beatmap with relevant changes..."); + public static LocalisableString Finishing => new TranslatableString(getKey(@"finishing"), @"Finishing up..."); /// /// "Before you continue, we ask you to check whether the content you are uploading has been cleared for upload. Please understand that you are responsible for the content you upload to the platform and if in doubt, should ask permission from the creators before uploading!" @@ -140,9 +140,9 @@ public static class BeatmapSubmissionStrings public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission"); /// - /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." + /// "Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost." /// - public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); + public static LocalisableString LegacyExportDisclaimer => new TranslatableString(getKey(@"legacy_export_disclaimer"), @"Note: In order to make it possible for users of all osu! versions to enjoy your beatmap, it will be exported in a backwards-compatible format. While we have made efforts to ensure that process keeps the beatmap playable in its intended form, some data related to features that previous versions of osu! do not support may be lost."); /// /// "Empty beatmaps cannot be submitted." diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 8536ba5f02d7..41c875ac1f2c 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -114,25 +114,25 @@ private void load() { createSetStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.PreparingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Preparing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, exportStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.ExportingBeatmapSet, + StageDescription = BeatmapSubmissionStrings.Exporting, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, uploadStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UploadingBeatmapSetContents, + StageDescription = BeatmapSubmissionStrings.Uploading, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, updateStep = new SubmissionStageProgress { - StageDescription = BeatmapSubmissionStrings.UpdatingLocalBeatmap, + StageDescription = BeatmapSubmissionStrings.Finishing, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, From de0aabbfc59963923637bc08edcc3c205a3e1f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:34:52 +0100 Subject: [PATCH 12/19] Add staging submission service URL to development endpoint config --- osu.Game/Online/DevelopmentEndpointConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index f4e1b257eede..e36e36ee9fc8 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -13,6 +13,7 @@ public DevelopmentEndpointConfiguration() SpectatorUrl = $@"{APIUrl}/signalr/spectator"; MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer"; MetadataUrl = $@"{APIUrl}/signalr/metadata"; + BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission"; } } } From 64f0d234d84222b00363397b43c9cda55c772a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 7 Feb 2025 15:37:27 +0100 Subject: [PATCH 13/19] Fix exiting being eternally blocked after successful beatmap submission --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 41c875ac1f2c..9dfe99813856 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -420,7 +420,7 @@ public override bool OnExiting(ScreenExitEvent e) s.Push(new EditorLoader()); }, [typeof(MainMenu)]); - return true; + return false; } return base.OnExiting(e); From 7853456c06abf8c7e46d233580b50cdf070f2efe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:12:59 +0900 Subject: [PATCH 14/19] Add delay before browser displays beatmap --- .../Submission/BeatmapSubmissionScreen.cs | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9dfe99813856..039c919ed6a1 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -290,15 +290,7 @@ private async Task patchBeatmapSet(ICollection onlineFiles) var patchRequest = new PatchBeatmapPackageRequest(beatmapSetId.Value); patchRequest.FilesChanged.AddRange(changedFiles); patchRequest.FilesDeleted.AddRange(onlineFilesByFilename.Keys); - patchRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + patchRequest.Success += uploadCompleted; patchRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -318,15 +310,7 @@ private void replaceBeatmapSet() var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray()); - uploadRequest.Success += async () => - { - uploadStep.SetCompleted(); - - if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) - game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); - - await updateLocalBeatmap().ConfigureAwait(true); - }; + uploadRequest.Success += uploadCompleted; uploadRequest.Failure += ex => { uploadStep.SetFailed(ex.Message); @@ -339,6 +323,12 @@ private void replaceBeatmapSet() uploadStep.SetInProgress(); } + private void uploadCompleted() + { + uploadStep.SetCompleted(); + updateLocalBeatmap().ConfigureAwait(true); + } + private async Task updateLocalBeatmap() { Debug.Assert(beatmapSetId != null); @@ -364,6 +354,12 @@ private async Task updateLocalBeatmap() updateStep.SetCompleted(); showBeatmapCard(); allowExit(); + + if (configManager.Get(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission)) + { + await Task.Delay(1000).ConfigureAwait(true); + game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}"); + } } private void showBeatmapCard() From 930aaecd7fc39a9455f3e56fe7baffe97b9dc360 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:22:31 +0900 Subject: [PATCH 15/19] Fix back button displaying before it should --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 039c919ed6a1..0967bcfc653c 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -39,6 +39,8 @@ public partial class BeatmapSubmissionScreen : OsuScreen public override bool DisallowExternalBeatmapRulesetChanges => true; + protected override bool InitialBackButtonVisibility => false; + [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); From eae1ea7e32484c03cd24b656c68c3138f4197b82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 15:23:25 +0900 Subject: [PATCH 16/19] Adjust animations and induce some short delays to make things more graceful --- .../Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 0967bcfc653c..121e25d8b7bc 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -92,6 +92,8 @@ private void load() { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + AutoSizeDuration = 400, + AutoSizeEasing = Easing.OutQuint, Alpha = 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -144,9 +146,6 @@ private void load() Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - AutoSizeDuration = 500, - AutoSizeEasing = Easing.OutQuint, - Masking = true, CornerRadius = BeatmapCard.CORNER_RADIUS, Child = flashLayer = new Container { @@ -252,6 +251,8 @@ private async Task createBeatmapPackage(ICollection onlineFiles) exportStep.SetCompleted(); exportProgressNotification = null; + await Task.Delay(200).ConfigureAwait(true); + if (onlineFiles.Count > 0) await patchBeatmapSet(onlineFiles).ConfigureAwait(true); else @@ -337,6 +338,7 @@ private async Task updateLocalBeatmap() Debug.Assert(beatmapPackageStream != null); updateStep.SetInProgress(); + await Task.Delay(200).ConfigureAwait(true); try { From 895493877cd0f04699099a4228657b05365c7b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:02:47 +0100 Subject: [PATCH 17/19] Allow performing beatmap reload after submission from song select --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 121e25d8b7bc..f53d10d23b0d 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -29,6 +29,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; using osuTK; namespace osu.Game.Screens.Edit.Submission @@ -418,7 +419,7 @@ public override bool OnExiting(ScreenExitEvent e) } s.Push(new EditorLoader()); - }, [typeof(MainMenu)]); + }, [typeof(SongSelect)]); return false; } From 45259b374a2fdd6626e06a7ed9c526cf28cd5fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Feb 2025 09:09:43 +0100 Subject: [PATCH 18/19] Remove unused using --- osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index f53d10d23b0d..9672e4360a03 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -28,7 +28,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; -using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osuTK; From b8e33a28d25c8590cf4d0b93e59deeaa21daa1d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Feb 2025 17:40:00 +0900 Subject: [PATCH 19/19] Minor code refactors --- .../Submission/BeatmapSubmissionScreen.cs | 19 ++++++++++------- .../Submission/SubmissionBeatmapExporter.cs | 21 +++++++------------ 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs index 9672e4360a03..201888e0784a 100644 --- a/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs +++ b/osu.Game/Screens/Edit/Submission/BeatmapSubmissionScreen.cs @@ -76,7 +76,6 @@ public partial class BeatmapSubmissionScreen : OsuScreen private uint? beatmapSetId; private MemoryStream? beatmapPackageStream; - private SubmissionBeatmapExporter legacyBeatmapExporter = null!; private ProgressNotification? exportProgressNotification; private ProgressNotification? updateProgressNotification; @@ -214,8 +213,7 @@ await realmAccess.WriteAsync(r => }).ConfigureAwait(true); } - legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); - await createBeatmapPackage(response.Files).ConfigureAwait(true); + await createBeatmapPackage(response).ConfigureAwait(true); }; createRequest.Failure += ex => { @@ -228,7 +226,7 @@ await realmAccess.WriteAsync(r => api.Queue(createRequest); } - private async Task createBeatmapPackage(ICollection onlineFiles) + private async Task createBeatmapPackage(PutBeatmapSetResponse response) { Debug.Assert(ThreadSafety.IsUpdateThread); @@ -237,8 +235,13 @@ private async Task createBeatmapPackage(ICollection onlineFiles) try { beatmapPackageStream = new MemoryStream(); - await legacyBeatmapExporter.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification = new ProgressNotification()) - .ConfigureAwait(true); + exportProgressNotification = new ProgressNotification(); + + var legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response); + + await legacyBeatmapExporter + .ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification) + .ConfigureAwait(true); } catch (Exception ex) { @@ -253,8 +256,8 @@ private async Task createBeatmapPackage(ICollection onlineFiles) await Task.Delay(200).ConfigureAwait(true); - if (onlineFiles.Count > 0) - await patchBeatmapSet(onlineFiles).ConfigureAwait(true); + if (response.Files.Count > 0) + await patchBeatmapSet(response.Files).ConfigureAwait(true); else replaceBeatmapSet(); } diff --git a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs index 3c50a1bf80d0..fab080cdba6e 100644 --- a/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs +++ b/osu.Game/Screens/Edit/Submission/SubmissionBeatmapExporter.cs @@ -14,43 +14,38 @@ namespace osu.Game.Screens.Edit.Submission public class SubmissionBeatmapExporter : LegacyBeatmapExporter { private readonly uint? beatmapSetId; - private readonly HashSet? beatmapIds; - - public SubmissionBeatmapExporter(Storage storage) - : base(storage) - { - } + private readonly HashSet? allocatedBeatmapIds; public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse) : base(storage) { beatmapSetId = putBeatmapSetResponse.BeatmapSetId; - beatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); + allocatedBeatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet(); } protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap) { base.MutateBeatmap(beatmapSet, playableBeatmap); - if (beatmapSetId != null && beatmapIds != null) + if (beatmapSetId != null && allocatedBeatmapIds != null) { playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet; playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId; - if (beatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) + if (allocatedBeatmapIds.Contains(playableBeatmap.BeatmapInfo.OnlineID)) { - beatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); + allocatedBeatmapIds.Remove(playableBeatmap.BeatmapInfo.OnlineID); return; } if (playableBeatmap.BeatmapInfo.OnlineID > 0) throw new InvalidOperationException(@"Encountered beatmap with ID that has not been assigned to it by the server!"); - if (beatmapIds.Count == 0) + if (allocatedBeatmapIds.Count == 0) throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!"); - int newId = beatmapIds.First(); - beatmapIds.Remove(newId); + int newId = allocatedBeatmapIds.First(); + allocatedBeatmapIds.Remove(newId); playableBeatmap.BeatmapInfo.OnlineID = newId; } }