1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-25 18:57:18 +08:00

Merge pull request #31805 from bdach/bss/the-actual-submission

Add initial beatmap submission support
This commit is contained in:
Dean Herbert 2025-02-13 17:32:09 +09:00 committed by GitHub
commit 37db539226
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 657 additions and 30 deletions

View File

@ -24,7 +24,11 @@ namespace osu.Game.Tests.Visual.Editing
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,

View File

@ -61,6 +61,20 @@ namespace osu.Game.Database
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 @@ namespace osu.Game.Database
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";

View File

@ -39,6 +39,31 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString SubmissionSettings => new TranslatableString(getKey(@"submission_settings"), @"Submission settings");
/// <summary>
/// "Submit beatmap!"
/// </summary>
public static LocalisableString ConfirmSubmission => new TranslatableString(getKey(@"confirm_submission"), @"Submit beatmap!");
/// <summary>
/// "Exporting beatmap for compatibility..."
/// </summary>
public static LocalisableString Exporting => new TranslatableString(getKey(@"exporting"), @"Exporting beatmap for compatibility...");
/// <summary>
/// "Preparing for upload..."
/// </summary>
public static LocalisableString Preparing => new TranslatableString(getKey(@"preparing"), @"Preparing for upload...");
/// <summary>
/// "Uploading beatmap contents..."
/// </summary>
public static LocalisableString Uploading => new TranslatableString(getKey(@"uploading"), @"Uploading beatmap contents...");
/// <summary>
/// "Finishing up..."
/// </summary>
public static LocalisableString Finishing => new TranslatableString(getKey(@"finishing"), @"Finishing up...");
/// <summary>
/// "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!"
/// </summary>
@ -115,9 +140,24 @@ namespace osu.Game.Localisation
public static LocalisableString LoadInBrowserAfterSubmission => new TranslatableString(getKey(@"load_in_browser_after_submission"), @"Load in browser after submission");
/// <summary>
/// "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."
/// </summary>
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.");
/// <summary>
/// "Empty beatmaps cannot be submitted."
/// </summary>
public static LocalisableString EmptyBeatmapsCannotBeSubmitted => new TranslatableString(getKey(@"empty_beatmaps_cannot_be_submitted"), @"Empty beatmaps cannot be submitted.");
/// <summary>
/// "Update beatmap!"
/// </summary>
public static LocalisableString UpdateBeatmap => new TranslatableString(getKey(@"update_beatmap"), @"Update beatmap!");
/// <summary>
/// "Upload NEW beatmap!"
/// </summary>
public static LocalisableString UploadNewBeatmap => new TranslatableString(getKey(@"upload_new_beatmap"), @"Upload NEW beatmap!");
private static string getKey(string key) => $@"{prefix}:{key}";
}

View File

@ -69,6 +69,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString DeleteDifficulty => new TranslatableString(getKey(@"delete_difficulty"), @"Delete difficulty");
/// <summary>
/// "Edit externally"
/// </summary>
public static LocalisableString EditExternally => new TranslatableString(getKey(@"edit_externally"), @"Edit externally");
/// <summary>
/// "Submit beatmap"
/// </summary>
public static LocalisableString SubmitBeatmap => new TranslatableString(getKey(@"submit_beatmap"), @"Submit beatmap");
/// <summary>
/// "setup"
/// </summary>

View File

@ -26,10 +26,8 @@ namespace osu.Game.Online.API.Requests
public uint BeatmapSetID { get; }
// ReSharper disable once CollectionNeverUpdated.Global
public Dictionary<string, byte[]> FilesChanged { get; } = new Dictionary<string, byte[]>();
// ReSharper disable once CollectionNeverUpdated.Global
public HashSet<string> FilesDeleted { get; } = new HashSet<string>();
public PatchBeatmapPackageRequest(uint beatmapSetId)
@ -48,7 +46,7 @@ namespace osu.Game.Online.API.Requests
foreach (string filename in FilesDeleted)
request.AddParameter(@"filesDeleted", filename, RequestParameterType.Form);
request.Timeout = 60_000;
request.Timeout = 600_000;
return request;
}
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Online.API.Requests
var request = base.CreateWebRequest();
request.AddFile(@"beatmapArchive", oszPackage);
request.Method = HttpMethod.Put;
request.Timeout = 60_000;
request.Timeout = 600_000;
return request;
}
}

View File

@ -13,6 +13,7 @@ namespace osu.Game.Online
SpectatorUrl = $@"{APIUrl}/signalr/spectator";
MultiplayerUrl = $@"{APIUrl}/signalr/multiplayer";
MetadataUrl = $@"{APIUrl}/signalr/metadata";
BeatmapSubmissionServiceUrl = $@"{APIUrl}/beatmap-submission";
}
}
}

View File

@ -45,6 +45,8 @@ namespace osu.Game.Overlays
private LoadingSpinner loading = null!;
private ScheduledDelegate? loadingShowDelegate;
public bool Completed { get; private set; }
protected WizardOverlay(OverlayColourScheme scheme)
: base(scheme)
{
@ -221,6 +223,7 @@ namespace osu.Game.Overlays
else
{
CurrentStepIndex = null;
Completed = true;
Hide();
}

View File

@ -32,6 +32,7 @@ using osu.Game.Beatmaps;
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.Compose.Components.Timeline;
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 @@ namespace osu.Game.Screens.Edit
[Resolved(canBeNull: true)]
private INotificationOverlay notifications { get; set; }
[Resolved(canBeNull: true)]
[CanBeNull]
private LoginOverlay loginOverlay { get; set; }
[Resolved]
private RealmAccess realm { get; set; }
@ -1251,11 +1257,22 @@ namespace osu.Game.Screens.Edit
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.Endpoints.BeatmapSubmissionServiceUrl != null;
if (isSetMadeOfLegacyRulesetBeatmaps && submissionAvailable)
{
var upload = new EditorMenuItem(EditorStrings.SubmitBeatmap, MenuItemType.Standard, submitBeatmap);
saveRelatedMenuItems.Add(upload);
yield return upload;
}
if (editorBeatmap.BeatmapInfo.OnlineID > 0)
{
yield return new OsuMenuItemSpacer();
@ -1304,6 +1321,42 @@ namespace osu.Game.Screens.Edit
}
}
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)

View File

@ -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 @@ namespace osu.Game.Screens.Edit.Submission
}
[BackgroundDependencyLoader]
private void load()
private void load(IBindable<WorkingBeatmap> beatmap)
{
AddStep<ScreenContentPermissions>();
AddStep<ScreenFrequentlyAskedQuestions>();
if (beatmap.Value.BeatmapSetInfo.OnlineID <= 0)
{
AddStep<ScreenContentPermissions>();
AddStep<ScreenFrequentlyAskedQuestions>();
}
AddStep<ScreenSubmissionSettings>();
Header.Title = BeatmapSubmissionStrings.BeatmapSubmissionTitle;

View File

@ -0,0 +1,446 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.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.Select;
using osuTK;
namespace osu.Game.Screens.Edit.Submission
{
public partial class BeatmapSubmissionScreen : OsuScreen
{
private BeatmapSubmissionOverlay overlay = null!;
public override bool DisallowExternalBeatmapRulesetChanges => true;
protected override bool InitialBackButtonVisibility => 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 uint? beatmapSetId;
private MemoryStream? beatmapPackageStream;
private ProgressNotification? exportProgressNotification;
private ProgressNotification? updateProgressNotification;
private Live<BeatmapSetInfo>? importedSet;
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
overlay = new BeatmapSubmissionOverlay(),
submissionProgress = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
AutoSizeDuration = 400,
AutoSizeEasing = Easing.OutQuint,
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.Preparing,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
exportStep = new SubmissionStageProgress
{
StageDescription = BeatmapSubmissionStrings.Exporting,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
uploadStep = new SubmissionStageProgress
{
StageDescription = BeatmapSubmissionStrings.Uploading,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
updateStep = new SubmissionStageProgress
{
StageDescription = BeatmapSubmissionStrings.Finishing,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
successContainer = new Container
{
Padding = new MarginPadding(20),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
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,
}
}
},
}
}
}
}
});
overlay.State.BindValueChanged(_ =>
{
if (overlay.State.Value == Visibility.Hidden)
{
if (!overlay.Completed)
{
allowExit();
this.Exit();
}
else
{
submissionProgress.FadeIn(200, Easing.OutQuint);
createBeatmapSet();
}
}
});
}
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<BeatmapSetInfo>(Beatmap.Value.BeatmapSetInfo.ID);
refetchedSet!.OnlineID = (int)beatmapSetId.Value;
}).ConfigureAwait(true);
}
await createBeatmapPackage(response).ConfigureAwait(true);
};
createRequest.Failure += ex =>
{
createSetStep.SetFailed(ex.Message);
Logger.Log($"Beatmap set submission failed on creation: {ex}");
allowExit();
};
createSetStep.SetInProgress();
api.Queue(createRequest);
}
private async Task createBeatmapPackage(PutBeatmapSetResponse response)
{
Debug.Assert(ThreadSafety.IsUpdateThread);
exportStep.SetInProgress();
try
{
beatmapPackageStream = new MemoryStream();
exportProgressNotification = new ProgressNotification();
var legacyBeatmapExporter = new SubmissionBeatmapExporter(storage, response);
await legacyBeatmapExporter
.ExportToStreamAsync(Beatmap.Value.BeatmapSetInfo.ToLive(realmAccess), beatmapPackageStream, exportProgressNotification)
.ConfigureAwait(true);
}
catch (Exception ex)
{
exportStep.SetFailed(ex.Message);
exportProgressNotification = null;
Logger.Log($"Beatmap set submission failed on export: {ex}");
allowExit();
}
exportStep.SetCompleted();
exportProgressNotification = null;
await Task.Delay(200).ConfigureAwait(true);
if (response.Files.Count > 0)
await patchBeatmapSet(response.Files).ConfigureAwait(true);
else
replaceBeatmapSet();
}
private async Task patchBeatmapSet(ICollection<BeatmapSetFile> onlineFiles)
{
Debug.Assert(beatmapSetId != null);
Debug.Assert(beatmapPackageStream != 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<string>();
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<string, byte[]>();
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 += uploadCompleted;
patchRequest.Failure += ex =>
{
uploadStep.SetFailed(ex.Message);
Logger.Log($"Beatmap submission failed on upload: {ex}");
allowExit();
};
patchRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / total);
api.Queue(patchRequest);
uploadStep.SetInProgress();
}
private void replaceBeatmapSet()
{
Debug.Assert(beatmapSetId != null);
Debug.Assert(beatmapPackageStream != null);
var uploadRequest = new ReplaceBeatmapPackageRequest(beatmapSetId.Value, beatmapPackageStream.ToArray());
uploadRequest.Success += uploadCompleted;
uploadRequest.Failure += ex =>
{
uploadStep.SetFailed(ex.Message);
Logger.Log($"Beatmap submission failed on upload: {ex}");
allowExit();
};
uploadRequest.Progressed += (current, total) => uploadStep.SetInProgress((float)current / Math.Max(total, 1));
api.Queue(uploadRequest);
uploadStep.SetInProgress();
}
private void uploadCompleted()
{
uploadStep.SetCompleted();
updateLocalBeatmap().ConfigureAwait(true);
}
private async Task updateLocalBeatmap()
{
Debug.Assert(beatmapSetId != null);
Debug.Assert(beatmapPackageStream != null);
updateStep.SetInProgress();
await Task.Delay(200).ConfigureAwait(true);
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}");
allowExit();
return;
}
updateStep.SetCompleted();
showBeatmapCard();
allowExit();
if (configManager.Get<bool>(OsuSetting.EditorSubmissionLoadInBrowserAfterSubmission))
{
await Task.Delay(1000).ConfigureAwait(true);
game?.OpenUrlExternally($"{api.Endpoints.WebsiteUrl}/beatmapsets/{beatmapSetId}");
}
}
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);
}
private void allowExit()
{
BackButtonVisibility.Value = true;
}
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 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(SongSelect)]);
return false;
}
return base.OnExiting(e);
}
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
overlay.Show();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
beatmapPackageStream?.Dispose();
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<BeatmapSubmissionTarget> Target { get; } = new Bindable<BeatmapSubmissionTarget>();
}
}

View File

@ -11,6 +11,7 @@ using osu.Game.Graphics;
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 @@ namespace osu.Game.Screens.Edit.Submission
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 @@ namespace osu.Game.Screens.Edit.Submission
{
RelativeSizeAxes = Axes.X,
Caption = BeatmapSubmissionStrings.BeatmapSubmissionTargetCaption,
Current = settings.Target,
},
new FormCheckBox
{
@ -60,14 +64,5 @@ namespace osu.Game.Screens.Edit.Submission
}
});
}
private enum BeatmapSubmissionTarget
{
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetWIP))]
WIP,
[LocalisableDescription(typeof(BeatmapSubmissionStrings), nameof(BeatmapSubmissionStrings.BeatmapSubmissionTargetPending))]
Pending,
}
}
}

View File

@ -0,0 +1,53 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<int>? allocatedBeatmapIds;
public SubmissionBeatmapExporter(Storage storage, PutBeatmapSetResponse putBeatmapSetResponse)
: base(storage)
{
beatmapSetId = putBeatmapSetResponse.BeatmapSetId;
allocatedBeatmapIds = putBeatmapSetResponse.BeatmapIds.Select(id => (int)id).ToHashSet();
}
protected override void MutateBeatmap(BeatmapSetInfo beatmapSet, IBeatmap playableBeatmap)
{
base.MutateBeatmap(beatmapSet, playableBeatmap);
if (beatmapSetId != null && allocatedBeatmapIds != null)
{
playableBeatmap.BeatmapInfo.BeatmapSet = beatmapSet;
playableBeatmap.BeatmapInfo.BeatmapSet!.OnlineID = (int)beatmapSetId;
if (allocatedBeatmapIds.Contains(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 (allocatedBeatmapIds.Count == 0)
throw new InvalidOperationException(@"Ran out of new beatmap IDs to assign to unsubmitted beatmaps!");
int newId = allocatedBeatmapIds.First();
allocatedBeatmapIds.Remove(newId);
playableBeatmap.BeatmapInfo.OnlineID = newId;
}
}
}
}