1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-20 14:40:19 +08:00

Compare commits

...

153 Commits

118 changed files with 2601 additions and 643 deletions
+1 -1
View File
@@ -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,
@@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.UI
protected override ResumeOverlay CreateResumeOverlay()
{
if (Mods.Any(m => m is OsuModAutopilot))
if (Mods.Any(m => m is OsuModAutopilot or OsuModTouchDevice))
return new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
return new OsuResumeOverlay();
@@ -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;
@@ -19,6 +21,7 @@ using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
@@ -27,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;
@@ -98,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);
@@ -154,6 +129,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect point", () => EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true }));
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
{
new HitCircle
@@ -200,6 +176,11 @@ namespace osu.Game.Tests.Visual.Editing
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
});
AddAssert("created difficulty has effect points", () =>
{
var effectPoint = EditorBeatmap.ControlPointInfo.EffectPoints.Single();
return effectPoint.Time == 500 && effectPoint.KiaiMode && effectPoint.ScrollSpeedBindable.IsDefault;
});
AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0);
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
@@ -219,6 +200,111 @@ namespace osu.Game.Tests.Visual.Editing
});
}
[Test]
public void TestCreateNewDifficultyWithScrollSpeed_SameRuleset()
{
string previousDifficultyName = null!;
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo));
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != previousDifficultyName;
});
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect points", () =>
{
EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 });
EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.1 });
EditorBeatmap.ControlPointInfo.Add(750, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.15 });
EditorBeatmap.ControlPointInfo.Add(1000, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.2 });
EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 });
});
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().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 != previousDifficultyName;
});
AddAssert("created difficulty has timing point", () =>
{
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
});
AddAssert("created difficulty has effect points", () =>
{
return EditorBeatmap.ControlPointInfo.EffectPoints.SequenceEqual(new[]
{
new EffectControlPoint { Time = 250, KiaiMode = false, ScrollSpeed = 0.05 },
new EffectControlPoint { Time = 500, KiaiMode = true, ScrollSpeed = 0.1 },
new EffectControlPoint { Time = 750, KiaiMode = true, ScrollSpeed = 0.15 },
new EffectControlPoint { Time = 1000, KiaiMode = false, ScrollSpeed = 0.2 },
new EffectControlPoint { Time = 1500, KiaiMode = false, ScrollSpeed = 0.3 },
});
});
}
[Test]
public void TestCreateNewDifficultyWithScrollSpeed_DifferentRuleset()
{
string firstDifficultyName = Guid.NewGuid().ToString();
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo));
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect points", () =>
{
EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 });
EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.1 });
EditorBeatmap.ControlPointInfo.Add(750, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.15 });
EditorBeatmap.ControlPointInfo.Add(1000, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.2 });
EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 });
});
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new TaikoRuleset().RulesetInfo));
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != firstDifficultyName;
});
AddAssert("created difficulty has timing point", () =>
{
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
});
AddAssert("created difficulty has effect points", () =>
{
// since this difficulty is on another ruleset, scroll speed specifications are completely reset,
// therefore discarding some effect points in the process due to being redundant.
return EditorBeatmap.ControlPointInfo.EffectPoints.SequenceEqual(new[]
{
new EffectControlPoint { Time = 500, KiaiMode = true },
new EffectControlPoint { Time = 1000, KiaiMode = false },
});
});
}
[Test]
public void TestCopyDifficulty()
{
@@ -530,5 +616,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);
@@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Editing
private TimingScreen timingScreen;
private EditorBeatmap editorBeatmap;
private BeatmapEditorChangeHandler changeHandler;
protected override bool ScrollUsingMouseWheel => false;
@@ -46,6 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
private void reloadEditorBeatmap()
{
editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(Ruleset.Value));
changeHandler = new BeatmapEditorChangeHandler(editorBeatmap);
Child = new DependencyProvidingContainer
{
@@ -53,6 +55,7 @@ namespace osu.Game.Tests.Visual.Editing
CachedDependencies = new (Type, object)[]
{
(typeof(EditorBeatmap), editorBeatmap),
(typeof(IEditorChangeHandler), changeHandler),
(typeof(IBeatSnapProvider), editorBeatmap)
},
Child = timingScreen = new TimingScreen
@@ -72,8 +75,10 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
}
// TODO: this is best-effort for now, but the comment out test below should probably be how things should work.
// Was originally working as of https://github.com/ppy/osu/pull/26141; Regressed at some point.
[Test]
public void TestSelectedRetainedOverUndo()
public void TestSelectionDismissedOnUndo()
{
AddStep("Select first timing point", () =>
{
@@ -95,25 +100,52 @@ namespace osu.Game.Tests.Visual.Editing
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
});
AddStep("simulate undo", () =>
{
var clone = editorBeatmap.ControlPointInfo.DeepClone();
AddStep("undo", () => changeHandler?.RestoreState(-1));
editorBeatmap.ControlPointInfo.Clear();
foreach (var group in clone.Groups)
{
foreach (var cp in group.ControlPoints)
editorBeatmap.ControlPointInfo.Add(group.Time, cp);
}
});
AddUntilStep("selection retained", () =>
{
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
});
AddUntilStep("selection dismissed", () => timingScreen.SelectedGroup.Value, () => Is.Null);
}
// [Test]
// public void TestSelectedRetainedOverUndo()
// {
// AddStep("Select first timing point", () =>
// {
// InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().First());
// InputManager.Click(MouseButton.Left);
// });
//
// AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 2170);
// AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 2170);
//
// AddStep("Adjust offset", () =>
// {
// InputManager.MoveMouseTo(timingScreen.ChildrenOfType<TimingAdjustButton>().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0));
// InputManager.Click(MouseButton.Left);
// });
//
// AddUntilStep("wait for offset changed", () =>
// {
// return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
// });
//
// AddStep("undo", () => changeHandler?.RestoreState(-1));
//
// AddUntilStep("selection retained", () =>
// {
// return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
// });
//
// AddAssert("check group count", () => editorBeatmap.ControlPointInfo.Groups.Count, () => Is.EqualTo(10));
//
// AddStep("Adjust offset", () =>
// {
// InputManager.MoveMouseTo(timingScreen.ChildrenOfType<TimingAdjustButton>().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0));
// InputManager.Click(MouseButton.Left);
// });
//
// AddAssert("check group count", () => editorBeatmap.ControlPointInfo.Groups.Count, () => Is.EqualTo(10));
// }
[Test]
public void TestScrollControlGroupIntoView()
{
@@ -0,0 +1,15 @@
// 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 NUnit.Framework;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public partial class TestSceneIntroChristmas : IntroTestScene
{
protected override bool IntroReliesOnTrack => true;
protected override IntroScreen CreateScreen() => new IntroChristmas();
}
}
@@ -0,0 +1,46 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.Menus
{
public partial class TestSceneMainMenuSeasonalLighting : OsuTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("prepare beatmap", () =>
{
var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77");
Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo!.Value.Beatmaps.First());
});
AddStep("create lighting", () => Child = new MainMenuSeasonalLighting());
AddStep("restart beatmap", () =>
{
Beatmap.Value.Track.Start();
Beatmap.Value.Track.Seek(4000);
});
}
[Test]
public void TestBasic()
{
}
}
}
@@ -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()
@@ -23,6 +23,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps.IO;
@@ -212,6 +213,33 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
}
[Test]
public void TestGameplaySettingsDoesNotExpandWhenSkinOverlayPresent()
{
advanceToSongSelect();
openSkinEditor();
AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() });
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
switchToGameplayScene();
AddUntilStep("wait for settings", () => getPlayerSettingsOverlay() != null);
AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
AddStep("move cursor to right of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight));
AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
toggleSkinEditor();
AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(1)));
AddUntilStep("settings visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.GreaterThan(0));
AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0)));
AddUntilStep("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
PlayerSettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PlayerSettingsOverlay>().SingleOrDefault();
}
[Test]
public void TestCinemaModRemovedOnEnteringGameplay()
{
@@ -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;
});
@@ -13,9 +13,12 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
@@ -23,6 +26,8 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
@@ -63,7 +68,7 @@ namespace osu.Game.Tests.Visual.SongSelect
return 336; // recommended star rating of 2
case 1:
return 928; // SR 3
return 973; // SR 3
case 2:
return 1905; // SR 4
@@ -170,6 +175,45 @@ namespace osu.Game.Tests.Visual.SongSelect
presentAndConfirm(() => maniaSet, 5);
}
[Test]
public void TestBeatmapListingFilter()
{
AddStep("set playmode to taiko", () => ((DummyAPIAccess)API).LocalUser.Value.PlayMode = "taiko");
AddStep("open beatmap listing", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.B);
InputManager.ReleaseKey(Key.B);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for load", () => Game.ChildrenOfType<BeatmapListingOverlay>().SingleOrDefault()?.IsLoaded, () => Is.True);
checkRecommendedDifficulty(3);
AddStep("change mode filter to osu!", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(1).TriggerClick());
checkRecommendedDifficulty(2);
AddStep("change mode filter to osu!taiko", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(2).TriggerClick());
checkRecommendedDifficulty(3);
AddStep("change mode filter to osu!catch", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(3).TriggerClick());
checkRecommendedDifficulty(4);
AddStep("change mode filter to osu!mania", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(4).TriggerClick());
checkRecommendedDifficulty(5);
void checkRecommendedDifficulty(double starRating)
=> AddAssert($"recommended difficulty is {starRating}",
() => Game.ChildrenOfType<BeatmapSearchGeneralFilterRow>().Single().ChildrenOfType<OsuSpriteText>().ElementAt(1).Text.ToString(),
() => Is.EqualTo($"Recommended difficulty ({starRating.FormatStarRating()})"));
}
private BeatmapSetInfo importBeatmapSet(IEnumerable<RulesetInfo> difficultyRulesets)
{
var rulesets = difficultyRulesets.ToArray();
@@ -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
{
@@ -4,22 +4,31 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Menu;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneOsuLogo : OsuTestScene
{
private OsuLogo? logo;
[Test]
public void TestBasic()
{
AddStep("Add logo", () =>
{
Child = new OsuLogo
Child = logo = new OsuLogo
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
AddSliderStep("scale", 0.1, 2, 1, scale =>
{
if (logo != null)
Child.Scale = new Vector2((float)scale);
});
}
}
}
+2 -1
View File
@@ -15,6 +15,7 @@ using osu.Game.Graphics;
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Screens.Menu;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -207,7 +208,7 @@ namespace osu.Game.Tournament.Components
Children = new Drawable[]
{
new DiffPiece(stats),
new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.00}{srExtra}"))
new DiffPiece(("Star Rating", $"{beatmap.StarRating.FormatStarRating()}{srExtra}"))
}
},
new FillFlowContainer
+2
View File
@@ -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();
+1
View File
@@ -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;
}
-3
View File
@@ -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();
+12
View File
@@ -15,6 +15,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.Extensions;
@@ -154,9 +155,20 @@ namespace osu.Game.Beatmaps
DifficultyName = NamingUtils.GetNextBestName(targetBeatmapSet.Beatmaps.Select(b => b.DifficultyName), "New Difficulty")
};
var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo };
foreach (var timingPoint in referenceWorkingBeatmap.Beatmap.ControlPointInfo.TimingPoints)
newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone());
foreach (var effectPoint in referenceWorkingBeatmap.Beatmap.ControlPointInfo.EffectPoints)
{
var clonedEffectPoint = (EffectControlPoint)effectPoint.DeepClone();
if (!rulesetInfo.Equals(referenceWorkingBeatmap.BeatmapInfo.Ruleset))
clonedEffectPoint.ScrollSpeedBindable.SetDefault();
newBeatmap.ControlPointInfo.Add(clonedEffectPoint.Time, clonedEffectPoint);
}
return addDifficultyToSet(targetBeatmapSet, newBeatmap, referenceWorkingBeatmap.Skin);
}
+15 -9
View File
@@ -1,12 +1,9 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
@@ -23,10 +20,12 @@ namespace osu.Game.Beatmaps
/// </summary>
public partial class DifficultyRecommender : Component
{
public event Action? StarRatingUpdated;
private readonly LocalUserStatisticsProvider statisticsProvider;
[Resolved]
private Bindable<RulesetInfo> gameRuleset { get; set; }
private Bindable<RulesetInfo> gameRuleset { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
@@ -78,10 +77,18 @@ namespace osu.Game.Beatmaps
private void updateMapping(RulesetInfo ruleset, UserStatistics statistics)
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[ruleset.ShortName] = Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195;
// algorithm taken from https://github.com/ppy/osu-web/blob/027026fccc91525e39cee5d2f369f1b343eb1bf1/app/Models/UserStatistics/Model.php#L93-L94
recommendedDifficultyMapping[ruleset.ShortName] =
ruleset.ShortName == @"taiko"
? Math.Pow((double)(statistics.PP ?? 0), 0.35) * 0.27
: Math.Pow((double)(statistics.PP ?? 0), 0.4) * 0.195;
StarRatingUpdated?.Invoke();
}
public double? GetRecommendedStarRatingFor(RulesetInfo ruleset)
=> recommendedDifficultyMapping.TryGetValue(ruleset.ShortName, out double starRating) ? starRating : null;
/// <summary>
/// Find the recommended difficulty from a selection of available difficulties for the current local user.
/// </summary>
@@ -90,15 +97,14 @@ namespace osu.Game.Beatmaps
/// </remarks>
/// <param name="beatmaps">A collection of beatmaps to select a difficulty from.</param>
/// <returns>The recommended difficulty, or null if a recommendation could not be provided.</returns>
[CanBeNull]
public BeatmapInfo GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
public BeatmapInfo? GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
{
foreach (string r in orderedRulesets)
{
if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation))
continue;
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b =>
BeatmapInfo? beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r, StringComparison.Ordinal)).MinBy(b =>
{
double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
@@ -5,7 +5,6 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -14,6 +13,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -156,7 +156,7 @@ namespace osu.Game.Beatmaps.Drawables
displayedStars.BindValueChanged(s =>
{
starsText.Text = s.NewValue < 0 ? "-" : s.NewValue.ToLocalisableString("0.00");
starsText.Text = s.NewValue < 0 ? "-" : s.NewValue.FormatStarRating();
background.Colour = colours.ForStarDifficulty(s.NewValue);
@@ -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}"));
+2
View File
@@ -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>
+3 -1
View File
@@ -202,6 +202,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.HideCountryFlags, false);
SetDefault(OsuSetting.MultiplayerRoomFilter, RoomPermissionsFilter.All);
SetDefault(OsuSetting.MultiplayerShowInProgressFilter, true);
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
@@ -447,6 +448,7 @@ namespace osu.Game.Configuration
EditorRotationOrigin,
EditorTimelineShowBreaks,
EditorAdjustExistingObjectsOnTimingChanges,
AlwaysRequireHoldingForPause
AlwaysRequireHoldingForPause,
MultiplayerShowInProgressFilter,
}
}
+35
View 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);
}
}
+2 -1
View File
@@ -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();
+21
View File
@@ -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;
}
}
}
+5
View 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>
@@ -62,8 +62,12 @@ namespace osu.Game.IO.Serialization.Converters
if (tok["$type"] == null)
throw new JsonException("Expected $type token.");
string typeName = lookupTable[(int)tok["$type"]];
var instance = (T)Activator.CreateInstance(Type.GetType(typeName).AsNonNull())!;
// Prevent instantiation of types that do not inherit the type targetted by this converter
Type type = Type.GetType(lookupTable[(int)tok["$type"]]).AsNonNull();
if (!type.IsAssignableTo(typeof(T)))
continue;
var instance = (T)Activator.CreateInstance(type)!;
serializer.Populate(itemReader, instance);
list.Add(instance);
@@ -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>
+31 -1
View File
@@ -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}";
}
}
@@ -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;
+13 -8
View File
@@ -11,28 +11,33 @@ namespace osu.Game.Online.Rooms
{
public class GetRoomsRequest : APIRequest<List<Room>>
{
private readonly RoomStatusFilter status;
private readonly RoomModeFilter mode;
private readonly RoomStatusFilter? status;
private readonly string category;
public GetRoomsRequest(RoomStatusFilter status, string category)
public GetRoomsRequest(FilterCriteria filterCriteria)
{
this.status = status;
this.category = category;
mode = filterCriteria.Mode;
category = filterCriteria.Category;
status = filterCriteria.Status;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
if (status != RoomStatusFilter.Open)
req.AddParameter("mode", status.ToString().ToSnakeCase().ToLowerInvariant());
if (mode != RoomModeFilter.Open)
req.AddParameter(@"mode", mode.ToString().ToSnakeCase().ToLowerInvariant());
if (status != null)
req.AddParameter(@"status", status.Value.ToString().ToSnakeCase().ToLowerInvariant());
if (!string.IsNullOrEmpty(category))
req.AddParameter("category", category);
req.AddParameter(@"category", category);
return req;
}
protected override string Target => "rooms";
protected override string Target => @"rooms";
}
}
+13 -17
View File
@@ -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
{
+3 -11
View File
@@ -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;
}
}
+1 -1
View File
@@ -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());
@@ -1,8 +1,6 @@
// 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 System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -29,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary>
/// Any time the text box receives key events (even while masked).
/// </summary>
public Action TypingStarted;
public Action? TypingStarted;
public Bindable<string> Query => textBox.Current;
@@ -51,7 +49,7 @@ namespace osu.Game.Overlays.BeatmapListing
public Bindable<SearchExplicit> ExplicitContent => explicitContentFilter.Current;
public APIBeatmapSet BeatmapSet
public APIBeatmapSet? BeatmapSet
{
set
{
@@ -67,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapListing
}
private readonly BeatmapSearchTextBox textBox;
private readonly BeatmapSearchMultipleSelectionFilterRow<SearchGeneral> generalFilter;
private readonly BeatmapSearchGeneralFilterRow generalFilter;
private readonly BeatmapSearchRulesetFilterRow modeFilter;
private readonly BeatmapSearchFilterRow<SearchCategory> categoryFilter;
private readonly BeatmapSearchFilterRow<SearchGenre> genreFilter;
@@ -151,7 +149,7 @@ namespace osu.Game.Overlays.BeatmapListing
categoryFilter.Current.Value = SearchCategory.Leaderboard;
}
private IBindable<bool> allowExplicitContent;
private IBindable<bool> allowExplicitContent = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuConfigManager config)
@@ -165,6 +163,13 @@ namespace osu.Game.Overlays.BeatmapListing
}, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
generalFilter.Ruleset.BindTo(Ruleset);
}
public void TakeFocus() => textBox.TakeFocus();
private partial class BeatmapSearchTextBox : BasicSearchTextBox
@@ -172,7 +177,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary>
/// Any time the text box receives key events (even while masked).
/// </summary>
public Action TextChanged;
public Action? TextChanged;
protected override Color4 SelectionColour => Color4.Gray;
@@ -1,18 +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.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays.Dialog;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Utils;
using osuTK.Graphics;
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
@@ -20,27 +25,97 @@ namespace osu.Game.Overlays.BeatmapListing
{
public partial class BeatmapSearchGeneralFilterRow : BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>
{
public readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
public BeatmapSearchGeneralFilterRow()
: base(BeatmapsStrings.ListingSearchFiltersGeneral)
{
}
protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter();
protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter
{
Ruleset = { BindTarget = Ruleset }
};
private partial class GeneralFilter : MultipleSelectionFilter
{
public readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
protected override MultipleSelectionFilterTabItem CreateTabItem(SearchGeneral value)
{
if (value == SearchGeneral.FeaturedArtists)
return new FeaturedArtistsTabItem();
switch (value)
{
case SearchGeneral.Recommended:
return new RecommendedDifficultyTabItem
{
Ruleset = { BindTarget = Ruleset }
};
return new MultipleSelectionFilterTabItem(value);
case SearchGeneral.FeaturedArtists:
return new FeaturedArtistsTabItem();
default:
return new MultipleSelectionFilterTabItem(value);
}
}
}
private partial class RecommendedDifficultyTabItem : MultipleSelectionFilterTabItem
{
public readonly IBindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
[Resolved]
private DifficultyRecommender? recommender { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
public RecommendedDifficultyTabItem()
: base(SearchGeneral.Recommended)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
if (recommender != null)
recommender.StarRatingUpdated += updateText;
Ruleset.BindValueChanged(_ => updateText(), true);
}
private void updateText()
{
// fallback to profile default game mode if beatmap listing mode filter is set to Any
// TODO: find a way to update `PlayMode` when the profile default game mode has changed
RulesetInfo? ruleset = Ruleset.Value.IsLegacyRuleset() ? Ruleset.Value : rulesets.GetRuleset(api.LocalUser.Value.PlayMode);
if (ruleset == null) return;
double? starRating = recommender?.GetRecommendedStarRatingFor(ruleset);
if (starRating != null)
Text.Text = LocalisableString.Interpolate($"{Value.GetLocalisableDescription()} ({starRating.Value.FormatStarRating()})");
else
Text.Text = Value.GetLocalisableDescription();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (recommender != null)
recommender.StarRatingUpdated -= updateText;
}
}
private partial class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
{
private Bindable<bool> disclaimerShown;
private Bindable<bool> disclaimerShown = null!;
public FeaturedArtistsTabItem()
: base(SearchGeneral.FeaturedArtists)
@@ -48,13 +123,13 @@ namespace osu.Game.Overlays.BeatmapListing
}
[Resolved]
private OsuColour colours { get; set; }
private OsuColour colours { get; set; } = null!;
[Resolved]
private SessionStatics sessionStatics { get; set; }
private SessionStatics sessionStatics { get; set; } = null!;
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
protected override void LoadComplete()
{
@@ -21,6 +21,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Overlays.BeatmapSet
@@ -185,7 +186,7 @@ namespace osu.Game.Overlays.BeatmapSet
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.00");
starRating.Text = beatmap.StarRating.FormatStarRating();
starRatingContainer.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },
@@ -12,7 +12,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@@ -35,7 +34,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private readonly FontUsage smallFont = OsuFont.GetFont(size: 16);
private readonly FontUsage largeFont = OsuFont.GetFont(size: 22, weight: FontWeight.Light);
private readonly TextColumn totalScoreColumn;
private readonly TotalScoreColumn totalScoreColumn;
private readonly TextColumn accuracyColumn;
private readonly TextColumn maxComboColumn;
private readonly TextColumn ppColumn;
@@ -67,7 +66,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
Spacing = new Vector2(margin, 0),
Children = new Drawable[]
{
totalScoreColumn = new TextColumn(BeatmapsetsStrings.ShowScoreboardHeadersScoreTotal, largeFont, top_columns_min_width),
totalScoreColumn = new TotalScoreColumn(BeatmapsetsStrings.ShowScoreboardHeadersScoreTotal, largeFont, top_columns_min_width),
accuracyColumn = new TextColumn(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, largeFont, top_columns_min_width),
maxComboColumn = new TextColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, largeFont, top_columns_min_width)
}
@@ -226,7 +225,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
}
private partial class TextColumn : InfoColumn, IHasCurrentValue<string>
private partial class TextColumn : InfoColumn
{
private readonly OsuTextFlowContainer text;
@@ -249,18 +248,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
}
private Bindable<string> current;
public Bindable<string> Current
{
get => current;
set
{
text.Clear();
text.AddText(value.Value, t => t.Current = current = value);
}
}
public TextColumn(LocalisableString title, FontUsage font, float? minWidth = null)
: this(title, new OsuTextFlowContainer(t => t.Font = font)
{
@@ -276,6 +263,28 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
}
private partial class TotalScoreColumn : TextColumn
{
private readonly BindableWithCurrent<string> current = new BindableWithCurrent<string>();
public TotalScoreColumn(LocalisableString title, FontUsage font, float? minWidth = null)
: base(title, font, minWidth)
{
}
public Bindable<string> Current
{
get => current;
set => current.Current = value;
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => Text = current.Value, true);
}
}
private partial class ModsInfoColumn : InfoColumn
{
private readonly FillFlowContainer modsContainer;
+3 -2
View File
@@ -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
+50 -31
View File
@@ -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);
}
}
}
@@ -4,12 +4,14 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
@@ -162,18 +164,77 @@ namespace osu.Game.Overlays.Profile.Header.Components
scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0;
detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
var rankHighest = user?.RankHighest;
detailGlobalRank.ContentTooltipText = rankHighest != null
? UsersStrings.ShowRankHighest(rankHighest.Rank.ToLocalisableString("\\##,##0"), rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"))
: string.Empty;
detailGlobalRank.ContentTooltipText = getGlobalRankTooltipText(user);
detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
detailCountryRank.ContentTooltipText = getCountryRankTooltipText(user);
rankGraph.Statistics.Value = user?.Statistics;
}
private static LocalisableString getGlobalRankTooltipText(APIUser? user)
{
var rankHighest = user?.RankHighest;
var variants = user?.Statistics?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
if (variant.GlobalRank != null)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.GlobalRank.ToLocalisableString("\\##,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
}
if (rankHighest != null)
{
var rankHighestText = UsersStrings.ShowRankHighest(
rankHighest.Rank.ToLocalisableString("\\##,##0"),
rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"));
if (result == null)
result = rankHighestText;
else
result = LocalisableString.Interpolate($"{result}\n{rankHighestText}");
}
return result ?? default;
}
private static LocalisableString getCountryRankTooltipText(APIUser? user)
{
var variants = user?.Statistics?.Variants;
LocalisableString? result = null;
if (variants?.Count > 0)
{
foreach (var variant in variants)
{
if (variant.CountryRank != null)
{
var variantText = LocalisableString.Interpolate($"{variant.VariantType.GetLocalisableDescription()}: {variant.CountryRank.ToLocalisableString("\\##,##0")}");
if (result == null)
result = variantText;
else
result = LocalisableString.Interpolate($"{result}\n{variantText}");
}
}
}
return result ?? default;
}
private partial class ScoreRankInfo : CompositeDrawable
{
private readonly OsuSpriteText rankCount;
@@ -57,10 +57,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
LabelText = MouseSettingsStrings.HighPrecisionMouse,
TooltipText = MouseSettingsStrings.HighPrecisionMouseTooltip,
Current = relativeMode,
Keywords = new[] { @"raw", @"input", @"relative", @"cursor" }
Keywords = new[] { @"raw", @"input", @"relative", @"cursor", "sensitivity", "speed", "velocity" },
},
new SensitivitySetting
{
Keywords = new[] { "speed", "velocity" },
LabelText = MouseSettingsStrings.CursorSensitivity,
Current = localSensitivity
},
@@ -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";
}
}
}
+66 -1
View File
@@ -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;
+16
View File
@@ -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);
@@ -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);
}
}
}
}
+81 -45
View File
@@ -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;
}
}
@@ -34,6 +34,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
[Resolved]
private IEditorChangeHandler? editorChangeHandler { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours, OverlayColourProvider colourProvider)
{
@@ -110,6 +113,9 @@ namespace osu.Game.Screens.Edit.Timing
}
},
};
if (editorChangeHandler != null)
editorChangeHandler.OnStateChange += onUndoRedo;
}
protected override void LoadComplete()
@@ -185,5 +191,21 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.Value = group;
}
private void onUndoRedo()
{
// Best effort. We have no tracking of control points through undo/redo changes.
// If we don't deselect, things like offset changes could spawn groups to be added from previous states (see https://github.com/ppy/osu/issues/31098).
if (selectedGroup.Value != null && !Beatmap.ControlPointInfo.Groups.Contains(selectedGroup.Value))
selectedGroup.Value = null;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (editorChangeHandler != null)
editorChangeHandler.OnStateChange -= onUndoRedo;
}
}
}
@@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -21,6 +22,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Timing.RowAttributes;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Timing
{
@@ -177,7 +179,7 @@ namespace osu.Game.Screens.Edit.Timing
private readonly BindableWithCurrent<ControlPointGroup> current = new BindableWithCurrent<ControlPointGroup>();
private Box background = null!;
private Box currentIndicator = null!;
private Drawable currentIndicator = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -202,7 +204,7 @@ namespace osu.Game.Screens.Edit.Timing
{
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
background = new Box
{
@@ -210,11 +212,26 @@ namespace osu.Game.Screens.Edit.Timing
Colour = colourProvider.Background1,
Alpha = 0,
},
currentIndicator = new Box
currentIndicator = new Container
{
RelativeSizeAxes = Axes.Y,
Width = 5,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Y,
Width = 5,
},
new Box
{
RelativeSizeAxes = Axes.Y,
Blending = BlendingParameters.Additive,
X = 5,
Width = 150,
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.1f), Color4.White.Opacity(0))
},
}
},
new Container
{
@@ -281,14 +298,8 @@ namespace osu.Game.Screens.Edit.Timing
bool hasCurrentTimingPoint = activeTimingPoint.Value != null && current.Value.ControlPoints.Contains(activeTimingPoint.Value);
bool hasCurrentEffectPoint = activeEffectPoint.Value != null && current.Value.ControlPoints.Contains(activeEffectPoint.Value);
if (IsHovered || isSelected)
background.FadeIn(100, Easing.OutQuint);
else if (hasCurrentTimingPoint || hasCurrentEffectPoint)
background.FadeTo(0.2f, 100, Easing.OutQuint);
else
background.FadeOut(100, Easing.OutQuint);
background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1;
background.FadeTo(IsHovered || isSelected ? 1 : 0, 100, Easing.OutQuint);
background.FadeColour(isSelected ? colourProvider.Colour3 : colourProvider.Background1, 100, Easing.OutQuint);
if (hasCurrentTimingPoint || hasCurrentEffectPoint)
{
+3
View File
@@ -37,6 +37,9 @@ namespace osu.Game.Screens
private IntroScreen getIntroSequence()
{
if (SeasonalUI.ENABLED)
return new IntroChristmas(createMainMenu);
if (introSequence == IntroSequence.Random)
introSequence = (IntroSequence)RNG.Next(0, (int)IntroSequence.Random);
+328
View File
@@ -0,0 +1,328 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
public partial class IntroChristmas : IntroScreen
{
protected override string BeatmapHash => "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77";
protected override string BeatmapFile => "christmas2024.osz";
private const double beat_length = 60000 / 172.0;
private const double offset = 5924;
protected override string SeeyaSampleName => "Intro/Welcome/seeya";
private TrianglesIntroSequence intro = null!;
public IntroChristmas(Func<MainMenu>? createNextScreen = null)
: base(createNextScreen)
{
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
if (!resuming)
{
PrepareMenuLoad();
var decouplingClock = new DecouplingFramedClock(UsingThemedIntro ? Track : null);
LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground())
{
RelativeSizeAxes = Axes.Both,
Clock = new InterpolatingFramedClock(decouplingClock),
LoadMenu = LoadMenu
}, _ =>
{
AddInternal(intro);
// There is a chance that the intro timed out before being displayed, and this scheduled callback could
// happen during the outro rather than intro.
// In such a scenario, we don't want to play the intro sample, nor attempt to start the intro track
// (that may have already been since disposed by MusicController).
if (DidLoadMenu)
return;
// If the user has requested no theme, fallback to the same intro voice and delay as IntroCircles.
// The triangles intro voice and theme are combined which makes it impossible to use.
StartTrack();
// no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure.
decouplingClock.Start();
});
}
}
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);
// important as there is a clock attached to a track which will likely be disposed before returning to this screen.
intro.Expire();
}
private partial class TrianglesIntroSequence : CompositeDrawable
{
private readonly OsuLogo logo;
private readonly Action showBackgroundAction;
private OsuSpriteText welcomeText = null!;
private Container logoContainerSecondary = null!;
private LazerLogo lazerLogo = null!;
private Drawable triangles = null!;
public Action LoadMenu = null!;
[Resolved]
private OsuGameBase game { get; set; } = null!;
public TrianglesIntroSequence(OsuLogo logo, Action showBackgroundAction)
{
this.logo = logo;
this.showBackgroundAction = showBackgroundAction;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new[]
{
welcomeText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 10 },
Font = OsuFont.GetFont(weight: FontWeight.Light, size: 42),
Alpha = 1,
Spacing = new Vector2(5),
},
logoContainerSecondary = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = lazerLogo = new LazerLogo
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
}
},
triangles = new CircularContainer
{
Alpha = 0,
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(960),
Child = new GlitchingTriangles
{
RelativeSizeAxes = Axes.Both,
},
}
};
}
private static double getTimeForBeat(int beat) => offset + beat_length * beat;
protected override void LoadComplete()
{
base.LoadComplete();
lazerLogo.Hide();
using (BeginAbsoluteSequence(0))
{
using (BeginDelayedSequence(getTimeForBeat(-16)))
welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!");
using (BeginDelayedSequence(getTimeForBeat(-15)))
welcomeText.FadeIn().OnComplete(t => t.Text = "");
using (BeginDelayedSequence(getTimeForBeat(-14)))
welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!");
using (BeginDelayedSequence(getTimeForBeat(-13)))
welcomeText.FadeIn().OnComplete(t => t.Text = "");
using (BeginDelayedSequence(getTimeForBeat(-12)))
welcomeText.FadeIn().OnComplete(t => t.Text = "merry christmas!");
using (BeginDelayedSequence(getTimeForBeat(-11)))
welcomeText.FadeIn().OnComplete(t => t.Text = "");
using (BeginDelayedSequence(getTimeForBeat(-10)))
welcomeText.FadeIn().OnComplete(t => t.Text = "merry osumas!");
using (BeginDelayedSequence(getTimeForBeat(-9)))
{
welcomeText.FadeIn().OnComplete(t => t.Text = "");
}
lazerLogo.Scale = new Vector2(0.2f);
triangles.Scale = new Vector2(0.2f);
for (int i = 0; i < 8; i++)
{
using (BeginDelayedSequence(getTimeForBeat(-8 + i)))
{
triangles.FadeIn();
lazerLogo.ScaleTo(new Vector2(0.2f + (i + 1) / 8f * 0.3f), beat_length * 1, Easing.OutQuint);
triangles.ScaleTo(new Vector2(0.2f + (i + 1) / 8f * 0.3f), beat_length * 1, Easing.OutQuint);
lazerLogo.FadeTo((i + 1) * 0.06f);
lazerLogo.TransformTo(nameof(LazerLogo.Progress), (i + 1) / 10f);
}
}
GameWideFlash flash = new GameWideFlash();
using (BeginDelayedSequence(getTimeForBeat(-2)))
{
lazerLogo.FadeIn().OnComplete(_ => game.Add(flash));
}
flash.FadeInCompleted = () =>
{
logoContainerSecondary.Remove(lazerLogo, true);
triangles.FadeOut();
logo.FadeIn();
showBackgroundAction();
LoadMenu();
};
}
}
private partial class GameWideFlash : Box
{
public Action? FadeInCompleted;
public GameWideFlash()
{
Colour = Color4.White;
RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive;
}
protected override void LoadComplete()
{
base.LoadComplete();
Alpha = 0;
this.FadeTo(0.5f, beat_length * 2, Easing.In)
.OnComplete(_ => FadeInCompleted?.Invoke());
this.Delay(beat_length * 2)
.Then()
.FadeOutFromOne(3000, Easing.OutQuint);
}
}
private partial class LazerLogo : CompositeDrawable
{
private LogoAnimation highlight = null!;
private LogoAnimation background = null!;
public float Progress
{
get => background.AnimationProgress;
set
{
background.AnimationProgress = value;
highlight.AnimationProgress = value;
}
}
public LazerLogo()
{
Size = new Vector2(960);
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
InternalChildren = new Drawable[]
{
highlight = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-highlight"),
Colour = Color4.White,
},
background = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-background"),
Colour = OsuColour.Gray(0.6f),
},
};
}
}
private partial class GlitchingTriangles : BeatSyncedContainer
{
private int beatsHandled;
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
Divisor = beatsHandled < 4 ? 1 : 4;
for (int i = 0; i < (beatsHandled + 1); i++)
{
float angle = (float)(RNG.NextDouble() * 2 * Math.PI);
float randomRadius = (float)(Math.Sqrt(RNG.NextDouble()));
float x = 0.5f + 0.5f * randomRadius * (float)Math.Cos(angle);
float y = 0.5f + 0.5f * randomRadius * (float)Math.Sin(angle);
Color4 christmasColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2;
Drawable triangle = new Triangle
{
Size = new Vector2(RNG.NextSingle() + 1.2f) * 80,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Both,
Position = new Vector2(x, y),
Colour = christmasColour
};
if (beatsHandled >= 10)
triangle.Blending = BlendingParameters.Additive;
AddInternal(triangle);
triangle
.ScaleTo(0.9f)
.ScaleTo(1, beat_length / 2, Easing.Out);
triangle.FadeInFromZero(100, Easing.OutQuint);
}
beatsHandled += 1;
}
}
}
}
}
+1 -1
View File
@@ -207,7 +207,7 @@ namespace osu.Game.Screens.Menu
Text = NotificationsStrings.AudioPlaybackIssue
});
}
}, 5000);
}, 8000);
}
public override void OnResuming(ScreenTransitionEvent e)
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
@@ -36,6 +37,8 @@ namespace osu.Game.Screens.Menu
X = -250,
},
};
Colour = SeasonalUI.ENABLED ? SeasonalUI.AMBIENT_COLOUR_2 : Color4.White;
}
private bool isTriggered;
+4 -1
View File
@@ -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;
}
+3 -1
View File
@@ -124,6 +124,7 @@ namespace osu.Game.Screens.Menu
AddRangeInternal(new[]
{
SeasonalUI.ENABLED ? new MainMenuSeasonalLighting() : Empty(),
buttonsContainer = new ParallaxContainer
{
ParallaxAmount = 0.01f,
@@ -166,7 +167,8 @@ namespace osu.Game.Screens.Menu
Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 15, Top = 5 }
},
new KiaiMenuFountains(),
// For now, this is too much alongside the seasonal lighting.
SeasonalUI.ENABLED ? Empty() : new KiaiMenuFountains(),
bottomElementsFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@@ -0,0 +1,188 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
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.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
public partial class MainMenuSeasonalLighting : CompositeDrawable
{
private IBindable<WorkingBeatmap> working = null!;
private InterpolatingFramedClock beatmapClock = null!;
private List<HitObject> hitObjects = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
public MainMenuSeasonalLighting()
{
RelativeChildSize = new Vector2(512, 384);
RelativeSizeAxes = Axes.X;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> working)
{
this.working = working.GetBoundCopy();
this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true);
}
private void updateBeatmap()
{
lastObjectIndex = null;
beatmapClock = new InterpolatingFramedClock(new FramedClock(working.Value.Track));
hitObjects = working.Value.GetPlayableBeatmap(rulesets.GetRuleset(0)).HitObjects.SelectMany(h => h.NestedHitObjects.Prepend(h))
.OrderBy(h => h.StartTime)
.ToList();
}
private int? lastObjectIndex;
protected override void Update()
{
base.Update();
Height = DrawWidth / 16 * 10;
beatmapClock.ProcessFrame();
// intentionally slightly early since we are doing fades on the lighting.
double time = beatmapClock.CurrentTime + 50;
// handle seeks or OOB by skipping to current.
if (lastObjectIndex == null || lastObjectIndex >= hitObjects.Count || (lastObjectIndex >= 0 && hitObjects[lastObjectIndex.Value].StartTime > time)
|| Math.Abs(beatmapClock.ElapsedFrameTime) > 500)
lastObjectIndex = hitObjects.Count(h => h.StartTime < time) - 1;
while (lastObjectIndex < hitObjects.Count - 1)
{
var h = hitObjects[lastObjectIndex.Value + 1];
if (h.StartTime > time)
break;
// Don't add lighting if the game is running too slow.
if (Clock.ElapsedFrameTime < 20)
addLight(h);
lastObjectIndex++;
}
}
private void addLight(HitObject h)
{
var light = new Light
{
RelativePositionAxes = Axes.Both,
Position = ((IHasPosition)h).Position
};
AddInternal(light);
if (h.GetType().Name.Contains("Tick"))
{
light.Colour = SeasonalUI.AMBIENT_COLOUR_1;
light.Scale = new Vector2(0.5f);
light
.FadeInFromZero(250)
.Then()
.FadeOutFromOne(1000, Easing.Out);
light.MoveToOffset(new Vector2(RNG.Next(-20, 20), RNG.Next(-20, 20)), 1400, Easing.Out);
}
else
{
// default green
Color4 col = SeasonalUI.PRIMARY_COLOUR_2;
// whistle red
if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE))
col = SeasonalUI.PRIMARY_COLOUR_1;
// clap is third colour
else if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP))
col = SeasonalUI.AMBIENT_COLOUR_1;
light.Colour = col;
// finish larger lighting
if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH))
light.Scale = new Vector2(3);
light
.FadeInFromZero(150)
.Then()
.FadeOutFromOne(1000, Easing.In);
light.Expire();
}
}
public partial class Light : CompositeDrawable
{
private readonly Circle circle;
public new Color4 Colour
{
set
{
circle.Colour = value.Darken(0.8f);
circle.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = value,
Radius = 80,
};
}
}
public Light()
{
InternalChildren = new Drawable[]
{
circle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(12),
Colour = SeasonalUI.AMBIENT_COLOUR_1,
Blending = BlendingParameters.Additive,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = SeasonalUI.AMBIENT_COLOUR_2,
Radius = 80,
}
}
};
Origin = Anchor.Centre;
Alpha = 0.5f;
}
}
}
}
@@ -3,12 +3,12 @@
#nullable disable
using osuTK.Graphics;
using osu.Game.Skinning;
using osu.Game.Online.API;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
@@ -29,7 +29,9 @@ namespace osu.Game.Screens.Menu
private void updateColour()
{
if (user.Value?.IsSupporter ?? false)
if (SeasonalUI.ENABLED)
Colour = SeasonalUI.AMBIENT_COLOUR_1;
else if (user.Value?.IsSupporter ?? false)
Colour = skin.Value.GetConfig<GlobalSkinColours, Color4>(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White;
else
Colour = Color4.White;
+16 -9
View File
@@ -3,22 +3,23 @@
#nullable disable
using osuTK.Graphics;
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Skinning;
using osu.Game.Online.API;
using System;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
@@ -67,7 +68,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Y,
Width = box_width * 2,
Width = box_width * (SeasonalUI.ENABLED ? 4 : 2),
Height = 1.5f,
// align off-screen to make sure our edges don't become visible during parallax.
X = -box_width,
@@ -79,7 +80,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
Width = box_width * 2,
Width = box_width * (SeasonalUI.ENABLED ? 4 : 2),
Height = 1.5f,
X = box_width,
Alpha = 0,
@@ -104,7 +105,11 @@ namespace osu.Game.Screens.Menu
private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes)
{
d.FadeTo(Math.Max(0, ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier)), box_fade_in_time)
if (SeasonalUI.ENABLED)
updateColour();
d.FadeTo(Math.Clamp(0.1f + ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier), 0.1f, 1),
box_fade_in_time)
.Then()
.FadeOut(beatLength, Easing.In);
}
@@ -113,7 +118,9 @@ namespace osu.Game.Screens.Menu
{
Color4 baseColour = colours.Blue;
if (user.Value?.IsSupporter ?? false)
if (SeasonalUI.ENABLED)
baseColour = RNG.NextBool() ? SeasonalUI.PRIMARY_COLOUR_1 : SeasonalUI.PRIMARY_COLOUR_2;
else if (user.Value?.IsSupporter ?? false)
baseColour = skin.Value.GetConfig<GlobalSkinColours, Color4>(GlobalSkinColours.MenuGlow)?.Value ?? baseColour;
// linear colour looks better in this case, so let's use it for now.
+64 -3
View File
@@ -4,6 +4,7 @@
#nullable disable
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -211,6 +212,15 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
SeasonalUI.ENABLED
? hat = new Sprite
{
BypassAutoSizeAxes = Axes.Both,
Alpha = 0,
Origin = Anchor.BottomCentre,
Scale = new Vector2(-1, 1),
}
: Empty(),
}
},
impactContainer = new CircularContainer
@@ -271,11 +281,21 @@ namespace osu.Game.Screens.Menu
private void load(TextureStore textures, AudioManager audio)
{
sampleClick = audio.Samples.Get(@"Menu/osu-logo-select");
sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat");
if (SeasonalUI.ENABLED)
{
sampleDownbeat = sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell");
}
else
{
sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat");
}
logo.Texture = textures.Get(@"Menu/logo");
ripple.Texture = textures.Get(@"Menu/logo");
if (hat != null)
hat.Texture = textures.Get(@"Menu/hat");
}
private int lastBeatIndex;
@@ -303,7 +323,10 @@ namespace osu.Game.Screens.Menu
else
{
var channel = sampleBeat.GetChannel();
channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1);
if (SeasonalUI.ENABLED)
channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
else
channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1);
channel.Play();
}
});
@@ -358,6 +381,9 @@ namespace osu.Game.Screens.Menu
const float scale_adjust_cutoff = 0.4f;
if (SeasonalUI.ENABLED)
updateHat();
if (musicController.CurrentTrack.IsRunning)
{
float maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0;
@@ -371,6 +397,38 @@ namespace osu.Game.Screens.Menu
}
}
private bool hasHat;
private void updateHat()
{
if (hat == null)
return;
bool shouldHat = DrawWidth * Scale.X < 400;
if (shouldHat != hasHat)
{
hasHat = shouldHat;
if (hasHat)
{
hat.Delay(400)
.Then()
.MoveTo(new Vector2(120, 160))
.RotateTo(0)
.RotateTo(-20, 500, Easing.OutQuint)
.FadeIn(250, Easing.OutQuint);
}
else
{
hat.Delay(100)
.Then()
.MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint)
.FadeOut(500, Easing.OutQuint);
}
}
}
public override bool HandlePositionalInput => base.HandlePositionalInput && Alpha > 0.2f;
protected override bool OnMouseDown(MouseDownEvent e)
@@ -448,6 +506,9 @@ namespace osu.Game.Screens.Menu
private Container currentProxyTarget;
private Drawable proxy;
[CanBeNull]
private readonly Sprite hat;
public void StopSamplePlayback() => sampleClickChannel?.Stop();
public Drawable ProxyToContainer(Container c)
@@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
lastPollRequest?.Cancel();
var req = new GetRoomsRequest(Filter.Value.Status, Filter.Value.Category);
var req = new GetRoomsRequest(Filter.Value);
req.Success += result =>
{
@@ -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)
@@ -8,7 +8,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public class FilterCriteria
{
public string SearchString = string.Empty;
public RoomStatusFilter Status;
public RoomModeFilter Mode;
public RoomStatusFilter? Status;
public string Category = string.Empty;
public RulesetInfo? Ruleset;
public RoomPermissionsFilter Permissions;
@@ -0,0 +1,17 @@
// 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.ComponentModel;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public enum RoomModeFilter
{
Open,
[Description("Recently Ended")]
Ended,
Participated,
Owned,
}
}
@@ -1,17 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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.ComponentModel;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public enum RoomStatusFilter
{
Open,
[Description("Recently Ended")]
Ended,
Participated,
Owned,
Idle,
Playing,
}
}
@@ -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, () =>
{
@@ -83,7 +83,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private LoadingLayer loadingLayer = null!;
private RoomsContainer roomsContainer = null!;
private SearchTextBox searchTextBox = null!;
private Dropdown<RoomStatusFilter> statusDropdown = null!;
protected Dropdown<RoomModeFilter> StatusDropdown { get; private set; } = null!;
[BackgroundDependencyLoader(true)]
private void load()
@@ -223,20 +224,20 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
SearchString = searchTextBox.Current.Value,
Ruleset = ruleset.Value,
Status = statusDropdown.Current.Value
Mode = StatusDropdown.Current.Value
};
protected virtual IEnumerable<Drawable> CreateFilterControls()
{
statusDropdown = new SlimEnumDropdown<RoomStatusFilter>
StatusDropdown = new SlimEnumDropdown<RoomModeFilter>
{
RelativeSizeAxes = Axes.None,
Width = 160,
};
statusDropdown.Current.BindValueChanged(_ => UpdateFilter());
StatusDropdown.Current.BindValueChanged(_ => UpdateFilter());
yield return statusDropdown;
yield return StatusDropdown;
}
#endregion
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -31,6 +30,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private MultiplayerClient client { get; set; } = null!;
private Dropdown<RoomPermissionsFilter> roomAccessTypeDropdown = null!;
private OsuCheckbox showInProgress = null!;
public override void OnResuming(ScreenTransitionEvent e)
{
@@ -47,7 +47,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override IEnumerable<Drawable> CreateFilterControls()
{
roomAccessTypeDropdown = new SlimEnumDropdown<RoomPermissionsFilter>
foreach (var control in base.CreateFilterControls())
yield return control;
yield return roomAccessTypeDropdown = new SlimEnumDropdown<RoomPermissionsFilter>
{
RelativeSizeAxes = Axes.None,
Current = Config.GetBindable<RoomPermissionsFilter>(OsuSetting.MultiplayerRoomFilter),
@@ -56,7 +59,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
roomAccessTypeDropdown.Current.BindValueChanged(_ => UpdateFilter());
return base.CreateFilterControls().Append(roomAccessTypeDropdown);
yield return showInProgress = new OsuCheckbox
{
LabelText = "Show in-progress rooms",
RelativeSizeAxes = Axes.None,
Width = 220,
Padding = new MarginPadding { Vertical = 5, },
Current = Config.GetBindable<bool>(OsuSetting.MultiplayerShowInProgressFilter),
};
showInProgress.Current.BindValueChanged(_ => UpdateFilter());
StatusDropdown.Current.BindValueChanged(_ => showInProgress.Alpha = StatusDropdown.Current.Value == RoomModeFilter.Open ? 1 : 0, true);
}
protected override FilterCriteria CreateFilterCriteria()
@@ -64,6 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
var criteria = base.CreateFilterCriteria();
criteria.Category = @"realtime";
criteria.Permissions = roomAccessTypeDropdown.Current.Value;
criteria.Status = showInProgress.Current.Value && criteria.Mode == RoomModeFilter.Open ? null : RoomStatusFilter.Idle;
return criteria;
}
@@ -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);
}));
}

Some files were not shown because too many files have changed in this diff Show More