mirror of
https://github.com/ppy/osu.git
synced 2025-01-18 11:02:57 +08:00
Merge branch 'master' into filter-playing-rooms
This commit is contained in:
commit
d8d5a00f3b
2
.github/workflows/diffcalc.yml
vendored
2
.github/workflows/diffcalc.yml
vendored
@ -115,7 +115,7 @@ jobs:
|
||||
steps:
|
||||
- name: Check permissions
|
||||
run: |
|
||||
ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte)
|
||||
ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte tsunyoku stanriders)
|
||||
for i in "${ALLOWED_USERS[@]}"; do
|
||||
if [[ "${{ github.actor }}" == "$i" ]]; then
|
||||
exit 0
|
||||
|
@ -10,10 +10,12 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -54,6 +56,12 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
[Resolved]
|
||||
private EditorBeatmap? editorBeatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IEditorChangeHandler? changeHandler { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BindableBeatDivisor? beatDivisor { get; set; }
|
||||
|
||||
public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
@ -119,6 +127,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
if (!IsSelected)
|
||||
return false;
|
||||
|
||||
if (e.Key == Key.F && e.ControlPressed && e.ShiftPressed)
|
||||
{
|
||||
convertToStream();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void onDefaultsApplied(HitObject _)
|
||||
{
|
||||
computeObjectBounds();
|
||||
@ -168,6 +190,50 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
lastSliderPathVersion = HitObject.Path.Version.Value;
|
||||
}
|
||||
|
||||
// duplicated in `SliderSelectionBlueprint.convertToStream()`
|
||||
// consider extracting common helper when applying changes here
|
||||
private void convertToStream()
|
||||
{
|
||||
if (editorBeatmap == null || beatDivisor == null)
|
||||
return;
|
||||
|
||||
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
|
||||
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
int i = 0;
|
||||
double time = HitObject.StartTime;
|
||||
|
||||
while (!Precision.DefinitelyBigger(time, HitObject.GetEndTime(), 1))
|
||||
{
|
||||
// positionWithRepeats is a fractional number in the range of [0, HitObject.SpanCount()]
|
||||
// and indicates how many fractional spans of a slider have passed up to time.
|
||||
double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount();
|
||||
double pathPosition = positionWithRepeats - (int)positionWithRepeats;
|
||||
// every second span is in the reverse direction - need to reverse the path position.
|
||||
if (positionWithRepeats % 2 >= 1)
|
||||
pathPosition = 1 - pathPosition;
|
||||
|
||||
float fruitXValue = HitObject.OriginalX + HitObject.Path.PositionAt(pathPosition).X;
|
||||
|
||||
editorBeatmap.Add(new Fruit
|
||||
{
|
||||
StartTime = time,
|
||||
OriginalX = fruitXValue,
|
||||
NewCombo = i == 0 && HitObject.NewCombo,
|
||||
Samples = HitObject.Samples.Select(s => s.With()).ToList()
|
||||
});
|
||||
|
||||
i += 1;
|
||||
time = HitObject.StartTime + i * streamSpacing;
|
||||
}
|
||||
|
||||
editorBeatmap.Remove(HitObject);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
private IEnumerable<MenuItem> getContextMenuItems()
|
||||
{
|
||||
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
|
||||
@ -177,6 +243,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
|
||||
};
|
||||
|
||||
yield return new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream)
|
||||
{
|
||||
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F))
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -551,6 +551,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
HitObject.Position += first;
|
||||
}
|
||||
|
||||
// duplicated in `JuiceStreamSelectionBlueprint.convertToStream()`
|
||||
// consider extracting common helper when applying changes here
|
||||
private void convertToStream()
|
||||
{
|
||||
if (editorBeatmap == null || beatDivisor == null)
|
||||
|
@ -57,11 +57,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const string base_lookup = @"hitcircle";
|
||||
|
||||
var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
|
||||
|
||||
// As a precondition, ensure that any prefix lookups are run against the skin which is providing "hitcircle".
|
||||
// This is to correctly handle a case such as:
|
||||
//
|
||||
// - Beatmap provides `hitcircle`
|
||||
// - User skin provides `sliderstartcircle`
|
||||
//
|
||||
// In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override.
|
||||
var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin;
|
||||
|
||||
// if a base texture for the specified prefix exists, continue using it for subsequent lookups.
|
||||
// otherwise fall back to the default prefix "hitcircle".
|
||||
string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle";
|
||||
string circleName = (priorityLookupPrefix != null && provider.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : base_lookup;
|
||||
|
||||
Vector2 maxSize = OsuHitObject.OBJECT_DIMENSIONS * 2;
|
||||
|
||||
@ -70,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
||||
InternalChildren = new[]
|
||||
{
|
||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) })
|
||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(circleName)?.WithMaximumSize(maxSize) })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -79,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
|
||||
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -109,9 +109,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
95901, 106450, 116999, 119637, 130186, 140735, 151285,
|
||||
161834, 164471, 175020, 185570, 196119, 206669, 209306
|
||||
};
|
||||
Assert.AreEqual(expectedBookmarks.Length, beatmap.BeatmapInfo.Bookmarks.Length);
|
||||
Assert.AreEqual(expectedBookmarks.Length, beatmap.Bookmarks.Length);
|
||||
for (int i = 0; i < expectedBookmarks.Length; i++)
|
||||
Assert.AreEqual(expectedBookmarks[i], beatmap.BeatmapInfo.Bookmarks[i]);
|
||||
Assert.AreEqual(expectedBookmarks[i], beatmap.Bookmarks[i]);
|
||||
Assert.AreEqual(1.8, beatmap.DistanceSpacing);
|
||||
Assert.AreEqual(4, beatmap.BeatmapInfo.BeatDivisor);
|
||||
Assert.AreEqual(4, beatmap.GridSize);
|
||||
|
@ -73,9 +73,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
95901, 106450, 116999, 119637, 130186, 140735, 151285,
|
||||
161834, 164471, 175020, 185570, 196119, 206669, 209306
|
||||
};
|
||||
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length);
|
||||
Assert.AreEqual(expectedBookmarks.Length, beatmap.Bookmarks.Length);
|
||||
for (int i = 0; i < expectedBookmarks.Length; i++)
|
||||
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]);
|
||||
Assert.AreEqual(expectedBookmarks[i], beatmap.Bookmarks[i]);
|
||||
Assert.AreEqual(1.8, beatmap.DistanceSpacing);
|
||||
Assert.AreEqual(4, beatmapInfo.BeatDivisor);
|
||||
Assert.AreEqual(4, beatmap.GridSize);
|
||||
|
@ -49,17 +49,17 @@ namespace osu.Game.Tests.Visual.Background
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
BeatmapStore beatmapStore;
|
||||
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(new OsuConfigManager(LocalStorage));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
Add(beatmapStore);
|
||||
|
||||
Beatmap.SetDefault();
|
||||
}
|
||||
|
@ -4,11 +4,13 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -28,6 +30,7 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
@ -99,44 +102,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("enter compose mode", () => InputManager.Key(Key.F1));
|
||||
AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType<Timeline>().FirstOrDefault()?.IsLoaded == true);
|
||||
|
||||
AddStep("enter setup mode", () => InputManager.Key(Key.F4));
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
|
||||
AddAssert("switch track to real track", () =>
|
||||
{
|
||||
var setup = Editor.ChildrenOfType<SetupScreen>().First();
|
||||
|
||||
string temp = TestResources.GetTestBeatmapForImport();
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
Directory.CreateDirectory(extractedFolder);
|
||||
|
||||
try
|
||||
{
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(extractedFolder);
|
||||
|
||||
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")));
|
||||
|
||||
// ensure audio file is copied to beatmap as "audio.mp3" rather than original filename.
|
||||
Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3");
|
||||
|
||||
return success;
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(temp);
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
});
|
||||
AddAssert("switch track to real track", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3"));
|
||||
|
||||
AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
|
||||
AddUntilStep("track length changed", () => Beatmap.Value.Track.Length > 60000);
|
||||
|
||||
AddStep("test play", () => Editor.TestGameplay());
|
||||
|
||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
|
||||
AddStep("confirm save", () => InputManager.Key(Key.Number1));
|
||||
|
||||
AddUntilStep("wait for return to editor", () => Editor.IsCurrentScreen());
|
||||
|
||||
AddAssert("track is still not virtual", () => Beatmap.Value.Track is not TrackVirtual);
|
||||
@ -635,5 +609,228 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return set != null && set.PerformRead(s => s.Beatmaps.Count == 3 && s.Files.Count == 3);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleBackgroundFile()
|
||||
{
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddAssert("set background", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg"));
|
||||
|
||||
createNewDifficulty();
|
||||
createNewDifficulty();
|
||||
|
||||
switchToDifficulty(1);
|
||||
|
||||
AddAssert("set background on second diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg"));
|
||||
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg"));
|
||||
|
||||
switchToDifficulty(0);
|
||||
|
||||
AddAssert("set background on first diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (2).jpg"));
|
||||
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (2).jpg"));
|
||||
|
||||
AddAssert("set background on all diff", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg"));
|
||||
AddAssert("all diff uses one background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.BackgroundFile == "bg.jpg"));
|
||||
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg.jpg"));
|
||||
AddAssert("other files removed", () => !Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg" || f.Filename == "bg (2).jpg"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackgroundFileChangesPreserveOnEncode()
|
||||
{
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddAssert("set background", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg"));
|
||||
|
||||
createNewDifficulty();
|
||||
createNewDifficulty();
|
||||
|
||||
switchToDifficulty(0);
|
||||
|
||||
AddAssert("set different background on all diff", () => setBackgroundDifferentExtension(applyToAllDifficulties: true, expected: "bg.jpeg"));
|
||||
AddAssert("all diff uses one background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.BackgroundFile == "bg.jpeg"));
|
||||
AddAssert("all diff encode same background", () =>
|
||||
{
|
||||
return Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b =>
|
||||
{
|
||||
var files = new RealmFileStore(Realm, Dependencies.Get<GameHost>().Storage);
|
||||
using var store = new RealmBackedResourceStore<BeatmapSetInfo>(b.BeatmapSet!.ToLive(Realm), files.Store, Realm);
|
||||
string[] osu = Encoding.UTF8.GetString(store.Get(b.File!.Filename)).Split(Environment.NewLine);
|
||||
Assert.That(osu, Does.Contain("0,0,\"bg.jpeg\",0,0"));
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSingleAudioFile()
|
||||
{
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddAssert("set audio", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3"));
|
||||
|
||||
createNewDifficulty();
|
||||
createNewDifficulty();
|
||||
|
||||
switchToDifficulty(1);
|
||||
|
||||
AddAssert("set audio on second diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3"));
|
||||
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3"));
|
||||
|
||||
switchToDifficulty(0);
|
||||
|
||||
AddAssert("set audio on first diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (2).mp3"));
|
||||
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (2).mp3"));
|
||||
|
||||
AddAssert("set audio on all diff", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3"));
|
||||
AddAssert("all diff uses one audio", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.AudioFile == "audio.mp3"));
|
||||
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3"));
|
||||
AddAssert("other files removed", () => !Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3" || f.Filename == "audio (2).mp3"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleBackgroundFiles()
|
||||
{
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg"));
|
||||
|
||||
createNewDifficulty();
|
||||
|
||||
AddAssert("new difficulty uses same background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg");
|
||||
AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg"));
|
||||
AddAssert("new difficulty uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg");
|
||||
|
||||
switchToDifficulty(0);
|
||||
|
||||
AddAssert("old difficulty uses old background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg");
|
||||
AddAssert("old background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg.jpg"));
|
||||
AddStep("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg"));
|
||||
AddAssert("other background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleAudioFiles()
|
||||
{
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3"));
|
||||
|
||||
createNewDifficulty();
|
||||
|
||||
AddAssert("new difficulty uses same audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3");
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
|
||||
AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3"));
|
||||
AddAssert("new difficulty uses new audio", () => Beatmap.Value.Metadata.AudioFile == "audio (1).mp3");
|
||||
|
||||
switchToDifficulty(0);
|
||||
|
||||
AddAssert("old difficulty uses old audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3");
|
||||
AddAssert("old audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3"));
|
||||
AddStep("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3"));
|
||||
AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3"));
|
||||
}
|
||||
|
||||
private void createNewDifficulty()
|
||||
{
|
||||
string? currentDifficulty = null;
|
||||
|
||||
AddStep("save", () => Editor.Save());
|
||||
AddStep("create new difficulty", () =>
|
||||
{
|
||||
currentDifficulty = EditorBeatmap.BeatmapInfo.DifficultyName;
|
||||
Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
|
||||
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||
AddUntilStep("wait for created", () =>
|
||||
{
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName != currentDifficulty;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for editor load", () => Editor.IsLoaded);
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
|
||||
}
|
||||
|
||||
private void switchToDifficulty(int index)
|
||||
{
|
||||
AddStep("save", () => Editor.Save());
|
||||
AddStep($"switch to difficulty #{index + 1}", () =>
|
||||
Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(index)));
|
||||
|
||||
AddUntilStep("wait for editor load", () => Editor.IsLoaded);
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
|
||||
}
|
||||
|
||||
private bool setBackground(bool applyToAllDifficulties, string expected)
|
||||
{
|
||||
var setup = Editor.ChildrenOfType<SetupScreen>().First();
|
||||
|
||||
return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder =>
|
||||
{
|
||||
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeBackgroundImage(
|
||||
new FileInfo(Path.Combine(extractedFolder, @"machinetop_background.jpg")),
|
||||
applyToAllDifficulties);
|
||||
|
||||
Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected));
|
||||
return success;
|
||||
});
|
||||
}
|
||||
|
||||
private bool setBackgroundDifferentExtension(bool applyToAllDifficulties, string expected)
|
||||
{
|
||||
var setup = Editor.ChildrenOfType<SetupScreen>().First();
|
||||
|
||||
return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder =>
|
||||
{
|
||||
File.Move(
|
||||
Path.Combine(extractedFolder, @"machinetop_background.jpg"),
|
||||
Path.Combine(extractedFolder, @"machinetop_background.jpeg"));
|
||||
|
||||
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeBackgroundImage(
|
||||
new FileInfo(Path.Combine(extractedFolder, @"machinetop_background.jpeg")),
|
||||
applyToAllDifficulties);
|
||||
|
||||
Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected));
|
||||
return success;
|
||||
});
|
||||
}
|
||||
|
||||
private bool setAudio(bool applyToAllDifficulties, string expected)
|
||||
{
|
||||
var setup = Editor.ChildrenOfType<SetupScreen>().First();
|
||||
|
||||
return setFile(TestResources.GetTestBeatmapForImport(), extractedFolder =>
|
||||
{
|
||||
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeAudioTrack(
|
||||
new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")),
|
||||
applyToAllDifficulties);
|
||||
|
||||
Assert.That(Beatmap.Value.Metadata.AudioFile, Is.EqualTo(expected));
|
||||
return success;
|
||||
});
|
||||
}
|
||||
|
||||
private bool setFile(string archivePath, Func<string, bool> func)
|
||||
{
|
||||
string temp = archivePath;
|
||||
|
||||
string extractedFolder = $"{temp}_extracted";
|
||||
Directory.CreateDirectory(extractedFolder);
|
||||
|
||||
try
|
||||
{
|
||||
using (var zip = ZipArchive.Open(temp))
|
||||
zip.WriteToDirectory(extractedFolder);
|
||||
|
||||
return func(extractedFolder);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(temp);
|
||||
Directory.Delete(extractedFolder, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
|
||||
beatmap.ControlPointInfo.Add(80000, new EffectControlPoint { KiaiMode = true });
|
||||
beatmap.ControlPointInfo.Add(110000, new EffectControlPoint { KiaiMode = false });
|
||||
beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 };
|
||||
beatmap.Bookmarks = new[] { 75000, 125000 };
|
||||
beatmap.Breaks.Add(new ManualBreakPeriod(90000, 120000));
|
||||
|
||||
editorBeatmap = new EditorBeatmap(beatmap);
|
||||
|
@ -44,14 +44,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
BeatmapStore beatmapStore;
|
||||
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
Add(beatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
|
@ -14,7 +14,6 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
@ -76,7 +75,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
createLoungeRoom(new Room
|
||||
{
|
||||
Name = "Multiplayer room",
|
||||
Status = new RoomStatusOpen(),
|
||||
EndDate = DateTimeOffset.Now.AddDays(1),
|
||||
Type = MatchType.HeadToHead,
|
||||
Playlist = [item1],
|
||||
@ -85,7 +83,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
createLoungeRoom(new Room
|
||||
{
|
||||
Name = "Private room",
|
||||
Status = new RoomStatusOpenPrivate(),
|
||||
Password = "*",
|
||||
EndDate = DateTimeOffset.Now.AddDays(1),
|
||||
Type = MatchType.HeadToHead,
|
||||
@ -95,36 +92,38 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
createLoungeRoom(new Room
|
||||
{
|
||||
Name = "Playlist room with multiple beatmaps",
|
||||
Status = new RoomStatusPlaying(),
|
||||
Status = RoomStatus.Playing,
|
||||
EndDate = DateTimeOffset.Now.AddDays(1),
|
||||
Playlist = [item1, item2],
|
||||
CurrentPlaylistItem = item1
|
||||
}),
|
||||
createLoungeRoom(new Room
|
||||
{
|
||||
Name = "Finished room",
|
||||
Status = new RoomStatusEnded(),
|
||||
Name = "Closing soon",
|
||||
EndDate = DateTimeOffset.Now.AddSeconds(5),
|
||||
}),
|
||||
createLoungeRoom(new Room
|
||||
{
|
||||
Name = "Closed room",
|
||||
EndDate = DateTimeOffset.Now,
|
||||
}),
|
||||
createLoungeRoom(new Room
|
||||
{
|
||||
Name = "Spotlight room",
|
||||
Status = new RoomStatusOpen(),
|
||||
Category = RoomCategory.Spotlight,
|
||||
}),
|
||||
createLoungeRoom(new Room
|
||||
{
|
||||
Name = "Featured artist room",
|
||||
Status = new RoomStatusOpen(),
|
||||
Category = RoomCategory.FeaturedArtist,
|
||||
}),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("wait for panel load", () => rooms.Count == 6);
|
||||
AddUntilStep("wait for panel load", () => rooms.Count == 7);
|
||||
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2);
|
||||
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 4);
|
||||
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -136,7 +135,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room
|
||||
{
|
||||
Name = "Room with password",
|
||||
Status = new RoomStatusOpen(),
|
||||
Type = MatchType.HeadToHead,
|
||||
}));
|
||||
|
||||
|
@ -66,14 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
BeatmapStore beatmapStore;
|
||||
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
Add(beatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
|
@ -46,16 +46,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
BeatmapStore beatmapStore;
|
||||
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()))!;
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
Add(beatmapStore);
|
||||
}
|
||||
|
||||
private void setUp()
|
||||
|
@ -31,18 +31,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
BeatmapStore beatmapStore;
|
||||
|
||||
Dependencies.Cache(new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
var beatmapSet = TestResources.CreateTestBeatmapSetInfo();
|
||||
|
||||
manager.Import(beatmapSet);
|
||||
|
||||
Add(detachedBeatmapStore);
|
||||
Add(beatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
|
@ -1,149 +0,0 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
|
||||
{
|
||||
private const double track_length = 10000;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
|
||||
|
||||
private BeatmapManager beatmaps = null!;
|
||||
private RulesetStore rulesets = null!;
|
||||
private BeatmapSetInfo? importedSet;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, API));
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
|
||||
Realm.Write(r =>
|
||||
{
|
||||
foreach (var set in r.All<BeatmapSetInfo>())
|
||||
{
|
||||
foreach (var b in set.Beatmaps)
|
||||
{
|
||||
// These will all have a virtual track length of 1000, see WorkingBeatmap.GetVirtualTrack().
|
||||
b.Length = track_length - 1000;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStatusUpdateOnEnter()
|
||||
{
|
||||
Room room = null!;
|
||||
PlaylistsRoomSubScreen roomScreen = null!;
|
||||
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
RoomManager.AddRoom(room = new Room
|
||||
{
|
||||
Name = @"Test Room",
|
||||
Host = new APIUser { Username = @"Host" },
|
||||
Category = RoomCategory.Normal,
|
||||
EndDate = DateTimeOffset.Now.AddMinutes(-1)
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
|
||||
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
|
||||
AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf<RoomStatusEnded>);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCloseButtonGoesAwayAfterGracePeriod()
|
||||
{
|
||||
Room room = null!;
|
||||
PlaylistsRoomSubScreen roomScreen = null!;
|
||||
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
RoomManager.AddRoom(room = new Room
|
||||
{
|
||||
Name = @"Test Room",
|
||||
Host = api.LocalUser.Value,
|
||||
Category = RoomCategory.Normal,
|
||||
StartDate = DateTimeOffset.Now.AddMinutes(-5).AddSeconds(3),
|
||||
EndDate = DateTimeOffset.Now.AddMinutes(30)
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
|
||||
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
|
||||
AddAssert("close button present", () => roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
|
||||
AddUntilStep("wait for close button to disappear", () => !roomScreen.ChildrenOfType<DangerousRoundedButton>().Any());
|
||||
}
|
||||
|
||||
[TestCase(120_000, true)] // Definitely enough time.
|
||||
[TestCase(45_000, true)] // Enough time.
|
||||
[TestCase(35_000, false)] // Not enough time to complete beatmap after lenience.
|
||||
[TestCase(20_000, false)] // Not enough time.
|
||||
[TestCase(5_000, false)] // Not enough time to complete beatmap before lenience.
|
||||
[TestCase(37_500, true, 2)] // Enough time to complete beatmap after mods are applied.
|
||||
public void TestReadyButtonEnablementPeriod(int offsetMs, bool enabled, double rate = 1)
|
||||
{
|
||||
Room room = null!;
|
||||
PlaylistsRoomSubScreen roomScreen = null!;
|
||||
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
RoomManager.AddRoom(room = new Room
|
||||
{
|
||||
Name = @"Test Room",
|
||||
Host = api.LocalUser.Value,
|
||||
Category = RoomCategory.Normal,
|
||||
StartDate = DateTimeOffset.Now,
|
||||
EndDate = DateTimeOffset.Now.AddMilliseconds(offsetMs),
|
||||
Playlist =
|
||||
[
|
||||
new PlaylistItem(importedSet!.Beatmaps[0])
|
||||
{
|
||||
RequiredMods = rate == 1
|
||||
? []
|
||||
: [new APIMod(new OsuModDoubleTime { SpeedChange = { Value = rate } })]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
|
||||
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
|
||||
AddUntilStep("ready button enabled", () => roomScreen.ChildrenOfType<PlaylistsReadyButton>().SingleOrDefault()?.Enabled.Value, () => Is.EqualTo(enabled));
|
||||
}
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -23,6 +24,7 @@ using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -42,6 +44,9 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
private const int set_count = 5;
|
||||
private const int diff_count = 3;
|
||||
|
||||
[Cached(typeof(BeatmapStore))]
|
||||
private TestBeatmapStore beatmaps = new TestBeatmapStore();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
@ -1329,7 +1334,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
carouselAdjust?.Invoke(carousel);
|
||||
|
||||
carousel.BeatmapSets = beatmapSets;
|
||||
beatmaps.BeatmapSets.Clear();
|
||||
beatmaps.BeatmapSets.AddRange(beatmapSets);
|
||||
|
||||
(target ?? this).Child = carousel;
|
||||
});
|
||||
|
@ -56,20 +56,20 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
{
|
||||
DetachedBeatmapStore detachedBeatmapStore;
|
||||
BeatmapStore beatmapStore;
|
||||
|
||||
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
|
||||
// At a point we have isolated interactive test runs enough, this can likely be removed.
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(Realm);
|
||||
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default));
|
||||
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore());
|
||||
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
|
||||
|
||||
Dependencies.Cache(music = new MusicController());
|
||||
|
||||
// required to get bindables attached
|
||||
Add(music);
|
||||
Add(detachedBeatmapStore);
|
||||
Add(beatmapStore);
|
||||
|
||||
Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -10,12 +9,14 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osu.Game.Tests.Online;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
@ -31,6 +32,9 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
private BeatmapSetInfo testBeatmapSetInfo = null!;
|
||||
|
||||
[Cached(typeof(BeatmapStore))]
|
||||
private TestBeatmapStore beatmaps = new TestBeatmapStore();
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
@ -246,13 +250,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
private BeatmapCarousel createCarousel()
|
||||
{
|
||||
beatmaps.BeatmapSets.Clear();
|
||||
beatmaps.BeatmapSets.Add(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5));
|
||||
|
||||
return carousel = new BeatmapCarousel(new FilterCriteria())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
BeatmapSets = new List<BeatmapSetInfo>
|
||||
{
|
||||
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5)),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.SelectV2;
|
||||
|
||||
namespace osu.Game.Tests.Visual.SongSelectV2
|
||||
{
|
||||
|
@ -3,8 +3,10 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.FirstRunSetup;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
[Cached(typeof(BeatmapStore))]
|
||||
private BeatmapStore beatmapStore = new TestBeatmapStore();
|
||||
|
||||
public TestSceneFirstRunScreenUIScale()
|
||||
{
|
||||
AddStep("load screen", () =>
|
||||
|
@ -17,12 +17,14 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.FirstRunSetup;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -47,6 +49,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage));
|
||||
Dependencies.CacheAs<IPerformFromScreenRunner>(performer.Object);
|
||||
Dependencies.CacheAs<INotificationOverlay>(notificationOverlay.Object);
|
||||
Dependencies.CacheAs<BeatmapStore>(new TestBeatmapStore());
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
|
@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
@ -89,8 +90,13 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
},
|
||||
new FormFileSelector
|
||||
{
|
||||
Caption = "Audio file",
|
||||
PlaceholderText = "Select an audio file",
|
||||
Caption = "File selector",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormBeatmapFileSelector(true)
|
||||
{
|
||||
Caption = "File selector with intermediate choice dialog",
|
||||
PlaceholderText = "Select a file",
|
||||
},
|
||||
new FormColourPalette
|
||||
{
|
||||
|
@ -139,6 +139,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public int CountdownOffset { get; set; }
|
||||
|
||||
public int[] Bookmarks { get; set; } = Array.Empty<int>();
|
||||
|
||||
IBeatmap IBeatmap.Clone() => Clone();
|
||||
|
||||
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();
|
||||
|
@ -85,6 +85,7 @@ namespace osu.Game.Beatmaps
|
||||
beatmap.TimelineZoom = original.TimelineZoom;
|
||||
beatmap.Countdown = original.Countdown;
|
||||
beatmap.CountdownOffset = original.CountdownOffset;
|
||||
beatmap.Bookmarks = original.Bookmarks;
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
@ -231,9 +231,6 @@ namespace osu.Game.Beatmaps
|
||||
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
|
||||
public int? MaxCombo { get; set; }
|
||||
|
||||
[Ignored]
|
||||
public int[] Bookmarks { get; set; } = Array.Empty<int>();
|
||||
|
||||
public int BeatmapVersion;
|
||||
|
||||
public BeatmapInfo Clone() => (BeatmapInfo)this.Detach().MemberwiseClone();
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not beatmap or runtime offsets should be applied. Defaults on; only disable for testing purposes.
|
||||
/// Whether beatmap or runtime offsets should be applied. Defaults on; only disable for testing purposes.
|
||||
/// </summary>
|
||||
public bool ApplyOffsets = true;
|
||||
|
||||
@ -305,7 +305,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
switch (pair.Key)
|
||||
{
|
||||
case @"Bookmarks":
|
||||
beatmap.BeatmapInfo.Bookmarks = pair.Value.Split(',').Select(v =>
|
||||
beatmap.Bookmarks = pair.Value.Split(',').Select(v =>
|
||||
{
|
||||
bool result = int.TryParse(v, out int val);
|
||||
return new { result, val };
|
||||
|
@ -110,8 +110,8 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
writer.WriteLine("[Editor]");
|
||||
|
||||
if (beatmap.BeatmapInfo.Bookmarks.Length > 0)
|
||||
writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}"));
|
||||
if (beatmap.Bookmarks.Length > 0)
|
||||
writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.Bookmarks)}"));
|
||||
writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.DistanceSpacing}"));
|
||||
writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}"));
|
||||
writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.GridSize}"));
|
||||
|
@ -107,6 +107,8 @@ namespace osu.Game.Beatmaps
|
||||
/// </summary>
|
||||
int CountdownOffset { get; internal set; }
|
||||
|
||||
int[] Bookmarks { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a shallow-clone of this beatmap and returns it.
|
||||
/// </summary>
|
||||
|
35
osu.Game/Database/BeatmapStore.cs
Normal file
35
osu.Game/Database/BeatmapStore.cs
Normal file
@ -0,0 +1,35 @@
|
||||
// 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.Threading;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// A store which contains a thread-safe representation of beatmaps available game-wide.
|
||||
/// This exposes changes to available beatmaps, such as post-import or deletion.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The main goal of classes which implement this interface should be to provide change
|
||||
/// tracking and thread safety in a performant way, rather than having to worry about such
|
||||
/// concerns at the point of usage.
|
||||
/// </remarks>
|
||||
public abstract partial class BeatmapStore : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all available beatmaps.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token which allows early abort from the operation.</param>
|
||||
/// <returns>A bindable list of all available beatmap sets.</returns>
|
||||
/// <remarks>
|
||||
/// This operation may block during the initial load process.
|
||||
///
|
||||
/// It is generally expected that once a beatmap store is in a good state, the overhead of this call
|
||||
/// should be negligible.
|
||||
/// </remarks>
|
||||
public abstract IBindableList<BeatmapSetInfo> GetBeatmapSets(CancellationToken? cancellationToken);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Utils;
|
||||
using Realms;
|
||||
@ -46,7 +47,7 @@ namespace osu.Game.Database
|
||||
|
||||
protected LegacyExporter(Storage storage)
|
||||
{
|
||||
exportStorage = storage.GetStorageForDirectory(@"exports");
|
||||
exportStorage = (storage as OsuStorage)?.GetExportStorage() ?? storage.GetStorageForDirectory(@"exports");
|
||||
UserFileStorage = storage.GetStorageForDirectory(@"files");
|
||||
}
|
||||
|
||||
|
@ -8,14 +8,13 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using Realms;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public partial class DetachedBeatmapStore : Component
|
||||
public partial class RealmDetachedBeatmapStore : BeatmapStore
|
||||
{
|
||||
private readonly ManualResetEventSlim loaded = new ManualResetEventSlim();
|
||||
|
||||
@ -28,7 +27,7 @@ namespace osu.Game.Database
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
public IBindableList<BeatmapSetInfo> GetDetachedBeatmaps(CancellationToken? cancellationToken)
|
||||
public override IBindableList<BeatmapSetInfo> GetBeatmapSets(CancellationToken? cancellationToken)
|
||||
{
|
||||
loaded.Wait(cancellationToken ?? CancellationToken.None);
|
||||
return detachedBeatmapSets.GetBoundCopy();
|
@ -195,6 +195,27 @@ namespace osu.Game.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the accent colour representing a <see cref="Room"/>'s current status.
|
||||
/// </summary>
|
||||
public Color4 ForRoomStatus(Room room)
|
||||
{
|
||||
if (room.HasEnded)
|
||||
return YellowDarker;
|
||||
|
||||
switch (room.Status)
|
||||
{
|
||||
case RoomStatus.Playing:
|
||||
return Purple;
|
||||
|
||||
default:
|
||||
if (room.HasPassword)
|
||||
return GreenDark;
|
||||
|
||||
return GreenLight;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves colour for a <see cref="RankingTier"/>.
|
||||
/// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours
|
||||
|
@ -242,20 +242,26 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
|
||||
Task ICanAcceptFiles.Import(ImportTask[] tasks, ImportParameters parameters) => throw new NotImplementedException();
|
||||
|
||||
protected virtual FileChooserPopover CreatePopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath) => new FileChooserPopover(handledExtensions, current, chooserPath);
|
||||
|
||||
public Popover GetPopover()
|
||||
{
|
||||
var popover = new FileChooserPopover(handledExtensions, Current, initialChooserPath);
|
||||
var popover = CreatePopover(handledExtensions, Current, initialChooserPath);
|
||||
popoverState.UnbindBindings();
|
||||
popoverState.BindTo(popover.State);
|
||||
return popover;
|
||||
}
|
||||
|
||||
private partial class FileChooserPopover : OsuPopover
|
||||
protected partial class FileChooserPopover : OsuPopover
|
||||
{
|
||||
protected override string PopInSampleName => "UI/overlay-big-pop-in";
|
||||
protected override string PopOutSampleName => "UI/overlay-big-pop-out";
|
||||
|
||||
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> currentFile, string? chooserPath)
|
||||
private readonly Bindable<FileInfo?> current = new Bindable<FileInfo?>();
|
||||
|
||||
protected OsuFileSelector FileSelector;
|
||||
|
||||
public FileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath)
|
||||
: base(false)
|
||||
{
|
||||
Child = new Container
|
||||
@ -264,12 +270,13 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
// simplest solution to avoid underlying text to bleed through the bottom border
|
||||
// https://github.com/ppy/osu/pull/30005#issuecomment-2378884430
|
||||
Padding = new MarginPadding { Bottom = 1 },
|
||||
Child = new OsuFileSelector(chooserPath, handledExtensions)
|
||||
Child = FileSelector = new OsuFileSelector(chooserPath, handledExtensions)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CurrentFile = { BindTarget = currentFile }
|
||||
},
|
||||
};
|
||||
|
||||
this.current.BindTo(current);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -292,6 +299,19 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
FileSelector.CurrentFile.ValueChanged += f =>
|
||||
{
|
||||
if (f.NewValue != null)
|
||||
OnFileSelected(f.NewValue);
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual void OnFileSelected(FileInfo file) => current.Value = file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,11 @@ namespace osu.Game.IO
|
||||
TryChangeToCustomStorage(out Error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="Storage"/> used for storing exported files.
|
||||
/// </summary>
|
||||
public virtual Storage GetExportStorage() => GetStorageForDirectory(@"exports");
|
||||
|
||||
/// <summary>
|
||||
/// Resets the custom storage path, changing the target storage to the default location.
|
||||
/// </summary>
|
||||
|
@ -152,6 +152,10 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Right }, GlobalAction.EditorSeekToNextHitObject),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Left }, GlobalAction.EditorSeekToPreviousSamplePoint),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.Right }, GlobalAction.EditorSeekToNextSamplePoint),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.B }, GlobalAction.EditorAddBookmark),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.B }, GlobalAction.EditorRemoveClosestBookmark),
|
||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Left }, GlobalAction.EditorSeekToPreviousBookmark),
|
||||
new KeyBinding(new[] { InputKey.Alt, InputKey.Right }, GlobalAction.EditorSeekToNextBookmark),
|
||||
};
|
||||
|
||||
private static IEnumerable<KeyBinding> editorTestPlayKeyBindings => new[]
|
||||
@ -476,6 +480,18 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridType))]
|
||||
EditorCycleGridType,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorAddBookmark))]
|
||||
EditorAddBookmark,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorRemoveClosestBookmark))]
|
||||
EditorRemoveClosestBookmark,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToPreviousBookmark))]
|
||||
EditorSeekToPreviousBookmark,
|
||||
|
||||
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorSeekToNextBookmark))]
|
||||
EditorSeekToNextBookmark,
|
||||
}
|
||||
|
||||
public enum GlobalActionCategory
|
||||
|
@ -198,6 +198,21 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ClickToSelectBackground => new TranslatableString(getKey(@"click_to_select_background"), @"Click to select a background image");
|
||||
|
||||
/// <summary>
|
||||
/// "Apply this change to all difficulties?"
|
||||
/// </summary>
|
||||
public static LocalisableString ApplicationScopeSelectionTitle => new TranslatableString(getKey(@"application_scope_selection_title"), @"Apply this change to all difficulties?");
|
||||
|
||||
/// <summary>
|
||||
/// "Apply to all difficulties"
|
||||
/// </summary>
|
||||
public static LocalisableString ApplyToAllDifficulties => new TranslatableString(getKey(@"apply_to_all_difficulties"), @"Apply to all difficulties");
|
||||
|
||||
/// <summary>
|
||||
/// "Only apply to this difficulty"
|
||||
/// </summary>
|
||||
public static LocalisableString ApplyToThisDifficulty => new TranslatableString(getKey(@"apply_to_this_difficulty"), @"Only apply to this difficulty");
|
||||
|
||||
/// <summary>
|
||||
/// "Ruleset ({0})"
|
||||
/// </summary>
|
||||
|
@ -154,6 +154,36 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString TimelineShowTicks => new TranslatableString(getKey(@"timeline_show_ticks"), @"Show ticks");
|
||||
|
||||
/// <summary>
|
||||
/// "Bookmarks"
|
||||
/// </summary>
|
||||
public static LocalisableString Bookmarks => new TranslatableString(getKey(@"bookmarks"), @"Bookmarks");
|
||||
|
||||
/// <summary>
|
||||
/// "Add bookmark"
|
||||
/// </summary>
|
||||
public static LocalisableString AddBookmark => new TranslatableString(getKey(@"add_bookmark"), @"Add bookmark");
|
||||
|
||||
/// <summary>
|
||||
/// "Remove closest bookmark"
|
||||
/// </summary>
|
||||
public static LocalisableString RemoveClosestBookmark => new TranslatableString(getKey(@"remove_closest_bookmark"), @"Remove closest bookmark");
|
||||
|
||||
/// <summary>
|
||||
/// "Seek to previous bookmark"
|
||||
/// </summary>
|
||||
public static LocalisableString SeekToPreviousBookmark => new TranslatableString(getKey(@"seek_to_previous_bookmark"), @"Seek to previous bookmark");
|
||||
|
||||
/// <summary>
|
||||
/// "Seek to next bookmark"
|
||||
/// </summary>
|
||||
public static LocalisableString SeekToNextBookmark => new TranslatableString(getKey(@"seek_to_next_bookmark"), @"Seek to next bookmark");
|
||||
|
||||
/// <summary>
|
||||
/// "Reset bookmarks"
|
||||
/// </summary>
|
||||
public static LocalisableString ResetBookmarks => new TranslatableString(getKey(@"reset_bookmarks"), @"Reset bookmarks");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
}
|
@ -429,6 +429,26 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString EditorSeekToNextSamplePoint => new TranslatableString(getKey(@"editor_seek_to_next_sample_point"), @"Seek to next sample point");
|
||||
|
||||
/// <summary>
|
||||
/// "Add bookmark"
|
||||
/// </summary>
|
||||
public static LocalisableString EditorAddBookmark => new TranslatableString(getKey(@"editor_add_bookmark"), @"Add bookmark");
|
||||
|
||||
/// <summary>
|
||||
/// "Remove closest bookmark"
|
||||
/// </summary>
|
||||
public static LocalisableString EditorRemoveClosestBookmark => new TranslatableString(getKey(@"editor_remove_closest_bookmark"), @"Remove closest bookmark");
|
||||
|
||||
/// <summary>
|
||||
/// "Seek to previous bookmark"
|
||||
/// </summary>
|
||||
public static LocalisableString EditorSeekToPreviousBookmark => new TranslatableString(getKey(@"editor_seek_to_previous_bookmark"), @"Seek to previous bookmark");
|
||||
|
||||
/// <summary>
|
||||
/// "Seek to next bookmark"
|
||||
/// </summary>
|
||||
public static LocalisableString EditorSeekToNextBookmark => new TranslatableString(getKey(@"editor_seek_to_next_bookmark"), @"Seek to next bookmark");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
34
osu.Game/Localisation/RoomStatusPillStrings.cs
Normal file
34
osu.Game/Localisation/RoomStatusPillStrings.cs
Normal file
@ -0,0 +1,34 @@
|
||||
// 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.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class RoomStatusPillStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.RoomStatusPill";
|
||||
|
||||
/// <summary>
|
||||
/// "Ended"
|
||||
/// </summary>
|
||||
public static LocalisableString Ended => new TranslatableString(getKey(@"ended"), @"Ended");
|
||||
|
||||
/// <summary>
|
||||
/// "Playing"
|
||||
/// </summary>
|
||||
public static LocalisableString Playing => new TranslatableString(getKey(@"playing"), @"Playing");
|
||||
|
||||
/// <summary>
|
||||
/// "Open (Private)"
|
||||
/// </summary>
|
||||
public static LocalisableString OpenPrivate => new TranslatableString(getKey(@"open_private"), @"Open (Private)");
|
||||
|
||||
/// <summary>
|
||||
/// "Open"
|
||||
/// </summary>
|
||||
public static LocalisableString Open => new TranslatableString(getKey(@"open"), @"Open");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
@ -18,7 +18,6 @@ using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -395,15 +394,17 @@ namespace osu.Game.Online.Multiplayer
|
||||
switch (state)
|
||||
{
|
||||
case MultiplayerRoomState.Open:
|
||||
APIRoom.Status = APIRoom.HasPassword ? new RoomStatusOpenPrivate() : new RoomStatusOpen();
|
||||
APIRoom.Status = RoomStatus.Idle;
|
||||
break;
|
||||
|
||||
case MultiplayerRoomState.WaitingForLoad:
|
||||
case MultiplayerRoomState.Playing:
|
||||
APIRoom.Status = new RoomStatusPlaying();
|
||||
APIRoom.Status = RoomStatus.Playing;
|
||||
break;
|
||||
|
||||
case MultiplayerRoomState.Closed:
|
||||
APIRoom.Status = new RoomStatusEnded();
|
||||
APIRoom.EndDate = DateTimeOffset.Now;
|
||||
APIRoom.Status = RoomStatus.Idle;
|
||||
break;
|
||||
}
|
||||
|
||||
@ -821,7 +822,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
Room.Settings = settings;
|
||||
APIRoom.Name = Room.Settings.Name;
|
||||
APIRoom.Password = Room.Settings.Password;
|
||||
APIRoom.Status = string.IsNullOrEmpty(Room.Settings.Password) ? new RoomStatusOpen() : new RoomStatusOpenPrivate();
|
||||
APIRoom.Type = Room.Settings.MatchType;
|
||||
APIRoom.QueueMode = Room.Settings.QueueMode;
|
||||
APIRoom.AutoStartDuration = Room.Settings.AutoStartDuration;
|
||||
|
@ -6,12 +6,10 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.IO.Serialization.Converters;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
|
||||
namespace osu.Game.Online.Rooms
|
||||
{
|
||||
@ -248,7 +246,7 @@ namespace osu.Game.Online.Rooms
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current room status.
|
||||
/// The current status of the room.
|
||||
/// </summary>
|
||||
public RoomStatus Status
|
||||
{
|
||||
@ -265,18 +263,6 @@ namespace osu.Game.Online.Rooms
|
||||
set => SetField(ref availability, value);
|
||||
}
|
||||
|
||||
[OnDeserialized]
|
||||
private void onDeserialised(StreamingContext context)
|
||||
{
|
||||
// API doesn't populate status so let's do it here.
|
||||
if (EndDate != null && DateTimeOffset.Now >= EndDate)
|
||||
Status = new RoomStatusEnded();
|
||||
else if (HasPassword)
|
||||
Status = new RoomStatusOpenPrivate();
|
||||
else
|
||||
Status = new RoomStatusOpen();
|
||||
}
|
||||
|
||||
[JsonProperty("id")]
|
||||
private long? roomId;
|
||||
|
||||
@ -349,8 +335,9 @@ namespace osu.Game.Online.Rooms
|
||||
[JsonProperty("channel_id")]
|
||||
private int channelId;
|
||||
|
||||
// Not serialised (see: GetRoomsRequest).
|
||||
private RoomStatus status = new RoomStatusOpen();
|
||||
[JsonProperty("status")]
|
||||
[JsonConverter(typeof(SnakeCaseStringEnumConverter))]
|
||||
private RoomStatus status;
|
||||
|
||||
// Not yet serialised (not implemented).
|
||||
private RoomAvailability availability;
|
||||
@ -388,6 +375,15 @@ namespace osu.Game.Online.Rooms
|
||||
RecentParticipants = other.RecentParticipants;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the room is no longer available.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property does not update in real-time and needs to be queried periodically.
|
||||
/// Subscribe to <see cref="EndDate"/> to be notified of any immediate changes.
|
||||
/// </remarks>
|
||||
public bool HasEnded => DateTimeOffset.Now >= EndDate;
|
||||
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class RoomPlaylistItemStats
|
||||
{
|
||||
|
@ -1,19 +1,11 @@
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Online.Rooms
|
||||
{
|
||||
public abstract class RoomStatus
|
||||
public enum RoomStatus
|
||||
{
|
||||
public abstract string Message { get; }
|
||||
public abstract Color4 GetAppropriateColour(OsuColour colours);
|
||||
|
||||
public override int GetHashCode() => GetType().GetHashCode();
|
||||
public override bool Equals(object obj) => GetType() == obj?.GetType();
|
||||
Idle,
|
||||
Playing,
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
// 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.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Online.Rooms.RoomStatuses
|
||||
{
|
||||
public class RoomStatusEnded : RoomStatus
|
||||
{
|
||||
public override string Message => "Ended";
|
||||
public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDarker;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
// 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.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Online.Rooms.RoomStatuses
|
||||
{
|
||||
public class RoomStatusOpen : RoomStatus
|
||||
{
|
||||
public override string Message => "Open";
|
||||
public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
// 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.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Online.Rooms.RoomStatuses
|
||||
{
|
||||
public class RoomStatusOpenPrivate : RoomStatus
|
||||
{
|
||||
public override string Message => "Open (Private)";
|
||||
public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDark;
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
// 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.Game.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Online.Rooms.RoomStatuses
|
||||
{
|
||||
public class RoomStatusPlaying : RoomStatus
|
||||
{
|
||||
public override string Message => "Playing";
|
||||
public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple;
|
||||
}
|
||||
}
|
@ -1143,7 +1143,7 @@ namespace osu.Game
|
||||
loadComponentSingleFile(new MedalOverlay(), topMostOverlayContent.Add);
|
||||
|
||||
loadComponentSingleFile(new BackgroundDataStoreProcessor(), Add);
|
||||
loadComponentSingleFile(new DetachedBeatmapStore(), Add, true);
|
||||
loadComponentSingleFile<BeatmapStore>(new RealmDetachedBeatmapStore(), Add, true);
|
||||
|
||||
Add(externalLinkOpener = new ExternalLinkOpener());
|
||||
Add(new MusicKeyBindingHandler());
|
||||
|
@ -245,18 +245,19 @@ namespace osu.Game.Overlays
|
||||
this.FadeOut(200);
|
||||
}
|
||||
|
||||
public void Dismiss()
|
||||
public bool Dismiss()
|
||||
{
|
||||
if (drawableMedal != null && drawableMedal.State != DisplayState.Full)
|
||||
{
|
||||
// if we haven't yet, play out the animation fully
|
||||
drawableMedal.State = DisplayState.Full;
|
||||
FinishTransforms(true);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
Hide();
|
||||
Expire();
|
||||
return true;
|
||||
}
|
||||
|
||||
private partial class BackgroundStrip : Container
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -35,7 +34,7 @@ namespace osu.Game.Overlays
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
private Container<Drawable> medalContainer = null!;
|
||||
private MedalAnimation? lastAnimation;
|
||||
private MedalAnimation? currentMedalDisplay;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@ -54,11 +53,12 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
OverlayActivationMode.BindValueChanged(val =>
|
||||
{
|
||||
if (val.NewValue == OverlayActivation.All && (queuedMedals.Any() || medalContainer.Any() || lastAnimation?.IsLoaded == false))
|
||||
Show();
|
||||
}, true);
|
||||
OverlayActivationMode.BindValueChanged(_ => showNextMedal(), true);
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
// don't allow hiding the overlay via any method other than our own.
|
||||
}
|
||||
|
||||
private void handleMedalMessages(SocketMessage obj)
|
||||
@ -83,34 +83,18 @@ namespace osu.Game.Overlays
|
||||
|
||||
var medalAnimation = new MedalAnimation(medal);
|
||||
|
||||
queuedMedals.Enqueue(medalAnimation);
|
||||
Logger.Log($"Queueing medal unlock for \"{medal.Name}\" ({queuedMedals.Count} to display)");
|
||||
|
||||
if (OverlayActivationMode.Value == OverlayActivation.All)
|
||||
Scheduler.AddOnce(Show);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (medalContainer.Any() || lastAnimation?.IsLoaded == false)
|
||||
return;
|
||||
|
||||
if (!queuedMedals.TryDequeue(out lastAnimation))
|
||||
Schedule(() => LoadComponentAsync(medalAnimation, m =>
|
||||
{
|
||||
Logger.Log("All queued medals have been displayed!");
|
||||
Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Log($"Preparing to display \"{lastAnimation.Medal.Name}\"");
|
||||
LoadComponentAsync(lastAnimation, medalContainer.Add);
|
||||
queuedMedals.Enqueue(m);
|
||||
showNextMedal();
|
||||
}));
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
lastAnimation?.Dismiss();
|
||||
progressDisplayByUser();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -118,19 +102,54 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
if (e.Action == GlobalAction.Back)
|
||||
{
|
||||
lastAnimation?.Dismiss();
|
||||
progressDisplayByUser();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
|
||||
private void progressDisplayByUser()
|
||||
{
|
||||
// Dismissing may sometimes play out the medal animation rather than immediately dismissing.
|
||||
if (currentMedalDisplay?.Dismiss() == false)
|
||||
return;
|
||||
|
||||
currentMedalDisplay = null;
|
||||
showNextMedal();
|
||||
}
|
||||
|
||||
private void showNextMedal()
|
||||
{
|
||||
// If already displayed, keep displaying medals regardless of activation mode changes.
|
||||
if (OverlayActivationMode.Value != OverlayActivation.All && State.Value == Visibility.Hidden)
|
||||
return;
|
||||
|
||||
// A medal is already displaying.
|
||||
if (currentMedalDisplay != null)
|
||||
return;
|
||||
|
||||
if (queuedMedals.TryDequeue(out currentMedalDisplay))
|
||||
{
|
||||
Logger.Log($"Displaying \"{currentMedalDisplay.Medal.Name}\"");
|
||||
medalContainer.Add(currentMedalDisplay);
|
||||
Show();
|
||||
}
|
||||
else if (State.Value == Visibility.Visible)
|
||||
{
|
||||
Logger.Log("All queued medals have been displayed, hiding overlay!");
|
||||
base.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
// this event subscription fires async loads, which hard-fail if `CompositeDrawable.disposalCancellationSource` is canceled, which happens in the base call.
|
||||
// therefore, unsubscribe from this event early to reduce the chances of a stray event firing at an inconvenient spot.
|
||||
if (api.IsNotNull())
|
||||
api.NotificationsClient.MessageReceived -= handleMedalMessages;
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -413,6 +413,12 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
set => baseBeatmap.CountdownOffset = value;
|
||||
}
|
||||
|
||||
public int[] Bookmarks
|
||||
{
|
||||
get => baseBeatmap.Bookmarks;
|
||||
set => baseBeatmap.Bookmarks = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
@ -15,24 +19,69 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
|
||||
/// </summary>
|
||||
public partial class BookmarkPart : TimelinePart
|
||||
{
|
||||
private readonly BindableList<int> bookmarks = new BindableList<int>();
|
||||
|
||||
private DrawablePool<BookmarkVisualisation> pool = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(pool = new DrawablePool<BookmarkVisualisation>(10));
|
||||
}
|
||||
|
||||
protected override void LoadBeatmap(EditorBeatmap beatmap)
|
||||
{
|
||||
base.LoadBeatmap(beatmap);
|
||||
foreach (int bookmark in beatmap.BeatmapInfo.Bookmarks)
|
||||
Add(new BookmarkVisualisation(bookmark));
|
||||
|
||||
bookmarks.UnbindAll();
|
||||
bookmarks.BindTo(beatmap.Bookmarks);
|
||||
}
|
||||
|
||||
private partial class BookmarkVisualisation : PointVisualisation, IHasTooltip
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
public BookmarkVisualisation(double startTime)
|
||||
: base(startTime)
|
||||
base.LoadComplete();
|
||||
bookmarks.BindCollectionChanged((_, _) =>
|
||||
{
|
||||
Clear(disposeChildren: false);
|
||||
foreach (int bookmark in bookmarks)
|
||||
Add(pool.Get(v => v.StartTime = bookmark));
|
||||
}, true);
|
||||
}
|
||||
|
||||
private partial class BookmarkVisualisation : PoolableDrawable, IHasTooltip
|
||||
{
|
||||
private int startTime;
|
||||
|
||||
public int StartTime
|
||||
{
|
||||
get => startTime;
|
||||
set
|
||||
{
|
||||
if (startTime == value)
|
||||
return;
|
||||
|
||||
startTime = value;
|
||||
X = startTime;
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours) => Colour = colours.Blue;
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
RelativePositionAxes = Axes.Both;
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
public LocalisableString TooltipText => $"{StartTime.ToEditorFormattedString()} bookmark";
|
||||
Anchor = Anchor.CentreLeft;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
Width = PointVisualisation.MAX_WIDTH;
|
||||
Height = 0.4f;
|
||||
|
||||
Colour = colours.Blue;
|
||||
InternalChild = new FastCircle { RelativeSizeAxes = Axes.Both };
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText => $"{((double)StartTime).ToEditorFormattedString()} bookmark";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -422,6 +422,29 @@ namespace osu.Game.Screens.Edit
|
||||
Items = new MenuItem[]
|
||||
{
|
||||
new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime),
|
||||
new EditorMenuItem(EditorStrings.Bookmarks)
|
||||
{
|
||||
Items = new MenuItem[]
|
||||
{
|
||||
new EditorMenuItem(EditorStrings.AddBookmark, MenuItemType.Standard, addBookmarkAtCurrentTime)
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorAddBookmark),
|
||||
},
|
||||
new EditorMenuItem(EditorStrings.RemoveClosestBookmark, MenuItemType.Destructive, removeBookmarksInProximityToCurrentTime)
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorRemoveClosestBookmark)
|
||||
},
|
||||
new EditorMenuItem(EditorStrings.SeekToPreviousBookmark, MenuItemType.Standard, () => seekBookmark(-1))
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorSeekToPreviousBookmark)
|
||||
},
|
||||
new EditorMenuItem(EditorStrings.SeekToNextBookmark, MenuItemType.Standard, () => seekBookmark(1))
|
||||
{
|
||||
Hotkey = new Hotkey(GlobalAction.EditorSeekToNextBookmark)
|
||||
},
|
||||
new EditorMenuItem(EditorStrings.ResetBookmarks, MenuItemType.Destructive, () => editorBeatmap.Bookmarks.Clear())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -753,6 +776,14 @@ namespace osu.Game.Screens.Edit
|
||||
case GlobalAction.EditorSeekToNextSamplePoint:
|
||||
seekSamplePoint(1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorSeekToPreviousBookmark:
|
||||
seekBookmark(-1);
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorSeekToNextBookmark:
|
||||
seekBookmark(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (e.Repeat)
|
||||
@ -760,6 +791,14 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.EditorAddBookmark:
|
||||
addBookmarkAtCurrentTime();
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorRemoveClosestBookmark:
|
||||
removeBookmarksInProximityToCurrentTime();
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorCloneSelection:
|
||||
Clone();
|
||||
return true;
|
||||
@ -792,6 +831,19 @@ namespace osu.Game.Screens.Edit
|
||||
return false;
|
||||
}
|
||||
|
||||
private void addBookmarkAtCurrentTime()
|
||||
{
|
||||
int bookmark = (int)clock.CurrentTimeAccurate;
|
||||
int idx = editorBeatmap.Bookmarks.BinarySearch(bookmark);
|
||||
if (idx < 0)
|
||||
editorBeatmap.Bookmarks.Insert(~idx, bookmark);
|
||||
}
|
||||
|
||||
private void removeBookmarksInProximityToCurrentTime()
|
||||
{
|
||||
editorBeatmap.Bookmarks.RemoveAll(b => Math.Abs(b - clock.CurrentTimeAccurate) < 2000);
|
||||
}
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
|
||||
{
|
||||
}
|
||||
@ -1127,6 +1179,16 @@ namespace osu.Game.Screens.Edit
|
||||
clock.SeekSmoothlyTo(found.StartTime);
|
||||
}
|
||||
|
||||
private void seekBookmark(int direction)
|
||||
{
|
||||
int? targetBookmark = direction < 1
|
||||
? editorBeatmap.Bookmarks.Cast<int?>().LastOrDefault(b => b < clock.CurrentTimeAccurate)
|
||||
: editorBeatmap.Bookmarks.Cast<int?>().FirstOrDefault(b => b > clock.CurrentTimeAccurate);
|
||||
|
||||
if (targetBookmark != null)
|
||||
clock.SeekSmoothlyTo(targetBookmark.Value);
|
||||
}
|
||||
|
||||
private void seekSamplePoint(int direction)
|
||||
{
|
||||
double currentTime = clock.CurrentTimeAccurate;
|
||||
@ -1215,12 +1277,15 @@ namespace osu.Game.Screens.Edit
|
||||
saveRelatedMenuItems.Add(save);
|
||||
yield return save;
|
||||
|
||||
if (RuntimeInfo.IsDesktop)
|
||||
if (RuntimeInfo.OS != RuntimeInfo.Platform.Android)
|
||||
{
|
||||
var export = createExportMenu();
|
||||
saveRelatedMenuItems.AddRange(export.Items);
|
||||
yield return export;
|
||||
}
|
||||
|
||||
if (RuntimeInfo.IsDesktop)
|
||||
{
|
||||
var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally);
|
||||
saveRelatedMenuItems.Add(externalEdit);
|
||||
yield return externalEdit;
|
||||
|
@ -118,6 +118,14 @@ namespace osu.Game.Screens.Edit
|
||||
playableBeatmap.Breaks.AddRange(Breaks);
|
||||
});
|
||||
|
||||
Bookmarks = new BindableList<int>(playableBeatmap.Bookmarks);
|
||||
Bookmarks.BindCollectionChanged((_, _) =>
|
||||
{
|
||||
BeginChange();
|
||||
playableBeatmap.Bookmarks = Bookmarks.OrderBy(x => x).Distinct().ToArray();
|
||||
EndChange();
|
||||
});
|
||||
|
||||
PreviewTime = new BindableInt(BeatmapInfo.Metadata.PreviewTime);
|
||||
PreviewTime.BindValueChanged(s =>
|
||||
{
|
||||
@ -270,6 +278,14 @@ namespace osu.Game.Screens.Edit
|
||||
set => PlayableBeatmap.CountdownOffset = value;
|
||||
}
|
||||
|
||||
public readonly BindableList<int> Bookmarks;
|
||||
|
||||
int[] IBeatmap.Bookmarks
|
||||
{
|
||||
get => PlayableBeatmap.Bookmarks;
|
||||
set => PlayableBeatmap.Bookmarks = value;
|
||||
}
|
||||
|
||||
public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone();
|
||||
|
||||
private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
|
||||
|
@ -46,6 +46,7 @@ namespace osu.Game.Screens.Edit
|
||||
processHitObjects(result, () => newBeatmap ??= readBeatmap(newState));
|
||||
processTimingPoints(() => newBeatmap ??= readBeatmap(newState));
|
||||
processBreaks(() => newBeatmap ??= readBeatmap(newState));
|
||||
processBookmarks(() => newBeatmap ??= readBeatmap(newState));
|
||||
processHitObjectLocalData(() => newBeatmap ??= readBeatmap(newState));
|
||||
editorBeatmap.EndChange();
|
||||
}
|
||||
@ -97,6 +98,27 @@ namespace osu.Game.Screens.Edit
|
||||
}
|
||||
}
|
||||
|
||||
private void processBookmarks(Func<IBeatmap> getNewBeatmap)
|
||||
{
|
||||
var newBookmarks = getNewBeatmap().Bookmarks.ToHashSet();
|
||||
|
||||
foreach (int oldBookmark in editorBeatmap.Bookmarks.ToArray())
|
||||
{
|
||||
if (newBookmarks.Contains(oldBookmark))
|
||||
continue;
|
||||
|
||||
editorBeatmap.Bookmarks.Remove(oldBookmark);
|
||||
}
|
||||
|
||||
foreach (int newBookmark in newBookmarks)
|
||||
{
|
||||
if (editorBeatmap.Bookmarks.Contains(newBookmark))
|
||||
continue;
|
||||
|
||||
editorBeatmap.Bookmarks.Add(newBookmark);
|
||||
}
|
||||
}
|
||||
|
||||
private void processHitObjects(DiffResult result, Func<IBeatmap> getNewBeatmap)
|
||||
{
|
||||
findChangedIndices(result, LegacyDecoder<Beatmap>.Section.HitObjects, out var removedIndices, out var addedIndices);
|
||||
|
155
osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs
Normal file
155
osu.Game/Screens/Edit/Setup/FormBeatmapFileSelector.cs
Normal file
@ -0,0 +1,155 @@
|
||||
// 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.Diagnostics;
|
||||
using System.IO;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of <see cref="FormFileSelector"/> dedicated to beatmap resources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This expands on <see cref="FormFileSelector"/> by adding an intermediate step before finalisation
|
||||
/// to choose whether the selected file should be applied to the current difficulty or all difficulties in the set,
|
||||
/// the user's choice is saved in <see cref="ApplyToAllDifficulties"/> before the file selection is finalised and propagated to <see cref="FormFileSelector.Current"/>.
|
||||
/// </remarks>
|
||||
public partial class FormBeatmapFileSelector : FormFileSelector
|
||||
{
|
||||
private readonly bool beatmapHasMultipleDifficulties;
|
||||
|
||||
public readonly Bindable<bool> ApplyToAllDifficulties = new Bindable<bool>(true);
|
||||
|
||||
public FormBeatmapFileSelector(bool beatmapHasMultipleDifficulties, params string[] handledExtensions)
|
||||
: base(handledExtensions)
|
||||
{
|
||||
this.beatmapHasMultipleDifficulties = beatmapHasMultipleDifficulties;
|
||||
}
|
||||
|
||||
protected override FileChooserPopover CreatePopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath)
|
||||
{
|
||||
var popover = new BeatmapFileChooserPopover(handledExtensions, current, chooserPath, beatmapHasMultipleDifficulties);
|
||||
popover.ApplyToAllDifficulties.BindTo(ApplyToAllDifficulties);
|
||||
return popover;
|
||||
}
|
||||
|
||||
private partial class BeatmapFileChooserPopover : FileChooserPopover
|
||||
{
|
||||
private readonly bool beatmapHasMultipleDifficulties;
|
||||
|
||||
public readonly Bindable<bool> ApplyToAllDifficulties = new Bindable<bool>(true);
|
||||
|
||||
private Container selectApplicationScopeContainer = null!;
|
||||
|
||||
public BeatmapFileChooserPopover(string[] handledExtensions, Bindable<FileInfo?> current, string? chooserPath, bool beatmapHasMultipleDifficulties)
|
||||
: base(handledExtensions, current, chooserPath)
|
||||
{
|
||||
this.beatmapHasMultipleDifficulties = beatmapHasMultipleDifficulties;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider, OsuColour colours)
|
||||
{
|
||||
Add(selectApplicationScopeContainer = new InputBlockingContainer
|
||||
{
|
||||
Alpha = 0f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background6.Opacity(0.9f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Masking = true,
|
||||
CornerRadius = 10f,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background5,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0f, 10f),
|
||||
Margin = new MarginPadding(30),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = EditorSetupStrings.ApplicationScopeSelectionTitle,
|
||||
Margin = new MarginPadding { Bottom = 20f },
|
||||
},
|
||||
new RoundedButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 300f,
|
||||
Text = EditorSetupStrings.ApplyToAllDifficulties,
|
||||
Action = () =>
|
||||
{
|
||||
ApplyToAllDifficulties.Value = true;
|
||||
updateFileSelection();
|
||||
},
|
||||
BackgroundColour = colours.Red2,
|
||||
},
|
||||
new RoundedButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 300f,
|
||||
Text = EditorSetupStrings.ApplyToThisDifficulty,
|
||||
Action = () =>
|
||||
{
|
||||
ApplyToAllDifficulties.Value = false;
|
||||
updateFileSelection();
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override void OnFileSelected(FileInfo file)
|
||||
{
|
||||
if (beatmapHasMultipleDifficulties)
|
||||
selectApplicationScopeContainer.FadeIn(200, Easing.InQuint);
|
||||
else
|
||||
base.OnFileSelected(file);
|
||||
}
|
||||
|
||||
private void updateFileSelection()
|
||||
{
|
||||
Debug.Assert(FileSelector.CurrentFile.Value != null);
|
||||
base.OnFileSelected(FileSelector.CurrentFile.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
public partial class ResourcesSection : SetupSection
|
||||
{
|
||||
private FormFileSelector audioTrackChooser = null!;
|
||||
private FormFileSelector backgroundChooser = null!;
|
||||
private FormBeatmapFileSelector audioTrackChooser = null!;
|
||||
private FormBeatmapFileSelector backgroundChooser = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.ResourcesHeader;
|
||||
|
||||
@ -30,9 +32,6 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> working { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Editor? editor { get; set; }
|
||||
|
||||
@ -47,14 +46,16 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Height = 110,
|
||||
};
|
||||
|
||||
bool beatmapHasMultipleDifficulties = working.Value.BeatmapSetInfo.Beatmaps.Count > 1;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
backgroundChooser = new FormFileSelector(SupportedExtensions.IMAGE_EXTENSIONS)
|
||||
backgroundChooser = new FormBeatmapFileSelector(beatmapHasMultipleDifficulties, SupportedExtensions.IMAGE_EXTENSIONS)
|
||||
{
|
||||
Caption = GameplaySettingsStrings.BackgroundHeader,
|
||||
PlaceholderText = EditorSetupStrings.ClickToSelectBackground,
|
||||
},
|
||||
audioTrackChooser = new FormFileSelector(SupportedExtensions.AUDIO_EXTENSIONS)
|
||||
audioTrackChooser = new FormBeatmapFileSelector(beatmapHasMultipleDifficulties, SupportedExtensions.AUDIO_EXTENSIONS)
|
||||
{
|
||||
Caption = EditorSetupStrings.AudioTrack,
|
||||
PlaceholderText = EditorSetupStrings.ClickToSelectTrack,
|
||||
@ -73,75 +74,110 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
audioTrackChooser.Current.BindValueChanged(audioTrackChanged);
|
||||
}
|
||||
|
||||
public bool ChangeBackgroundImage(FileInfo source)
|
||||
public bool ChangeBackgroundImage(FileInfo source, bool applyToAllDifficulties)
|
||||
{
|
||||
if (!source.Exists)
|
||||
return false;
|
||||
|
||||
var set = working.Value.BeatmapSetInfo;
|
||||
changeResource(source, applyToAllDifficulties, @"bg",
|
||||
metadata => metadata.BackgroundFile,
|
||||
(metadata, name) => metadata.BackgroundFile = name);
|
||||
|
||||
var destination = new FileInfo($@"bg{source.Extension}");
|
||||
|
||||
// remove the previous background for now.
|
||||
// in the future we probably want to check if this is being used elsewhere (other difficulties?)
|
||||
var oldFile = set.GetFile(working.Value.Metadata.BackgroundFile);
|
||||
|
||||
using (var stream = source.OpenRead())
|
||||
{
|
||||
if (oldFile != null)
|
||||
beatmaps.DeleteFile(set, oldFile);
|
||||
|
||||
beatmaps.AddFile(set, stream, destination.Name);
|
||||
}
|
||||
|
||||
editorBeatmap.SaveState();
|
||||
|
||||
working.Value.Metadata.BackgroundFile = destination.Name;
|
||||
headerBackground.UpdateBackground();
|
||||
|
||||
editor?.ApplyToBackground(bg => bg.RefreshBackground());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ChangeAudioTrack(FileInfo source)
|
||||
public bool ChangeAudioTrack(FileInfo source, bool applyToAllDifficulties)
|
||||
{
|
||||
if (!source.Exists)
|
||||
return false;
|
||||
|
||||
changeResource(source, applyToAllDifficulties, @"audio",
|
||||
metadata => metadata.AudioFile,
|
||||
(metadata, name) => metadata.AudioFile = name);
|
||||
|
||||
music.ReloadCurrentTrack();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void changeResource(FileInfo source, bool applyToAllDifficulties, string baseFilename, Func<BeatmapMetadata, string> readFilename, Action<BeatmapMetadata, string> writeFilename)
|
||||
{
|
||||
var set = working.Value.BeatmapSetInfo;
|
||||
var beatmap = working.Value.BeatmapInfo;
|
||||
|
||||
var destination = new FileInfo($@"audio{source.Extension}");
|
||||
var otherBeatmaps = set.Beatmaps.Where(b => !b.Equals(beatmap));
|
||||
|
||||
// remove the previous audio track for now.
|
||||
// in the future we probably want to check if this is being used elsewhere (other difficulties?)
|
||||
var oldFile = set.GetFile(working.Value.Metadata.AudioFile);
|
||||
|
||||
using (var stream = source.OpenRead())
|
||||
// First, clean up files which will no longer be used.
|
||||
if (applyToAllDifficulties)
|
||||
{
|
||||
if (oldFile != null)
|
||||
beatmaps.DeleteFile(set, oldFile);
|
||||
foreach (var b in set.Beatmaps)
|
||||
{
|
||||
if (set.GetFile(readFilename(b.Metadata)) is RealmNamedFileUsage otherExistingFile)
|
||||
beatmaps.DeleteFile(set, otherExistingFile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RealmNamedFileUsage? oldFile = set.GetFile(readFilename(working.Value.Metadata));
|
||||
|
||||
beatmaps.AddFile(set, stream, destination.Name);
|
||||
if (oldFile != null)
|
||||
{
|
||||
bool oldFileUsedInOtherDiff = otherBeatmaps
|
||||
.Any(b => readFilename(b.Metadata) == oldFile.Filename);
|
||||
if (!oldFileUsedInOtherDiff)
|
||||
beatmaps.DeleteFile(set, oldFile);
|
||||
}
|
||||
}
|
||||
|
||||
working.Value.Metadata.AudioFile = destination.Name;
|
||||
// Choose a new filename that doesn't clash with any other existing files.
|
||||
string newFilename = $"{baseFilename}{source.Extension}";
|
||||
|
||||
editorBeatmap.SaveState();
|
||||
music.ReloadCurrentTrack();
|
||||
if (set.GetFile(newFilename) != null)
|
||||
{
|
||||
string[] existingFilenames = set.Files.Select(f => f.Filename).Where(f =>
|
||||
f.StartsWith(baseFilename, StringComparison.OrdinalIgnoreCase) &&
|
||||
f.EndsWith(source.Extension, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
newFilename = NamingUtils.GetNextBestFilename(existingFilenames, $@"{baseFilename}{source.Extension}");
|
||||
}
|
||||
|
||||
return true;
|
||||
using (var stream = source.OpenRead())
|
||||
beatmaps.AddFile(set, stream, newFilename);
|
||||
|
||||
if (applyToAllDifficulties)
|
||||
{
|
||||
foreach (var b in otherBeatmaps)
|
||||
{
|
||||
// This operation is quite expensive, so only perform it if required.
|
||||
if (readFilename(b.Metadata) == newFilename) continue;
|
||||
|
||||
writeFilename(b.Metadata, newFilename);
|
||||
|
||||
// save the difficulty to re-encode the .osu file, updating any reference of the old filename.
|
||||
//
|
||||
// note that this triggers a full save flow, including triggering a difficulty calculation.
|
||||
// this is not a cheap operation and should be reconsidered in the future.
|
||||
var beatmapWorking = beatmaps.GetWorkingBeatmap(b);
|
||||
beatmaps.Save(b, beatmapWorking.Beatmap, beatmapWorking.GetSkin());
|
||||
}
|
||||
}
|
||||
|
||||
writeFilename(beatmap.Metadata, newFilename);
|
||||
|
||||
// editor change handler cannot be aware of any file changes or other difficulties having their metadata modified.
|
||||
// for simplicity's sake, trigger a save when changing any resource to ensure the change is correctly saved.
|
||||
editor?.Save();
|
||||
}
|
||||
|
||||
private void backgroundChanged(ValueChangedEvent<FileInfo?> file)
|
||||
{
|
||||
if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue))
|
||||
if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue, backgroundChooser.ApplyToAllDifficulties.Value))
|
||||
backgroundChooser.Current.Value = file.OldValue;
|
||||
}
|
||||
|
||||
private void audioTrackChanged(ValueChangedEvent<FileInfo?> file)
|
||||
{
|
||||
if (file.NewValue == null || !ChangeAudioTrack(file.NewValue))
|
||||
if (file.NewValue == null || !ChangeAudioTrack(file.NewValue, audioTrackChooser.ApplyToAllDifficulties.Value))
|
||||
audioTrackChooser.Current.Value = file.OldValue;
|
||||
}
|
||||
}
|
||||
|
@ -106,9 +106,12 @@ namespace osu.Game.Screens.Menu
|
||||
foreach (var source in amplitudeSources)
|
||||
addAmplitudesFromSource(source);
|
||||
|
||||
float kiaiMultiplier = beatSyncProvider.CheckIsKiaiTime() ? 1 : 0.5f;
|
||||
|
||||
for (int i = 0; i < bars_per_visualiser; i++)
|
||||
{
|
||||
float targetAmplitude = (temporalAmplitudes[(i + indexOffset) % bars_per_visualiser]) * (beatSyncProvider.CheckIsKiaiTime() ? 1 : 0.5f);
|
||||
float targetAmplitude = (temporalAmplitudes[(i + indexOffset) % bars_per_visualiser]) * kiaiMultiplier;
|
||||
|
||||
if (targetAmplitude > frequencyAmplitudes[i])
|
||||
frequencyAmplitudes[i] = targetAmplitude;
|
||||
}
|
||||
|
@ -29,18 +29,28 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
base.LoadComplete();
|
||||
|
||||
room.PropertyChanged += onRoomPropertyChanged;
|
||||
|
||||
// Timed update required to track rooms which have hit the end time, see `HasEnded`.
|
||||
Scheduler.AddDelayed(updateRoomStatus, 1000, true);
|
||||
updateRoomStatus();
|
||||
}
|
||||
|
||||
private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(Room.Status))
|
||||
updateRoomStatus();
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(Room.Category):
|
||||
case nameof(Room.Status):
|
||||
case nameof(Room.EndDate):
|
||||
case nameof(Room.HasPassword):
|
||||
updateRoomStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRoomStatus()
|
||||
{
|
||||
this.FadeColour(colours.ForRoomCategory(room.Category) ?? room.Status.GetAppropriateColour(colours), transitionDuration);
|
||||
this.FadeColour(colours.ForRoomCategory(room.Category) ?? colours.ForRoomStatus(room), transitionDuration);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
{
|
||||
@ -35,8 +36,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
Pill.Background.Alpha = 1;
|
||||
|
||||
room.PropertyChanged += onRoomPropertyChanged;
|
||||
updateDisplay();
|
||||
|
||||
// Timed update required to track rooms which have hit the end time, see `HasEnded`.
|
||||
Scheduler.AddDelayed(updateDisplay, 1000, true);
|
||||
updateDisplay();
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
@ -46,6 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
{
|
||||
case nameof(Room.Status):
|
||||
case nameof(Room.EndDate):
|
||||
case nameof(Room.HasPassword):
|
||||
updateDisplay();
|
||||
break;
|
||||
}
|
||||
@ -53,8 +57,23 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
|
||||
private void updateDisplay()
|
||||
{
|
||||
Pill.Background.FadeColour(room.Status.GetAppropriateColour(colours), 100);
|
||||
TextFlow.Text = room.Status.Message;
|
||||
Pill.Background.FadeColour(colours.ForRoomStatus(room), 100);
|
||||
|
||||
if (room.HasEnded)
|
||||
TextFlow.Text = RoomStatusPillStrings.Ended;
|
||||
else
|
||||
{
|
||||
switch (room.Status)
|
||||
{
|
||||
case RoomStatus.Playing:
|
||||
TextFlow.Text = RoomStatusPillStrings.Playing;
|
||||
break;
|
||||
|
||||
default:
|
||||
TextFlow.Text = room.HasPassword ? RoomStatusPillStrings.OpenPrivate : RoomStatusPillStrings.Open;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -26,7 +26,6 @@ using osu.Game.Input.Bindings;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
@ -168,7 +167,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
||||
})
|
||||
};
|
||||
|
||||
if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && Room.Status is not RoomStatusEnded)
|
||||
if (Room.Type == MatchType.Playlists && Room.Host?.Id == api.LocalUser.Value.Id && Room.StartDate?.AddMinutes(5) >= DateTimeOffset.Now && !Room.HasEnded)
|
||||
{
|
||||
items.Add(new OsuMenuItem("Close playlist", MenuItemType.Destructive, () =>
|
||||
{
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Extensions.ExceptionExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
@ -31,7 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
// this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join.
|
||||
// should probably be done at a higher level, but due to the current structure of things this is the easiest place for now.
|
||||
if (room.Status is RoomStatusEnded)
|
||||
if (room.HasEnded)
|
||||
{
|
||||
onError?.Invoke("Cannot join an ended room.");
|
||||
return;
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
@ -99,7 +98,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
|
||||
if (room.Host?.Id == api.LocalUser.Value.Id)
|
||||
{
|
||||
if (deletionGracePeriodRemaining > TimeSpan.Zero && room.Status is not RoomStatusEnded)
|
||||
if (deletionGracePeriodRemaining > TimeSpan.Zero && !room.HasEnded)
|
||||
{
|
||||
closeButton.FadeIn();
|
||||
using (BeginDelayedSequence(deletionGracePeriodRemaining.Value.TotalMilliseconds))
|
||||
|
@ -16,7 +16,6 @@ using osu.Game.Input;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
@ -286,11 +285,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
DialogOverlay?.Push(new ClosePlaylistDialog(Room, () =>
|
||||
{
|
||||
var request = new ClosePlaylistRequest(Room.RoomID!.Value);
|
||||
request.Success += () =>
|
||||
{
|
||||
Room.Status = new RoomStatusEnded();
|
||||
Room.EndDate = DateTimeOffset.UtcNow;
|
||||
};
|
||||
request.Success += () => Room.EndDate = DateTimeOffset.UtcNow;
|
||||
API.Queue(request);
|
||||
}));
|
||||
}
|
||||
|
@ -112,27 +112,13 @@ namespace osu.Game.Screens.Select
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private DetachedBeatmapStore? detachedBeatmapStore { get; set; }
|
||||
|
||||
private IBindableList<BeatmapSetInfo>? detachedBeatmapSets;
|
||||
|
||||
private readonly NoResultsPlaceholder noResultsPlaceholder;
|
||||
|
||||
private IEnumerable<CarouselBeatmapSet> beatmapSets => root.Items.OfType<CarouselBeatmapSet>();
|
||||
|
||||
internal IEnumerable<BeatmapSetInfo> BeatmapSets
|
||||
{
|
||||
get => beatmapSets.Select(g => g.BeatmapSet);
|
||||
set
|
||||
{
|
||||
if (LoadState != LoadState.NotLoaded)
|
||||
throw new InvalidOperationException("If not using a realm source, beatmap sets must be set before load.");
|
||||
|
||||
detachedBeatmapSets = new BindableList<BeatmapSetInfo>(value);
|
||||
Schedule(loadNewRoot);
|
||||
}
|
||||
}
|
||||
internal IEnumerable<BeatmapSetInfo> BeatmapSets => beatmapSets.Select(g => g.BeatmapSet);
|
||||
|
||||
private void loadNewRoot()
|
||||
{
|
||||
@ -234,7 +220,7 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config, AudioManager audio, CancellationToken? cancellationToken)
|
||||
private void load(OsuConfigManager config, AudioManager audio, BeatmapStore beatmaps, CancellationToken? cancellationToken)
|
||||
{
|
||||
spinSample = audio.Samples.Get("SongSelect/random-spin");
|
||||
randomSelectSample = audio.Samples.Get(@"SongSelect/select-random");
|
||||
@ -244,15 +230,9 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
RightClickScrollingEnabled.BindValueChanged(enabled => Scroll.RightMouseScrollbar = enabled.NewValue, true);
|
||||
|
||||
if (detachedBeatmapStore != null && detachedBeatmapSets == null)
|
||||
{
|
||||
// This is performing an unnecessary second lookup on realm (in addition to the subscription), but for performance reasons
|
||||
// we require it to be separate: the subscription's initial callback (with `ChangeSet` of `null`) will run on the update
|
||||
// thread. If we attempt to detach beatmaps in this callback the game will fall over (it takes time).
|
||||
detachedBeatmapSets = detachedBeatmapStore.GetDetachedBeatmaps(cancellationToken);
|
||||
detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged);
|
||||
loadNewRoot();
|
||||
}
|
||||
detachedBeatmapSets = beatmaps.GetBeatmapSets(cancellationToken);
|
||||
detachedBeatmapSets.BindCollectionChanged(beatmapSetsChanged);
|
||||
loadNewRoot();
|
||||
}
|
||||
|
||||
private readonly HashSet<BeatmapSetInfo> setsRequiringUpdate = new HashSet<BeatmapSetInfo>();
|
||||
|
@ -10,8 +10,31 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
/// <summary>
|
||||
/// A group which ensures only one item is selected.
|
||||
/// </summary>
|
||||
public class CarouselGroup : CarouselItem
|
||||
public abstract class CarouselGroup : CarouselItem
|
||||
{
|
||||
protected CarouselGroup(List<CarouselItem>? items = null)
|
||||
{
|
||||
if (items != null) this.items = items;
|
||||
|
||||
State.ValueChanged += state =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case CarouselItemState.Collapsed:
|
||||
case CarouselItemState.NotSelected:
|
||||
this.items.ForEach(c => c.State.Value = CarouselItemState.Collapsed);
|
||||
break;
|
||||
|
||||
case CarouselItemState.Selected:
|
||||
this.items.ForEach(c =>
|
||||
{
|
||||
if (c.State.Value == CarouselItemState.Collapsed) c.State.Value = CarouselItemState.NotSelected;
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override DrawableCarouselItem? CreateDrawableRepresentation() => null;
|
||||
|
||||
public SlimReadOnlyListWrapper<CarouselItem> Items => items.AsSlimReadOnly();
|
||||
@ -67,29 +90,6 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
TotalItemsNotFiltered++;
|
||||
}
|
||||
|
||||
public CarouselGroup(List<CarouselItem>? items = null)
|
||||
{
|
||||
if (items != null) this.items = items;
|
||||
|
||||
State.ValueChanged += state =>
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case CarouselItemState.Collapsed:
|
||||
case CarouselItemState.NotSelected:
|
||||
this.items.ForEach(c => c.State.Value = CarouselItemState.Collapsed);
|
||||
break;
|
||||
|
||||
case CarouselItemState.Selected:
|
||||
this.items.ForEach(c =>
|
||||
{
|
||||
if (c.State.Value == CarouselItemState.Collapsed) c.State.Value = CarouselItemState.NotSelected;
|
||||
});
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override void Filter(FilterCriteria criteria)
|
||||
{
|
||||
base.Filter(criteria);
|
||||
|
@ -10,9 +10,9 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
/// <summary>
|
||||
/// A group which ensures at least one item is selected (if the group itself is selected).
|
||||
/// </summary>
|
||||
public class CarouselGroupEagerSelect : CarouselGroup
|
||||
public abstract class CarouselGroupEagerSelect : CarouselGroup
|
||||
{
|
||||
public CarouselGroupEagerSelect()
|
||||
protected CarouselGroupEagerSelect()
|
||||
{
|
||||
State.ValueChanged += state =>
|
||||
{
|
||||
|
@ -3,23 +3,24 @@
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using osuTK;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
public partial class BeatmapInfoWedgeV2 : VisibilityContainer
|
||||
{
|
16
osu.Game/Tests/Beatmaps/TestBeatmapStore.cs
Normal file
16
osu.Game/Tests/Beatmaps/TestBeatmapStore.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// 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.Threading;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
internal partial class TestBeatmapStore : BeatmapStore
|
||||
{
|
||||
public readonly BindableList<BeatmapSetInfo> BeatmapSets = new BindableList<BeatmapSetInfo>();
|
||||
public override IBindableList<BeatmapSetInfo> GetBeatmapSets(CancellationToken? cancellationToken) => BeatmapSets;
|
||||
}
|
||||
}
|
@ -5,6 +5,8 @@ using System;
|
||||
using Foundation;
|
||||
using Microsoft.Maui.Devices;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.iOS;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Utils;
|
||||
@ -19,6 +21,8 @@ namespace osu.iOS
|
||||
|
||||
protected override BatteryInfo CreateBatteryInfo() => new IOSBatteryInfo();
|
||||
|
||||
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorageIOS((IOSGameHost)host, defaultStorage);
|
||||
|
||||
protected override Edges SafeAreaOverrideEdges =>
|
||||
// iOS shows a home indicator at the bottom, and adds a safe area to account for this.
|
||||
// Because we have the home indicator (mostly) hidden we don't really care about drawing in this region.
|
||||
|
23
osu.iOS/OsuStorageIOS.cs
Normal file
23
osu.iOS/OsuStorageIOS.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// 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.IO;
|
||||
using osu.Framework.iOS;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.IO;
|
||||
|
||||
namespace osu.iOS
|
||||
{
|
||||
public class OsuStorageIOS : OsuStorage
|
||||
{
|
||||
private readonly IOSGameHost host;
|
||||
|
||||
public OsuStorageIOS(IOSGameHost host, Storage defaultStorage)
|
||||
: base(host, defaultStorage)
|
||||
{
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public override Storage GetExportStorage() => new IOSStorage(Path.GetTempPath(), host);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user