mirror of
https://github.com/ppy/osu
synced 2024-12-28 18:02:53 +00:00
Merge pull request #24186 from OliBomby/legacy-export
Add ability to export beatmaps from editor in a stable-compatible format
This commit is contained in:
commit
4bf300d64d
@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
public class BeatmapImporter : RealmArchiveModelImporter<BeatmapSetInfo>
|
||||
{
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osz" };
|
||||
public override IEnumerable<string> HandledExtensions => new[] { ".osz", ".olz" };
|
||||
|
||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||
|
||||
@ -145,7 +145,7 @@ namespace osu.Game.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
|
||||
protected override bool ShouldDeleteArchive(string path) => HandledExtensions.Contains(Path.GetExtension(path).ToLowerInvariant());
|
||||
|
||||
protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
|
||||
{
|
||||
|
@ -40,7 +40,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
private readonly WorkingBeatmapCache workingBeatmapCache;
|
||||
|
||||
private readonly LegacyBeatmapExporter beatmapExporter;
|
||||
private readonly BeatmapExporter beatmapExporter;
|
||||
|
||||
private readonly LegacyBeatmapExporter legacyBeatmapExporter;
|
||||
|
||||
public ProcessBeatmapDelegate? ProcessBeatmap { private get; set; }
|
||||
|
||||
@ -77,7 +79,12 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
|
||||
|
||||
beatmapExporter = new LegacyBeatmapExporter(storage)
|
||||
beatmapExporter = new BeatmapExporter(storage)
|
||||
{
|
||||
PostNotification = obj => PostNotification?.Invoke(obj)
|
||||
};
|
||||
|
||||
legacyBeatmapExporter = new LegacyBeatmapExporter(storage)
|
||||
{
|
||||
PostNotification = obj => PostNotification?.Invoke(obj)
|
||||
};
|
||||
@ -402,6 +409,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm));
|
||||
|
||||
public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm));
|
||||
|
||||
private void updateHashAndMarkDirty(BeatmapSetInfo setInfo)
|
||||
{
|
||||
setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
|
||||
|
22
osu.Game/Database/BeatmapExporter.cs
Normal file
22
osu.Game/Database/BeatmapExporter.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// 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.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Exporter for beatmap archives.
|
||||
/// This is not for legacy purposes and works for lazer only.
|
||||
/// </summary>
|
||||
public class BeatmapExporter : LegacyArchiveExporter<BeatmapSetInfo>
|
||||
{
|
||||
public BeatmapExporter(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string FileExtension => @".olz";
|
||||
}
|
||||
}
|
@ -39,7 +39,7 @@ namespace osu.Game.Database
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var stream = UserFileStorage.GetStream(file.File.GetStoragePath()))
|
||||
using (var stream = GetFileContents(model, file))
|
||||
{
|
||||
if (stream == null)
|
||||
{
|
||||
@ -65,5 +65,7 @@ namespace osu.Game.Database
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Stream? GetFileContents(TModel model, INamedFileUsage file) => UserFileStorage.GetStream(file.File.GetStoragePath());
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,25 @@
|
||||
// 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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Exporter for osu!stable legacy beatmap archives.
|
||||
/// Converts all beatmaps in the set to legacy format and exports it as a legacy package.
|
||||
/// </summary>
|
||||
public class LegacyBeatmapExporter : LegacyArchiveExporter<BeatmapSetInfo>
|
||||
{
|
||||
public LegacyBeatmapExporter(Storage storage)
|
||||
@ -13,6 +27,72 @@ namespace osu.Game.Database
|
||||
{
|
||||
}
|
||||
|
||||
protected override Stream? GetFileContents(BeatmapSetInfo model, INamedFileUsage file)
|
||||
{
|
||||
bool isBeatmap = model.Beatmaps.Any(o => o.Hash == file.File.Hash);
|
||||
|
||||
if (!isBeatmap)
|
||||
return base.GetFileContents(model, file);
|
||||
|
||||
// Read the beatmap contents and skin
|
||||
using var contentStream = base.GetFileContents(model, file);
|
||||
|
||||
if (contentStream == null)
|
||||
return null;
|
||||
|
||||
using var contentStreamReader = new LineBufferedReader(contentStream);
|
||||
var beatmapContent = new LegacyBeatmapDecoder().Decode(contentStreamReader);
|
||||
|
||||
using var skinStream = base.GetFileContents(model, file);
|
||||
|
||||
if (skinStream == null)
|
||||
return null;
|
||||
|
||||
using var skinStreamReader = new LineBufferedReader(skinStream);
|
||||
var beatmapSkin = new LegacySkin(new SkinInfo(), null!)
|
||||
{
|
||||
Configuration = new LegacySkinDecoder().Decode(skinStreamReader)
|
||||
};
|
||||
|
||||
// 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 bezier curves
|
||||
foreach (var controlPoint in beatmapContent.ControlPointInfo.AllControlPoints)
|
||||
controlPoint.Time = Math.Floor(controlPoint.Time);
|
||||
|
||||
foreach (var hitObject in beatmapContent.HitObjects)
|
||||
{
|
||||
// Truncate end time before truncating start time because end time is dependent on start time
|
||||
if (hitObject is IHasDuration hasDuration && hitObject is not IHasPath)
|
||||
hasDuration.Duration = Math.Floor(hasDuration.EndTime) - Math.Floor(hitObject.StartTime);
|
||||
|
||||
hitObject.StartTime = Math.Floor(hitObject.StartTime);
|
||||
|
||||
if (hitObject is not IHasPath hasPath || BezierConverter.CountSegments(hasPath.Path.ControlPoints) <= 1) continue;
|
||||
|
||||
var newControlPoints = BezierConverter.ConvertToModernBezier(hasPath.Path.ControlPoints);
|
||||
|
||||
// Truncate control points to integer positions
|
||||
foreach (var pathControlPoint in newControlPoints)
|
||||
{
|
||||
pathControlPoint.Position = new Vector2(
|
||||
(float)Math.Floor(pathControlPoint.Position.X),
|
||||
(float)Math.Floor(pathControlPoint.Position.Y));
|
||||
}
|
||||
|
||||
hasPath.Path.ControlPoints.Clear();
|
||||
hasPath.Path.ControlPoints.AddRange(newControlPoints);
|
||||
}
|
||||
|
||||
// Encode to legacy format
|
||||
var stream = new MemoryStream();
|
||||
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
|
||||
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
|
||||
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
return stream;
|
||||
}
|
||||
|
||||
protected override string FileExtension => @".osz";
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Default => new TranslatableString(getKey(@"default"), @"Default");
|
||||
|
||||
/// <summary>
|
||||
/// "Export"
|
||||
/// </summary>
|
||||
public static LocalisableString Export => new TranslatableString(getKey(@"export"), @"Export");
|
||||
|
||||
/// <summary>
|
||||
/// "Width"
|
||||
/// </summary>
|
||||
|
@ -35,9 +35,14 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString SetPreviewPointToCurrent => new TranslatableString(getKey(@"set_preview_point_to_current"), @"Set preview point to current time");
|
||||
|
||||
/// <summary>
|
||||
/// "Export package"
|
||||
/// "For editing (.olz)"
|
||||
/// </summary>
|
||||
public static LocalisableString ExportPackage => new TranslatableString(getKey(@"export_package"), @"Export package");
|
||||
public static LocalisableString ExportForEditing => new TranslatableString(getKey(@"export_for_editing"), @"For editing (.olz)");
|
||||
|
||||
/// <summary>
|
||||
/// "For compatibility (.osz)"
|
||||
/// </summary>
|
||||
public static LocalisableString ExportForCompatibility => new TranslatableString(getKey(@"export_for_compatibility"), @"For compatibility (.osz)");
|
||||
|
||||
/// <summary>
|
||||
/// "Create new difficulty"
|
||||
|
@ -425,7 +425,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
if (Score.Files.Count > 0)
|
||||
{
|
||||
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score)));
|
||||
items.Add(new OsuMenuItem(Localisation.CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(Score)));
|
||||
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,13 @@ namespace osu.Game.Rulesets.Objects
|
||||
new[] { new Vector2d(1, 0), new Vector2d(1, 1.2447058f), new Vector2d(-0.8526471f, 2.118367f), new Vector2d(-2.6211002f, 7.854936e-06f), new Vector2d(-0.8526448f, -2.118357f), new Vector2d(1, -1.2447058f), new Vector2d(1, 0) })
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Counts the number of segments in a slider path.
|
||||
/// </summary>
|
||||
/// <param name="controlPoints">The control points of the path.</param>
|
||||
/// <returns>The number of segments in a slider path.</returns>
|
||||
public static int CountSegments(IList<PathControlPoint> controlPoints) => controlPoints.Where((t, i) => t.Type != null && i < controlPoints.Count - 1).Count();
|
||||
|
||||
/// <summary>
|
||||
/// Converts a slider path to bezier control point positions compatible with the legacy osu! client.
|
||||
/// </summary>
|
||||
|
@ -997,23 +997,40 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private List<MenuItem> createFileMenuItems() => new List<MenuItem>
|
||||
{
|
||||
new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
|
||||
new EditorMenuItem(EditorStrings.ExportPackage, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
new EditorMenuItemSpacer(),
|
||||
createDifficultyCreationMenu(),
|
||||
createDifficultySwitchMenu(),
|
||||
new EditorMenuItemSpacer(),
|
||||
new EditorMenuItem(EditorStrings.DeleteDifficulty, MenuItemType.Standard, deleteDifficulty) { Action = { Disabled = Beatmap.Value.BeatmapSetInfo.Beatmaps.Count < 2 } },
|
||||
new EditorMenuItemSpacer(),
|
||||
new EditorMenuItem(WebCommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
|
||||
createExportMenu(),
|
||||
new EditorMenuItemSpacer(),
|
||||
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, this.Exit)
|
||||
};
|
||||
|
||||
private EditorMenuItem createExportMenu()
|
||||
{
|
||||
var exportItems = new List<MenuItem>
|
||||
{
|
||||
new EditorMenuItem(EditorStrings.ExportForEditing, MenuItemType.Standard, exportBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
new EditorMenuItem(EditorStrings.ExportForCompatibility, MenuItemType.Standard, exportLegacyBeatmap) { Action = { Disabled = !RuntimeInfo.IsDesktop } },
|
||||
};
|
||||
|
||||
return new EditorMenuItem(CommonStrings.Export) { Items = exportItems };
|
||||
}
|
||||
|
||||
private void exportBeatmap()
|
||||
{
|
||||
Save();
|
||||
beatmapManager.Export(Beatmap.Value.BeatmapSetInfo);
|
||||
}
|
||||
|
||||
private void exportLegacyBeatmap()
|
||||
{
|
||||
Save();
|
||||
beatmapManager.ExportLegacy(Beatmap.Value.BeatmapSetInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Beatmaps of the currently edited set, grouped by ruleset and ordered by difficulty.
|
||||
/// </summary>
|
||||
|
Loading…
Reference in New Issue
Block a user