1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 16:32:54 +08:00

Merge branch 'master' into fix-taiko-hitexplosions

This commit is contained in:
Dean Herbert 2018-06-15 17:52:53 +09:00 committed by GitHub
commit 30f7837b03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1113 additions and 633 deletions

View File

@ -1,36 +0,0 @@
# Linux
### 1. Requirements:
Mono >= 5.4.0 (>= 5.8.0 recommended)
Please check [here](http://www.mono-project.com/download/) for stable or [here](http://www.mono-project.com/download/alpha/) for an alpha release.
NuGet >= 4.4.0
msbuild
git
### 2. Cloning project
Clone the entire repository with submodules using
```
git clone https://github.com/ppy/osu --recursive
```
Then restore NuGet packages from the repository
```
nuget restore
```
### 3. Compiling
Simply run `msbuild` where `osu.sln` is located, this will create all binaries in `osu/osu.Desktop/bin/Debug`.
### 4. Optimizing
If you want additional performance you can change build type to Release with
```
msbuild -p:Configuration=Release
```
Additionally, mono provides an AOT utility which attempts to precompile binaries. You can utilize that by running
```
mono --aot ./osu\!.exe
```
### 5. Troubleshooting
You may run into trouble with NuGet versioning, as the one in packaging system is almost always out of date. Simply run
```
nuget
sudo nuget update -self
```
**Warning** NuGet creates few config files when it's run for the first time.
Do not run NuGet as root on the first run or you might run into very peculiar issues.

View File

@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{ {
public virtual bool CanBePlated => false; public virtual bool CanBePlated => false;
public virtual bool StaysOnPlate => CanBePlated;
protected DrawableCatchHitObject(CatchHitObject hitObject) protected DrawableCatchHitObject(CatchHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {

View File

@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{ {
private Pulp pulp; private Pulp pulp;
public override bool StaysOnPlate => false;
public DrawableDroplet(Droplet h) public DrawableDroplet(Droplet h)
: base(h) : base(h)
{ {

View File

@ -124,6 +124,9 @@ namespace osu.Game.Rulesets.Catch.Objects
X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
}); });
} }
if (NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested)
lastNested.LastInCombo = LastInCombo;
} }
public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity;

View File

@ -48,6 +48,16 @@ namespace osu.Game.Rulesets.Catch.UI
public void OnJudgement(DrawableCatchHitObject fruit, Judgement judgement) public void OnJudgement(DrawableCatchHitObject fruit, Judgement judgement)
{ {
void runAfterLoaded(Action action)
{
// this is required to make this run after the last caught fruit runs UpdateState at least once.
// TODO: find a better alternative
if (lastPlateableFruit.IsLoaded)
action();
else
lastPlateableFruit.OnLoadComplete = _ => action();
}
if (judgement.IsHit && fruit.CanBePlated) if (judgement.IsHit && fruit.CanBePlated)
{ {
var caughtFruit = (DrawableCatchHitObject)GetVisualRepresentation?.Invoke(fruit.HitObject); var caughtFruit = (DrawableCatchHitObject)GetVisualRepresentation?.Invoke(fruit.HitObject);
@ -63,21 +73,17 @@ namespace osu.Game.Rulesets.Catch.UI
caughtFruit.LifetimeEnd = double.MaxValue; caughtFruit.LifetimeEnd = double.MaxValue;
MovableCatcher.Add(caughtFruit); MovableCatcher.Add(caughtFruit);
lastPlateableFruit = caughtFruit; lastPlateableFruit = caughtFruit;
if (!fruit.StaysOnPlate)
runAfterLoaded(() => MovableCatcher.Explode(caughtFruit));
} }
if (fruit.HitObject.LastInCombo) if (fruit.HitObject.LastInCombo)
{ {
if (judgement.IsHit) if (judgement.IsHit)
{ runAfterLoaded(() => MovableCatcher.Explode());
// this is required to make this run after the last caught fruit runs UpdateState at least once.
// TODO: find a better alternative
if (lastPlateableFruit.IsLoaded)
MovableCatcher.Explode();
else
lastPlateableFruit.OnLoadComplete = _ => { MovableCatcher.Explode(); };
}
else else
MovableCatcher.Drop(); MovableCatcher.Drop();
} }
@ -378,28 +384,31 @@ namespace osu.Game.Rulesets.Catch.UI
var fruit = caughtFruit.ToArray(); var fruit = caughtFruit.ToArray();
foreach (var f in fruit) foreach (var f in fruit)
Explode(f);
}
public void Explode(DrawableHitObject fruit)
{
var originalX = fruit.X * Scale.X;
if (ExplodingFruitTarget != null)
{ {
var originalX = f.X * Scale.X; fruit.Anchor = Anchor.TopLeft;
fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
if (ExplodingFruitTarget != null) caughtFruit.Remove(fruit);
{
f.Anchor = Anchor.TopLeft;
f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget);
caughtFruit.Remove(f); ExplodingFruitTarget.Add(fruit);
ExplodingFruitTarget.Add(f);
}
f.MoveToY(f.Y - 50, 250, Easing.OutSine)
.Then()
.MoveToY(f.Y + 50, 500, Easing.InSine);
f.MoveToX(f.X + originalX * 6, 1000);
f.FadeOut(750);
f.Expire();
} }
fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine)
.Then()
.MoveToY(fruit.Y + 50, 500, Easing.InSine);
fruit.MoveToX(fruit.X + originalX * 6, 1000);
fruit.FadeOut(750);
fruit.Expire();
} }
private class CatcherSprite : Sprite private class CatcherSprite : Sprite

View File

@ -58,6 +58,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public override Pattern Generate() public override Pattern Generate()
{ {
if (TotalColumns == 1)
{
var pattern = new Pattern();
addToPattern(pattern, 0, HitObject.StartTime, endTime);
return pattern;
}
if (spanCount > 1) if (spanCount > 1)
{ {
if (segmentDuration <= 90) if (segmentDuration <= 90)

View File

@ -77,10 +77,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
} }
else else
convertType |= PatternType.LowProbability; convertType |= PatternType.LowProbability;
if ((convertType & PatternType.KeepSingle) == 0)
{
if (HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_FINISH) && TotalColumns != 8)
convertType |= PatternType.Mirror;
else
convertType |= PatternType.Gathered;
}
} }
public override Pattern Generate() public override Pattern Generate()
{ {
if (TotalColumns == 1)
{
var pattern = new Pattern();
addToPattern(pattern, 0);
return pattern;
}
int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0;
if ((convertType & PatternType.Reverse) > 0 && PreviousPattern.HitObjects.Any()) if ((convertType & PatternType.Reverse) > 0 && PreviousPattern.HitObjects.Any())
@ -346,7 +361,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
addToCentre = false; addToCentre = false;
if ((convertType & PatternType.ForceNotStack) > 0) if ((convertType & PatternType.ForceNotStack) > 0)
return getRandomNoteCount(p2 / 2, p2, (p2 + p3) / 2, p3); return getRandomNoteCount(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
switch (TotalColumns) switch (TotalColumns)
{ {

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{ {
public class ManiaConfigManager : RulesetConfigManager<ManiaSetting> public class ManiaConfigManager : RulesetConfigManager<ManiaSetting>
{ {
public ManiaConfigManager(SettingsStore settings, RulesetInfo ruleset, int variant) public ManiaConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant) : base(settings, ruleset, variant)
{ {
} }

View File

@ -15,8 +15,11 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Difficulty; using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -150,6 +153,8 @@ namespace osu.Game.Rulesets.Mania
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame();
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new ManiaConfigManager(settings, RulesetInfo);
public ManiaRuleset(RulesetInfo rulesetInfo = null) public ManiaRuleset(RulesetInfo rulesetInfo = null)
: base(rulesetInfo) : base(rulesetInfo)
{ {

View File

@ -10,12 +10,9 @@ using osu.Framework.Input;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
@ -103,7 +100,5 @@ namespace osu.Game.Rulesets.Mania.UI
protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f); protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f);
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
protected override IRulesetConfigManager CreateConfig(Ruleset ruleset, SettingsStore settings) => new ManiaConfigManager(settings, Ruleset.RulesetInfo, Variant);
} }
} }

View File

@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Osu
public override string ShortName => "osu"; public override string ShortName => "osu";
public override SettingsSubsection CreateSettings() => new OsuSettings(); public override RulesetSettingsSubsection CreateSettings() => new OsuSettings(this);
public override int? LegacyID => 0; public override int? LegacyID => 0;

View File

@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}; };
this.beatmap.BindTo(beatmap); this.beatmap.BindTo(beatmap);
beatmap.ValueChanged += v => calculateScale(); this.beatmap.ValueChanged += v => calculateScale();
cursorScale = config.GetBindable<double>(OsuSetting.GameplayCursorSize); cursorScale = config.GetBindable<double>(OsuSetting.GameplayCursorSize);
cursorScale.ValueChanged += v => calculateScale(); cursorScale.ValueChanged += v => calculateScale();

View File

@ -8,10 +8,15 @@ using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Osu.UI namespace osu.Game.Rulesets.Osu.UI
{ {
public class OsuSettings : SettingsSubsection public class OsuSettings : RulesetSettingsSubsection
{ {
protected override string Header => "osu!"; protected override string Header => "osu!";
public OsuSettings(Ruleset ruleset)
: base(ruleset)
{
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {

View File

@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko
{ {
new TaikoModHardRock(), new TaikoModHardRock(),
new MultiMod(new TaikoModSuddenDeath(), new TaikoModPerfect()), new MultiMod(new TaikoModSuddenDeath(), new TaikoModPerfect()),
new MultiMod(new TaikoModDoubleTime(), new TaikoModDaycore()), new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()),
new TaikoModHidden(), new TaikoModHidden(),
new TaikoModFlashlight(), new TaikoModFlashlight(),
}; };

View File

@ -86,7 +86,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual(string.Empty, metadata.Source); Assert.AreEqual(string.Empty, metadata.Source);
Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags); Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags);
Assert.AreEqual(557821, beatmapInfo.OnlineBeatmapID); Assert.AreEqual(557821, beatmapInfo.OnlineBeatmapID);
Assert.AreEqual(241526, metadata.OnlineBeatmapSetID); Assert.AreEqual(241526, beatmapInfo.BeatmapSet.OnlineBeatmapSetID);
} }
} }

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
var beatmap = decodeAsJson(normal); var beatmap = decodeAsJson(normal);
var meta = beatmap.BeatmapInfo.Metadata; var meta = beatmap.BeatmapInfo.Metadata;
Assert.AreEqual(241526, meta.OnlineBeatmapSetID); Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID);
Assert.AreEqual("Soleily", meta.Artist); Assert.AreEqual("Soleily", meta.Artist);
Assert.AreEqual("Soleily", meta.ArtistUnicode); Assert.AreEqual("Soleily", meta.ArtistUnicode);
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile);

View File

@ -48,11 +48,14 @@ namespace osu.Game.Tests.Beatmaps.IO
{ {
var reader = new ZipArchiveReader(osz); var reader = new ZipArchiveReader(osz);
BeatmapMetadata meta; Beatmap beatmap;
using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
meta = Decoder.GetDecoder<Beatmap>(stream).Decode(stream).Metadata;
Assert.AreEqual(241526, meta.OnlineBeatmapSetID); using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu")))
beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
var meta = beatmap.Metadata;
Assert.AreEqual(241526, beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID);
Assert.AreEqual("Soleily", meta.Artist); Assert.AreEqual("Soleily", meta.Artist);
Assert.AreEqual("Soleily", meta.ArtistUnicode); Assert.AreEqual("Soleily", meta.ArtistUnicode);
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile);

View File

@ -449,7 +449,6 @@ namespace osu.Game.Tests.Visual
Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
OnlineBeatmapSetID = id,
// Create random metadata, then we can check if sorting works based on these // Create random metadata, then we can check if sorting works based on these
Artist = $"peppy{id.ToString().PadLeft(6, '0')}", Artist = $"peppy{id.ToString().PadLeft(6, '0')}",
Title = $"test set #{id}!", Title = $"test set #{id}!",
@ -503,7 +502,6 @@ namespace osu.Game.Tests.Visual
Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
OnlineBeatmapSetID = id,
// Create random metadata, then we can check if sorting works based on these // Create random metadata, then we can check if sorting works based on these
Artist = $"peppy{id.ToString().PadLeft(6, '0')}", Artist = $"peppy{id.ToString().PadLeft(6, '0')}",
Title = $"test set #{id}!", Title = $"test set #{id}!",

View File

@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual
AddStep("remove scores", () => scoresContainer.Scores = null); AddStep("remove scores", () => scoresContainer.Scores = null);
AddStep("resize to big", () => container.ResizeWidthTo(1, 300)); AddStep("resize to big", () => container.ResizeWidthTo(1, 300));
AddStep("resize to normal", () => container.ResizeWidthTo(0.8f, 300)); AddStep("resize to normal", () => container.ResizeWidthTo(0.8f, 300));
AddStep("online scores", () => scoresContainer.Beatmap = new BeatmapInfo { OnlineBeatmapSetID = 1, OnlineBeatmapID = 75, Ruleset = new OsuRuleset().RulesetInfo }); AddStep("online scores", () => scoresContainer.Beatmap = new BeatmapInfo { OnlineBeatmapID = 75, Ruleset = new OsuRuleset().RulesetInfo });
scores = new[] scores = new[]

View File

@ -15,7 +15,7 @@ namespace osu.Game.Tests.Visual
[TestFixture] [TestFixture]
public class TestCaseEditorComposeTimeline : OsuTestCase public class TestCaseEditorComposeTimeline : OsuTestCase
{ {
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(ScrollableTimeline), typeof(ScrollingTimelineContainer), typeof(BeatmapWaveformGraph), typeof(TimelineButton) }; public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(TimelineArea), typeof(Timeline), typeof(TimelineButton) };
public TestCaseEditorComposeTimeline() public TestCaseEditorComposeTimeline()
{ {
@ -27,11 +27,12 @@ namespace osu.Game.Tests.Visual
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
State = Visibility.Visible State = Visibility.Visible
}, },
new ScrollableTimeline new TimelineArea
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(1000, 100) RelativeSizeAxes = Axes.X,
Size = new Vector2(0.8f, 100)
} }
}; };
} }

View File

@ -122,7 +122,6 @@ namespace osu.Game.Tests.Visual
Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(), Hash = new MemoryStream(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString())).ComputeMD5Hash(),
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
OnlineBeatmapSetID = 1234 + i,
// Create random metadata, then we can check if sorting works based on these // Create random metadata, then we can check if sorting works based on these
Artist = "MONACA " + RNG.Next(0, 9), Artist = "MONACA " + RNG.Next(0, 9),
Title = "Black Song " + RNG.Next(0, 9), Title = "Black Song " + RNG.Next(0, 9),

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Overlays.Volume; using osu.Game.Overlays.Volume;
using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
@ -17,13 +18,21 @@ namespace osu.Game.Tests.Visual
{ {
VolumeMeter meter; VolumeMeter meter;
MuteButton mute; MuteButton mute;
Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue)); Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue) { Position = new Vector2(10) });
AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1);
Add(new VolumeMeter("BIG", 250, Color4.Red)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Position = new Vector2(10),
});
Add(mute = new MuteButton Add(mute = new MuteButton
{ {
Margin = new MarginPadding { Top = 200 } Margin = new MarginPadding { Top = 200 }
}); });
AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1);
AddToggleStep("mute", b => mute.Current.Value = b); AddToggleStep("mute", b => mute.Current.Value = b);
} }
} }

View File

@ -6,11 +6,11 @@ using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.Edit.Screens.Compose.Timeline;
namespace osu.Game.Tests.Visual namespace osu.Game.Tests.Visual
{ {
@ -40,14 +40,13 @@ namespace osu.Game.Tests.Visual
for (int i = 1; i <= 16; i *= 2) for (int i = 1; i <= 16; i *= 2)
{ {
var newDisplay = new BeatmapWaveformGraph var newDisplay = new WaveformGraph
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Resolution = 1f / i, Resolution = 1f / i,
Beatmap = Beatmap
}; };
Beatmap.ValueChanged += b => newDisplay.Beatmap = b; Beatmap.ValueChanged += b => newDisplay.Waveform = b.Waveform;
flow.Add(new Container flow.Add(new Container
{ {

View File

@ -0,0 +1,142 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.MathUtils;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Screens.Edit.Screens.Compose.Timeline;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Input;
namespace osu.Game.Tests.Visual
{
public class TestCaseZoomableScrollContainer : ManualInputManagerTestCase
{
private readonly ZoomableScrollContainer scrollContainer;
private readonly Drawable innerBox;
public TestCaseZoomableScrollContainer()
{
Children = new Drawable[]
{
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 250,
Width = 0.75f,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(30)
},
scrollContainer = new ZoomableScrollContainer { RelativeSizeAxes = Axes.Both }
}
},
new MenuCursor()
};
scrollContainer.Add(innerBox = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(new Color4(0.8f, 0.6f, 0.4f, 1f), new Color4(0.4f, 0.6f, 0.8f, 1f))
});
}
[Test]
public void TestZoom0()
{
reset();
AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
AddAssert("Box width = 1x", () => Precision.AlmostEquals(boxQuad.Size, scrollQuad.Size));
}
[Test]
public void TestZoom10()
{
reset();
AddStep("Set zoom = 10", () => scrollContainer.Zoom = 10);
AddAssert("Box at 1/2", () => Precision.AlmostEquals(boxQuad.Centre, scrollQuad.Centre));
AddAssert("Box width = 10x", () => Precision.AlmostEquals(boxQuad.Size.X, 10 * scrollQuad.Size.X));
}
[Test]
public void TestMouseZoomInOnceOutOnce()
{
reset();
// Scroll in at 0.25
AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y)));
AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl));
AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(3, 0)));
AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl));
AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X));
// Scroll out at 0.25
AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl));
AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(-3, 0)));
AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl));
AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X));
}
[Test]
public void TestMouseZoomInTwiceOutTwice()
{
reset();
// Scroll in at 0.25
AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y)));
AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl));
AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(1, 0)));
AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl));
// Scroll in at 0.6
AddStep("Move mouse to 0.75x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.75f * scrollQuad.Size.X, scrollQuad.Centre.Y)));
AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl));
AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(1, 0)));
AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl));
AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
// Very hard to determine actual position, so approximate
AddAssert("Box at correct position (1)", () => Precision.DefinitelyBigger(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X));
AddAssert("Box at correct position (2)", () => Precision.DefinitelyBigger(scrollQuad.TopLeft.X + 0.6f * scrollQuad.Size.X, boxQuad.TopLeft.X + 0.3f * boxQuad.Size.X));
AddAssert("Box at correct position (3)", () => Precision.DefinitelyBigger(boxQuad.TopLeft.X + 0.6f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.6f * scrollQuad.Size.X));
// Scroll out at 0.6
AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl));
AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(-1, 0)));
AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl));
// Scroll out at 0.25
AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y)));
AddStep("Press ctrl", () => InputManager.PressKey(Key.LControl));
AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(-1, 0)));
AddStep("Release ctrl", () => InputManager.ReleaseKey(Key.LControl));
AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
}
private void reset()
{
AddStep("Reset", () =>
{
scrollContainer.Zoom = 0;
scrollContainer.ScrollTo(0, false);
});
}
private Quad scrollQuad => scrollContainer.ScreenSpaceDrawQuad;
private Quad boxQuad => innerBox.ScreenSpaceDrawQuad;
}
}

View File

@ -23,7 +23,6 @@ namespace osu.Game.Beatmaps
public int BeatmapVersion; public int BeatmapVersion;
private int? onlineBeatmapID; private int? onlineBeatmapID;
private int? onlineBeatmapSetID;
[JsonProperty("id")] [JsonProperty("id")]
public int? OnlineBeatmapID public int? OnlineBeatmapID
@ -32,19 +31,10 @@ namespace osu.Game.Beatmaps
set { onlineBeatmapID = value > 0 ? value : null; } set { onlineBeatmapID = value > 0 ? value : null; }
} }
[JsonProperty("beatmapset_id")]
[NotMapped]
public int? OnlineBeatmapSetID
{
get { return onlineBeatmapSetID; }
set { onlineBeatmapSetID = value > 0 ? value : null; }
}
[JsonIgnore] [JsonIgnore]
public int BeatmapSetInfoID { get; set; } public int BeatmapSetInfoID { get; set; }
[Required] [Required]
[JsonIgnore]
public BeatmapSetInfo BeatmapSet { get; set; } public BeatmapSetInfo BeatmapSet { get; set; }
public BeatmapMetadata Metadata { get; set; } public BeatmapMetadata Metadata { get; set; }
@ -141,8 +131,8 @@ namespace osu.Game.Beatmaps
(Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile; (Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile;
public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null && public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null &&
BeatmapSet.Hash == other.BeatmapSet.Hash && BeatmapSet.Hash == other.BeatmapSet.Hash &&
(Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile; (Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile;
/// <summary> /// <summary>
/// Returns a shallow-clone of this <see cref="BeatmapInfo"/>. /// Returns a shallow-clone of this <see cref="BeatmapInfo"/>.

View File

@ -81,12 +81,31 @@ namespace osu.Game.Beatmaps
protected override void Populate(BeatmapSetInfo model, ArchiveReader archive) protected override void Populate(BeatmapSetInfo model, ArchiveReader archive)
{ {
model.Beatmaps = createBeatmapDifficulties(model, archive); model.Beatmaps = createBeatmapDifficulties(archive);
// remove metadata from difficulties where it matches the set
foreach (BeatmapInfo b in model.Beatmaps) foreach (BeatmapInfo b in model.Beatmaps)
{
// remove metadata from difficulties where it matches the set
if (model.Metadata.Equals(b.Metadata)) if (model.Metadata.Equals(b.Metadata))
b.Metadata = null; b.Metadata = null;
// by setting the model here, we can update the noline set id below.
b.BeatmapSet = model;
fetchAndPopulateOnlineIDs(b);
}
// check if a set already exists with the same online id, delete if it does.
if (model.OnlineBeatmapSetID != null)
{
var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID);
if (existingOnlineId != null)
{
Delete(existingOnlineId);
beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID);
Logger.Log($"Found existing beatmap set with same OnlineBeatmapSetID ({model.OnlineBeatmapSetID}). It has been purged.", LoggingTarget.Database);
}
}
} }
protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model) protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model)
@ -99,18 +118,6 @@ namespace osu.Game.Beatmaps
return existingHashMatch; return existingHashMatch;
} }
// check if a set already exists with the same online id
if (model.OnlineBeatmapSetID != null)
{
var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID);
if (existingOnlineId != null)
{
Delete(existingOnlineId);
beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID);
Logger.Log($"Found existing beatmap set with same OnlineBeatmapSetID ({model.OnlineBeatmapSetID}). It has been purged.", LoggingTarget.Database);
}
}
return null; return null;
} }
@ -306,29 +313,29 @@ namespace osu.Game.Beatmaps
return hashable.ComputeSHA2Hash(); return hashable.ComputeSHA2Hash();
} }
protected override BeatmapSetInfo CreateModel(ArchiveReader reader) protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{ {
// let's make sure there are actually .osu files to import. // let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in this beatmap archive."); if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in this beatmap archive.");
BeatmapMetadata metadata; Beatmap beatmap;
using (var stream = new StreamReader(reader.GetStream(mapName))) using (var stream = new StreamReader(reader.GetStream(mapName)))
metadata = Decoder.GetDecoder<Beatmap>(stream).Decode(stream).Metadata; beatmap = Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
return new BeatmapSetInfo return new BeatmapSetInfo
{ {
OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
Beatmaps = new List<BeatmapInfo>(), Beatmaps = new List<BeatmapInfo>(),
Hash = computeBeatmapSetHash(reader), Hash = computeBeatmapSetHash(reader),
Metadata = metadata Metadata = beatmap.Metadata
}; };
} }
/// <summary> /// <summary>
/// Create all required <see cref="BeatmapInfo"/>s for the provided archive. /// Create all required <see cref="BeatmapInfo"/>s for the provided archive.
/// </summary> /// </summary>
private List<BeatmapInfo> createBeatmapDifficulties(BeatmapSetInfo model, ArchiveReader reader) private List<BeatmapInfo> createBeatmapDifficulties(ArchiveReader reader)
{ {
var beatmapInfos = new List<BeatmapInfo>(); var beatmapInfos = new List<BeatmapInfo>();
@ -348,10 +355,6 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash();
beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash();
// ensure we have the same online set ID as the set itself.
beatmap.BeatmapInfo.OnlineBeatmapSetID = model.OnlineBeatmapSetID;
beatmap.BeatmapInfo.Metadata.OnlineBeatmapSetID = model.OnlineBeatmapSetID;
// check that no existing beatmap exists that is imported with the same online beatmap ID. if so, give it precedence. // check that no existing beatmap exists that is imported with the same online beatmap ID. if so, give it precedence.
if (beatmap.BeatmapInfo.OnlineBeatmapID.HasValue && QueryBeatmap(b => b.OnlineBeatmapID.Value == beatmap.BeatmapInfo.OnlineBeatmapID.Value) != null) if (beatmap.BeatmapInfo.OnlineBeatmapID.HasValue && QueryBeatmap(b => b.OnlineBeatmapID.Value == beatmap.BeatmapInfo.OnlineBeatmapID.Value) != null)
beatmap.BeatmapInfo.OnlineBeatmapID = null; beatmap.BeatmapInfo.OnlineBeatmapID = null;
@ -376,6 +379,40 @@ namespace osu.Game.Beatmaps
return beatmapInfos; return beatmapInfos;
} }
/// <summary>
/// Query the API to populate mising OnlineBeatmapID / OnlineBeatmapSetID properties.
/// </summary>
/// <param name="beatmap">The beatmap to populate.</param>
/// <param name="force">Whether to re-query if the provided beatmap already has populated values.</param>
/// <returns>True if population was successful.</returns>
private bool fetchAndPopulateOnlineIDs(BeatmapInfo beatmap, bool force = false)
{
if (!force && beatmap.OnlineBeatmapID != null && beatmap.BeatmapSet.OnlineBeatmapSetID != null)
return true;
Logger.Log("Attempting online lookup for IDs...", LoggingTarget.Database);
try
{
var req = new GetBeatmapRequest(beatmap);
req.Perform(api);
var res = req.Result;
Logger.Log($"Successfully mapped to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.", LoggingTarget.Database);
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
return true;
}
catch (Exception e)
{
Logger.Log($"Failed ({e})", LoggingTarget.Database);
return false;
}
}
/// <summary> /// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary> /// </summary>

View File

@ -17,16 +17,6 @@ namespace osu.Game.Beatmaps
[JsonIgnore] [JsonIgnore]
public int ID { get; set; } public int ID { get; set; }
private int? onlineBeatmapSetID;
[NotMapped]
[JsonProperty(@"id")]
public int? OnlineBeatmapSetID
{
get { return onlineBeatmapSetID; }
set { onlineBeatmapSetID = value > 0 ? value : null; }
}
public string Title { get; set; } public string Title { get; set; }
public string TitleUnicode { get; set; } public string TitleUnicode { get; set; }
public string Artist { get; set; } public string Artist { get; set; }
@ -82,8 +72,7 @@ namespace osu.Game.Beatmaps
if (other == null) if (other == null)
return false; return false;
return onlineBeatmapSetID == other.onlineBeatmapSetID return Title == other.Title
&& Title == other.Title
&& TitleUnicode == other.TitleUnicode && TitleUnicode == other.TitleUnicode
&& Artist == other.Artist && Artist == other.Artist
&& ArtistUnicode == other.ArtistUnicode && ArtistUnicode == other.ArtistUnicode

View File

@ -22,18 +22,18 @@ namespace osu.Game.Beatmaps
[NotMapped] [NotMapped]
public BeatmapSetOnlineInfo OnlineInfo { get; set; } public BeatmapSetOnlineInfo OnlineInfo { get; set; }
public double MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty); public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0;
[NotMapped] [NotMapped]
public bool DeletePending { get; set; } public bool DeletePending { get; set; }
public string Hash { get; set; } public string Hash { get; set; }
public string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(".osb"))?.Filename; public string StoryboardFile => Files?.FirstOrDefault(f => f.Filename.EndsWith(".osb"))?.Filename;
public List<BeatmapSetFileInfo> Files { get; set; } public List<BeatmapSetFileInfo> Files { get; set; }
public override string ToString() => Metadata.ToString(); public override string ToString() => Metadata?.ToString() ?? base.ToString();
public bool Protected { get; set; } public bool Protected { get; set; }
} }

View File

@ -34,7 +34,8 @@ namespace osu.Game.Beatmaps.Formats
private readonly int offset; private readonly int offset;
public LegacyBeatmapDecoder(int version = LATEST_VERSION) : base(version) public LegacyBeatmapDecoder(int version = LATEST_VERSION)
: base(version)
{ {
// BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off)
offset = FormatVersion < 5 ? 24 : 0; offset = FormatVersion < 5 ? 24 : 0;
@ -135,6 +136,7 @@ namespace osu.Game.Beatmaps.Formats
parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(); parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser();
break; break;
} }
break; break;
case @"LetterboxInBreaks": case @"LetterboxInBreaks":
beatmap.BeatmapInfo.LetterboxInBreaks = int.Parse(pair.Value) == 1; beatmap.BeatmapInfo.LetterboxInBreaks = int.Parse(pair.Value) == 1;
@ -207,8 +209,7 @@ namespace osu.Game.Beatmaps.Formats
beatmap.BeatmapInfo.OnlineBeatmapID = int.Parse(pair.Value); beatmap.BeatmapInfo.OnlineBeatmapID = int.Parse(pair.Value);
break; break;
case @"BeatmapSetID": case @"BeatmapSetID":
beatmap.BeatmapInfo.OnlineBeatmapSetID = int.Parse(pair.Value); beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = int.Parse(pair.Value) };
metadata.OnlineBeatmapSetID = int.Parse(pair.Value);
break; break;
} }
} }

View File

@ -13,13 +13,13 @@ namespace osu.Game.Configuration
{ {
private readonly SettingsStore settings; private readonly SettingsStore settings;
private readonly int variant; private readonly int? variant;
private readonly List<DatabasedSetting> databasedSettings; private readonly List<DatabasedSetting> databasedSettings;
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
protected DatabasedConfigManager(SettingsStore settings, RulesetInfo ruleset = null, int variant = 0) protected DatabasedConfigManager(SettingsStore settings, RulesetInfo ruleset = null, int? variant = null)
{ {
this.settings = settings; this.settings = settings;
this.ruleset = ruleset; this.ruleset = ruleset;

View File

@ -181,24 +181,6 @@ namespace osu.Game.Database
} }
} }
public void Migrate() public void Migrate() => Database.Migrate();
{
try
{
Database.Migrate();
}
catch (Exception e)
{
throw new MigrationFailedException(e);
}
}
}
public class MigrationFailedException : Exception
{
public MigrationFailedException(Exception exception)
: base("sqlite-net migration failed", exception)
{
}
} }
} }

View File

@ -33,7 +33,7 @@ namespace osu.Game.Graphics.Containers
/// <summary> /// <summary>
/// Whether mouse input should be blocked screen-wide while this overlay is visible. /// Whether mouse input should be blocked screen-wide while this overlay is visible.
/// Performing mouse actions outside of the valid extents will hide the overlay but pass the events through. /// Performing mouse actions outside of the valid extents will hide the overlay.
/// </summary> /// </summary>
public virtual bool BlockScreenWideMouse => BlockPassThroughMouse; public virtual bool BlockScreenWideMouse => BlockPassThroughMouse;

View File

@ -14,16 +14,19 @@ namespace osu.Game.Online.API
{ {
protected override WebRequest CreateWebRequest() => new JsonWebRequest<T>(Uri); protected override WebRequest CreateWebRequest() => new JsonWebRequest<T>(Uri);
public T Result => ((JsonWebRequest<T>)WebRequest).ResponseObject;
protected APIRequest() protected APIRequest()
{ {
base.Success += onSuccess; base.Success += onSuccess;
} }
private void onSuccess() private void onSuccess() => Success?.Invoke(Result);
{
Success?.Invoke(((JsonWebRequest<T>)WebRequest).ResponseObject);
}
/// <summary>
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
/// </summary>
public new event APISuccessHandler<T> Success; public new event APISuccessHandler<T> Success;
} }
@ -52,7 +55,16 @@ namespace osu.Game.Online.API
protected APIAccess API; protected APIAccess API;
protected WebRequest WebRequest; protected WebRequest WebRequest;
/// <summary>
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
/// </summary>
public event APISuccessHandler Success; public event APISuccessHandler Success;
/// <summary>
/// Invoked on failure to complete an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
/// </summary>
public event APIFailureHandler Failure; public event APIFailureHandler Failure;
private bool cancelled; private bool cancelled;

View File

@ -10,13 +10,11 @@ namespace osu.Game.Online.API.Requests
{ {
private readonly BeatmapInfo beatmap; private readonly BeatmapInfo beatmap;
private string lookupString => beatmap.OnlineBeatmapID > 0 ? beatmap.OnlineBeatmapID.ToString() : $@"lookup?checksum={beatmap.Hash}&filename={System.Uri.EscapeUriString(beatmap.Path)}";
public GetBeatmapDetailsRequest(BeatmapInfo beatmap) public GetBeatmapDetailsRequest(BeatmapInfo beatmap)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
} }
protected override string Target => $@"beatmaps/{lookupString}"; protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}";
} }
} }

View File

@ -0,0 +1,22 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetBeatmapRequest : APIRequest<APIBeatmap>
{
private readonly BeatmapInfo beatmap;
private string lookupString => beatmap.OnlineBeatmapID > 0 ? beatmap.OnlineBeatmapID.ToString() : $@"lookup?checksum={beatmap.MD5Hash}&filename={System.Uri.EscapeUriString(beatmap.Path)}";
public GetBeatmapRequest(BeatmapInfo beatmap)
{
this.beatmap = beatmap;
}
protected override string Target => $@"beatmaps/{lookupString}";
}
}

View File

@ -10,7 +10,10 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIBeatmap : BeatmapMetadata public class APIBeatmap : BeatmapMetadata
{ {
[JsonProperty(@"id")] [JsonProperty(@"id")]
private int onlineBeatmapID { get; set; } public int OnlineBeatmapID { get; set; }
[JsonProperty(@"beatmapset_id")]
public int OnlineBeatmapSetID { get; set; }
[JsonProperty(@"playcount")] [JsonProperty(@"playcount")]
private int playCount { get; set; } private int playCount { get; set; }
@ -55,7 +58,11 @@ namespace osu.Game.Online.API.Requests.Responses
Metadata = this, Metadata = this,
Ruleset = rulesets.GetRuleset(ruleset), Ruleset = rulesets.GetRuleset(ruleset),
StarDifficulty = starDifficulty, StarDifficulty = starDifficulty,
OnlineBeatmapID = onlineBeatmapID, OnlineBeatmapID = OnlineBeatmapID,
BeatmapSet = new BeatmapSetInfo
{
OnlineBeatmapSetID = OnlineBeatmapSetID,
},
Version = version, Version = version,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {

View File

@ -15,6 +15,15 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"covers")] [JsonProperty(@"covers")]
private BeatmapSetOnlineCovers covers { get; set; } private BeatmapSetOnlineCovers covers { get; set; }
private int? onlineBeatmapSetID;
[JsonProperty(@"id")]
public int? OnlineBeatmapSetID
{
get { return onlineBeatmapSetID; }
set { onlineBeatmapSetID = value > 0 ? value : null; }
}
[JsonProperty(@"preview_url")] [JsonProperty(@"preview_url")]
private string preview { get; set; } private string preview { get; set; }

View File

@ -25,7 +25,6 @@ namespace osu.Game.Online.API.Requests.Responses
{ {
BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets); BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets);
beatmap.BeatmapSet = setInfo; beatmap.BeatmapSet = setInfo;
beatmap.OnlineBeatmapSetID = setInfo.OnlineBeatmapSetID;
beatmap.Metadata = setInfo.Metadata; beatmap.Metadata = setInfo.Metadata;
return beatmap; return beatmap;
} }

View File

@ -56,6 +56,8 @@ namespace osu.Game
protected SettingsStore SettingsStore; protected SettingsStore SettingsStore;
protected RulesetConfigCache RulesetConfigCache;
protected MenuCursorContainer MenuCursorContainer; protected MenuCursorContainer MenuCursorContainer;
private Container content; private Container content;
@ -123,6 +125,7 @@ namespace osu.Game
dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory, Host, BeatmapManager, RulesetStore)); dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory, Host, BeatmapManager, RulesetStore));
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));
dependencies.Cache(new OsuColour()); dependencies.Cache(new OsuColour());
fileImporters.Add(BeatmapManager); fileImporters.Add(BeatmapManager);

View File

@ -149,7 +149,7 @@ namespace osu.Game.Overlays.Direct
{ {
new OsuSpriteText new OsuSpriteText
{ {
Text = $"{SetInfo.Metadata.Source}", Text = SetInfo.Metadata.Source,
TextSize = 14, TextSize = 14,
Shadow = false, Shadow = false,
Colour = colours.Gray5, Colour = colours.Gray5,

View File

@ -160,7 +160,7 @@ namespace osu.Game.Overlays.Direct
}, },
new OsuSpriteText new OsuSpriteText
{ {
Text = $"from {SetInfo.Metadata.Source}", Text = SetInfo.Metadata.Source,
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
TextSize = 14, TextSize = 14,

View File

@ -66,34 +66,6 @@ namespace osu.Game.Overlays
AlwaysPresent = true; AlwaysPresent = true;
} }
private Vector2 dragStart;
protected override bool OnDragStart(InputState state)
{
base.OnDragStart(state);
dragStart = state.Mouse.Position;
return true;
}
protected override bool OnDrag(InputState state)
{
if (base.OnDrag(state)) return true;
Vector2 change = state.Mouse.Position - dragStart;
// Diminish the drag distance as we go further to simulate "rubber band" feeling.
change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length;
dragContainer.MoveTo(change);
return true;
}
protected override bool OnDragEnd(InputState state)
{
dragContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
return base.OnDragEnd(state);
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(BindableBeatmap beatmap, BeatmapManager beatmaps, OsuColour colours, LocalisationEngine localisation) private void load(BindableBeatmap beatmap, BeatmapManager beatmaps, OsuColour colours, LocalisationEngine localisation)
{ {
@ -103,7 +75,7 @@ namespace osu.Game.Overlays
Children = new Drawable[] Children = new Drawable[]
{ {
dragContainer = new Container dragContainer = new DragContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -470,5 +442,36 @@ namespace osu.Game.Overlays
sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4"); sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4");
} }
} }
private class DragContainer : Container
{
private Vector2 dragStart;
protected override bool OnDragStart(InputState state)
{
base.OnDragStart(state);
dragStart = state.Mouse.Position;
return true;
}
protected override bool OnDrag(InputState state)
{
if (base.OnDrag(state)) return true;
Vector2 change = state.Mouse.Position - dragStart;
// Diminish the drag distance as we go further to simulate "rubber band" feeling.
change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length;
this.MoveTo(change);
return true;
}
protected override bool OnDragEnd(InputState state)
{
this.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
return base.OnDragEnd(state);
}
}
} }
} }

View File

@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Profile.Sections
{ {
Action = () => Action = () =>
{ {
if (beatmap.OnlineBeatmapSetID.HasValue) beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.OnlineBeatmapSetID.Value); if (beatmap.BeatmapSet?.OnlineBeatmapSetID != null) beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.BeatmapSet.OnlineBeatmapSetID.Value);
}; };
Child = new FillFlowContainer Child = new FillFlowContainer

View File

@ -0,0 +1,35 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.Settings
{
/// <summary>
/// A <see cref="SettingsSubsection"/> which provides subclasses with the <see cref="IRulesetConfigManager"/>
/// from the <see cref="Ruleset"/>'s <see cref="Ruleset.CreateConfig()"/>.
/// </summary>
public abstract class RulesetSettingsSubsection : SettingsSubsection
{
private readonly Ruleset ruleset;
protected RulesetSettingsSubsection(Ruleset ruleset)
{
this.ruleset = ruleset;
}
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
{
dependencies = new DependencyContainer(base.CreateLocalDependencies(parent));
var config = dependencies.Get<RulesetConfigCache>().GetConfigFor(ruleset);
if (config != null)
dependencies.Cache(config);
return dependencies;
}
}
}

View File

@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Volume
public class VolumeMeter : Container, IKeyBindingHandler<GlobalAction> public class VolumeMeter : Container, IKeyBindingHandler<GlobalAction>
{ {
private CircularProgress volumeCircle; private CircularProgress volumeCircle;
private CircularProgress volumeCircleGlow;
public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 };
private readonly float circleSize; private readonly float circleSize;
private readonly Color4 meterColour; private readonly Color4 meterColour;
@ -44,90 +46,143 @@ namespace osu.Game.Overlays.Volume
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
Add(new Container Color4 backgroundColour = colours.Gray1;
{
Size = new Vector2(120, 20),
CornerRadius = 10,
Masking = true,
Margin = new MarginPadding { Left = circleSize + 10 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray1,
Alpha = 0.9f,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = "Exo2.0-Bold",
Text = name
}
}
});
CircularProgress bgProgress; CircularProgress bgProgress;
Add(new CircularContainer const float progress_start_radius = 0.75f;
const float progress_size = 0.03f;
const float progress_end_radius = progress_start_radius + progress_size;
const float blur_amount = 5;
Children = new Drawable[]
{ {
Masking = true, new Container
Size = new Vector2(circleSize),
Children = new Drawable[]
{ {
new Box Size = new Vector2(circleSize),
Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, new BufferedContainer
Colour = colours.Gray1,
Alpha = 0.9f,
},
bgProgress = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
InnerRadius = 0.05f,
Rotation = 180,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colours.Gray2,
Size = new Vector2(0.8f)
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f),
Padding = new MarginPadding(-Blur.KernelSize(5)),
Rotation = 180,
Child = (volumeCircle = new CircularProgress
{ {
Alpha = 0.9f,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
InnerRadius = 0.05f, Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour,
},
new CircularContainer
{
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(progress_end_radius),
Children = new Drawable[]
{
bgProgress = new CircularProgress
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Rotation = 180,
Colour = backgroundColour,
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Name = "Progress under covers for smoothing",
RelativeSizeAxes = Axes.Both,
Rotation = 180,
Child = volumeCircle = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
}
},
}
},
new Circle
{
Name = "Inner Cover",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour,
Size = new Vector2(progress_start_radius),
},
new Container
{
Name = "Progress overlay for glow",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(progress_start_radius + progress_size / 1.5f),
Rotation = 180,
Padding = new MarginPadding(-Blur.KernelSize(blur_amount)),
Child = (volumeCircleGlow = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
InnerRadius = progress_size * 0.8f,
}).WithEffect(new GlowEffect
{
Colour = meterColour,
BlurSigma = new Vector2(blur_amount),
Strength = 5,
PadExtent = true
}),
},
},
},
maxGlow = (text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = "Venera",
TextSize = 0.16f * circleSize
}).WithEffect(new GlowEffect }).WithEffect(new GlowEffect
{ {
Colour = meterColour, Colour = Color4.Transparent,
Strength = 2, PadExtent = true,
PadExtent = true })
}), }
}, },
maxGlow = (text = new OsuSpriteText new Container
{
Size = new Vector2(120, 20),
CornerRadius = 10,
Masking = true,
Margin = new MarginPadding { Left = circleSize + 10 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Children = new Drawable[]
{ {
Anchor = Anchor.Centre, new Box
Origin = Anchor.Centre, {
Font = "Venera", Alpha = 0.9f,
TextSize = 0.16f * circleSize RelativeSizeAxes = Axes.Both,
}).WithEffect(new GlowEffect Colour = backgroundColour,
{ },
Colour = Color4.Transparent, new OsuSpriteText
PadExtent = true, {
}) Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = "Exo2.0-Bold",
Text = name
}
}
} }
}); };
Bindable.ValueChanged += newVolume =>
Bindable.ValueChanged += newVolume => { this.TransformTo("DisplayVolume", newVolume, 400, Easing.OutQuint); }; {
this.TransformTo("DisplayVolume",
newVolume,
400,
Easing.OutQuint);
};
bgProgress.Current.Value = 0.75f; bgProgress.Current.Value = 0.75f;
} }
@ -158,6 +213,7 @@ namespace osu.Game.Overlays.Volume
} }
volumeCircle.Current.Value = displayVolume * 0.75f; volumeCircle.Current.Value = displayVolume * 0.75f;
volumeCircleGlow.Current.Value = displayVolume * 0.75f;
} }
} }

View File

@ -8,7 +8,8 @@ namespace osu.Game.Rulesets.Configuration
public abstract class RulesetConfigManager<T> : DatabasedConfigManager<T>, IRulesetConfigManager public abstract class RulesetConfigManager<T> : DatabasedConfigManager<T>, IRulesetConfigManager
where T : struct where T : struct
{ {
protected RulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int variant) : base(settings, ruleset, variant) protected RulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant)
{ {
} }
} }

View File

@ -15,6 +15,8 @@ using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets namespace osu.Game.Rulesets
@ -69,7 +71,13 @@ namespace osu.Game.Rulesets
public abstract string Description { get; } public abstract string Description { get; }
public virtual SettingsSubsection CreateSettings() => null; public virtual RulesetSettingsSubsection CreateSettings() => null;
/// <summary>
/// Creates the <see cref="IRulesetConfigManager"/> for this <see cref="Ruleset"/>.
/// </summary>
/// <param name="settings">The <see cref="SettingsStore"/> to store the settings.</param>
public virtual IRulesetConfigManager CreateConfig(SettingsStore settings) => null;
/// <summary> /// <summary>
/// Do not override this unless you are a legacy mode. /// Do not override this unless you are a legacy mode.

View File

@ -0,0 +1,43 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
namespace osu.Game.Rulesets
{
/// <summary>
/// A cache that provides a single <see cref="IRulesetConfigManager"/> per-ruleset.
/// This is done to support referring to and updating ruleset configs from multiple locations in the absence of inter-config bindings.
/// </summary>
public class RulesetConfigCache : Component
{
private readonly Dictionary<int, IRulesetConfigManager> configCache = new Dictionary<int, IRulesetConfigManager>();
private readonly SettingsStore settingsStore;
public RulesetConfigCache(SettingsStore settingsStore)
{
this.settingsStore = settingsStore;
}
/// <summary>
/// Retrieves the <see cref="IRulesetConfigManager"/> for a <see cref="Ruleset"/>.
/// </summary>
/// <param name="ruleset">The <see cref="Ruleset"/> to retrieve the <see cref="IRulesetConfigManager"/> for.</param>
/// <returns>The <see cref="IRulesetConfigManager"/> defined by <paramref name="ruleset"/>, null if <paramref name="ruleset"/> doesn't define one.</returns>
/// <exception cref="InvalidOperationException">If <paramref name="ruleset"/> doesn't have a valid <see cref="RulesetInfo.ID"/>.</exception>
public IRulesetConfigManager GetConfigFor(Ruleset ruleset)
{
if (ruleset.RulesetInfo.ID == null)
throw new InvalidOperationException("The provided ruleset doesn't have a valid id.");
if (configCache.TryGetValue(ruleset.RulesetInfo.ID.Value, out var existing))
return existing;
return configCache[ruleset.RulesetInfo.ID.Value] = ruleset.CreateConfig(settingsStore);
}
}
}

View File

@ -73,11 +73,6 @@ namespace osu.Game.Rulesets.UI
private IRulesetConfigManager rulesetConfig; private IRulesetConfigManager rulesetConfig;
private OnScreenDisplay onScreenDisplay; private OnScreenDisplay onScreenDisplay;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateLocalDependencies(parent));
/// <summary> /// <summary>
/// A visual representation of a <see cref="Rulesets.Ruleset"/>. /// A visual representation of a <see cref="Rulesets.Ruleset"/>.
/// </summary> /// </summary>
@ -90,18 +85,20 @@ namespace osu.Game.Rulesets.UI
Cursor = CreateCursor(); Cursor = CreateCursor();
} }
[BackgroundDependencyLoader(true)] protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
private void load(OnScreenDisplay onScreenDisplay, SettingsStore settings)
{ {
this.onScreenDisplay = onScreenDisplay; var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent));
rulesetConfig = CreateConfig(Ruleset, settings); onScreenDisplay = dependencies.Get<OnScreenDisplay>();
rulesetConfig = dependencies.Get<RulesetConfigCache>().GetConfigFor(Ruleset);
if (rulesetConfig != null) if (rulesetConfig != null)
{ {
dependencies.Cache(rulesetConfig); dependencies.Cache(rulesetConfig);
onScreenDisplay?.BeginTracking(this, rulesetConfig); onScreenDisplay?.BeginTracking(this, rulesetConfig);
} }
return dependencies;
} }
public abstract ScoreProcessor CreateScoreProcessor(); public abstract ScoreProcessor CreateScoreProcessor();
@ -136,8 +133,6 @@ namespace osu.Game.Rulesets.UI
/// </summary> /// </summary>
protected virtual CursorContainer CreateCursor() => null; protected virtual CursorContainer CreateCursor() => null;
protected virtual IRulesetConfigManager CreateConfig(Ruleset ruleset, SettingsStore settings) => null;
/// <summary> /// <summary>
/// Creates a Playfield. /// Creates a Playfield.
/// </summary> /// </summary>

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// <summary> /// <summary>
/// The step increase/decrease of the span of time visible by the length of the scrolling axes. /// The step increase/decrease of the span of time visible by the length of the scrolling axes.
/// </summary> /// </summary>
private const double time_span_step = 50; private const double time_span_step = 200;
/// <summary> /// <summary>
/// The span of time that is visible by the length of the scrolling axes. /// The span of time that is visible by the length of the scrolling axes.
@ -88,10 +88,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
switch (args.Key) switch (args.Key)
{ {
case Key.Minus: case Key.Minus:
this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 200, Easing.OutQuint); this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 600, Easing.OutQuint);
break; break;
case Key.Plus: case Key.Plus:
this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 200, Easing.OutQuint); this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 600, Easing.OutQuint);
break; break;
} }
} }

View File

@ -64,7 +64,7 @@ namespace osu.Game.Screens.Edit.Screens.Compose
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 }, Padding = new MarginPadding { Right = 5 },
Child = new ScrollableTimeline { RelativeSizeAxes = Axes.Both } Child = new TimelineArea { RelativeSizeAxes = Axes.Both }
}, },
new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both }
}, },

View File

@ -1,31 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Edit.Screens.Compose.Timeline
{
public class BeatmapWaveformGraph : CompositeDrawable
{
public WorkingBeatmap Beatmap { set => graph.Waveform = value.Waveform; }
private readonly WaveformGraph graph;
public BeatmapWaveformGraph()
{
InternalChild = graph = new WaveformGraph { RelativeSizeAxes = Axes.Both };
}
/// <summary>
/// Gets or sets the <see cref="WaveformGraph.Resolution"/>.
/// </summary>
public float Resolution
{
get { return graph.Resolution; }
set { graph.Resolution = value; }
}
}
}

View File

@ -1,126 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit.Screens.Compose.Timeline
{
public class ScrollableTimeline : CompositeDrawable
{
private readonly ScrollingTimelineContainer timelineContainer;
public ScrollableTimeline()
{
Masking = true;
CornerRadius = 5;
OsuCheckbox hitObjectsCheckbox;
OsuCheckbox hitSoundsCheckbox;
OsuCheckbox waveformCheckbox;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("111")
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("222")
},
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Y,
Width = 160,
Padding = new MarginPadding { Horizontal = 15 },
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4),
Children = new[]
{
hitObjectsCheckbox = new OsuCheckbox { LabelText = "Hitobjects" },
hitSoundsCheckbox = new OsuCheckbox { LabelText = "Hitsounds" },
waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" }
}
}
}
},
new Container
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("333")
},
new Container<TimelineButton>
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Masking = true,
Children = new[]
{
new TimelineButton
{
RelativeSizeAxes = Axes.Y,
Height = 0.5f,
Icon = FontAwesome.fa_search_plus,
Action = () => timelineContainer.Zoom++
},
new TimelineButton
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Y,
Height = 0.5f,
Icon = FontAwesome.fa_search_minus,
Action = () => timelineContainer.Zoom--
},
}
}
}
},
timelineContainer = new ScrollingTimelineContainer { RelativeSizeAxes = Axes.Y }
}
}
};
hitObjectsCheckbox.Current.Value = true;
hitSoundsCheckbox.Current.Value = true;
waveformCheckbox.Current.Value = true;
timelineContainer.WaveformVisible.BindTo(waveformCheckbox.Current);
}
protected override void Update()
{
base.Update();
timelineContainer.Size = new Vector2(DrawSize.X - timelineContainer.DrawPosition.X, 1);
}
}
}

View File

@ -1,151 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Allocation;
using OpenTK;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Screens.Compose.Timeline
{
public class ScrollingTimelineContainer : ScrollContainer
{
public readonly Bindable<bool> HitObjectsVisible = new Bindable<bool>();
public readonly Bindable<bool> HitSoundsVisible = new Bindable<bool>();
public readonly Bindable<bool> WaveformVisible = new Bindable<bool>();
private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
private readonly BeatmapWaveformGraph waveform;
public ScrollingTimelineContainer()
: base(Direction.Horizontal)
{
Masking = true;
Add(waveform = new BeatmapWaveformGraph
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("222"),
Depth = float.MaxValue
});
Content.AutoSizeAxes = Axes.None;
Content.RelativeSizeAxes = Axes.Both;
WaveformVisible.ValueChanged += waveformVisibilityChanged;
Zoom = 10;
}
[BackgroundDependencyLoader]
private void load(IBindableBeatmap beatmap)
{
this.beatmap.BindTo(beatmap);
this.beatmap.BindValueChanged(beatmapChanged, true);
}
private void beatmapChanged(WorkingBeatmap beatmap) => waveform.Beatmap = beatmap;
private float minZoom = 1;
/// <summary>
/// The minimum zoom level allowed.
/// </summary>
public float MinZoom
{
get { return minZoom; }
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value));
if (minZoom == value)
return;
minZoom = value;
// Update the zoom level
Zoom = Zoom;
}
}
private float maxZoom = 30;
/// <summary>
/// The maximum zoom level allowed.
/// </summary>
public float MaxZoom
{
get { return maxZoom; }
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value));
if (maxZoom == value)
return;
maxZoom = value;
// Update the zoom level
Zoom = Zoom;
}
}
private float zoom = 1;
/// <summary>
/// The current zoom level.
/// </summary>
public float Zoom
{
get { return zoom; }
set
{
value = MathHelper.Clamp(value, MinZoom, MaxZoom);
if (zoom == value)
return;
zoom = value;
// Make the zoom target default to the center of the graph if it hasn't been set
if (relativeContentZoomTarget == null)
relativeContentZoomTarget = ToSpaceOfOtherDrawable(DrawSize / 2, Content).X / Content.DrawSize.X;
if (localZoomTarget == null)
localZoomTarget = DrawSize.X / 2;
Content.ResizeWidthTo(Zoom);
// Update the scroll position to focus on the zoom target
float scrollPos = Content.DrawSize.X * relativeContentZoomTarget.Value - localZoomTarget.Value;
ScrollTo(scrollPos, false);
relativeContentZoomTarget = null;
localZoomTarget = null;
}
}
/// <summary>
/// Zoom target as a relative position in the <see cref="ScrollingTimelineContainer.Content"/> space.
/// </summary>
private float? relativeContentZoomTarget;
/// <summary>
/// Zoom target as a position in our local space.
/// </summary>
private float? localZoomTarget;
protected override bool OnScroll(InputState state)
{
if (!state.Keyboard.ControlPressed)
return base.OnScroll(state);
relativeContentZoomTarget = Content.ToLocalSpace(state.Mouse.NativeState.Position).X / Content.DrawSize.X;
localZoomTarget = ToLocalSpace(state.Mouse.NativeState.Position).X;
Zoom += state.Mouse.ScrollDelta.Y;
return true;
}
private void waveformVisibilityChanged(bool visible) => waveform.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint);
}
}

View File

@ -0,0 +1,57 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Screens.Compose.Timeline
{
public class Timeline : ZoomableScrollContainer
{
public readonly Bindable<bool> WaveformVisible = new Bindable<bool>();
public readonly IBindable<WorkingBeatmap> Beatmap = new Bindable<WorkingBeatmap>();
public Timeline()
{
ZoomDuration = 200;
ZoomEasing = Easing.OutQuint;
Zoom = 10;
}
private WaveformGraph waveform;
[BackgroundDependencyLoader]
private void load(IBindableBeatmap beatmap)
{
Child = waveform = new WaveformGraph
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("222"),
Depth = float.MaxValue
};
WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint);
Beatmap.BindTo(beatmap);
}
protected override void LoadComplete()
{
base.LoadComplete();
Beatmap.BindValueChanged(b => waveform.Waveform = b.Waveform);
waveform.Waveform = Beatmap.Value.Waveform;
}
protected override void Update()
{
base.Update();
// We want time = 0 to be at the centre of the container when scrolled to the start
Content.Margin = new MarginPadding { Horizontal = DrawWidth / 2 };
}
}
}

View File

@ -0,0 +1,128 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Edit.Screens.Compose.Timeline
{
public class TimelineArea : CompositeDrawable
{
private readonly Timeline timeline;
public TimelineArea()
{
Masking = true;
CornerRadius = 5;
OsuCheckbox hitObjectsCheckbox;
OsuCheckbox hitSoundsCheckbox;
OsuCheckbox waveformCheckbox;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("111")
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("222")
},
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Y,
Width = 160,
Padding = new MarginPadding { Horizontal = 15 },
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4),
Children = new[]
{
hitObjectsCheckbox = new OsuCheckbox { LabelText = "Hitobjects" },
hitSoundsCheckbox = new OsuCheckbox { LabelText = "Hitsounds" },
waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" }
}
}
}
},
new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.FromHex("333")
},
new Container<TimelineButton>
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Masking = true,
Children = new[]
{
new TimelineButton
{
RelativeSizeAxes = Axes.Y,
Height = 0.5f,
Icon = FontAwesome.fa_search_plus,
Action = () => timeline.Zoom++
},
new TimelineButton
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Y,
Height = 0.5f,
Icon = FontAwesome.fa_search_minus,
Action = () => timeline.Zoom--
},
}
}
}
},
timeline = new Timeline { RelativeSizeAxes = Axes.Both }
},
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Distributed),
}
}
};
hitObjectsCheckbox.Current.Value = true;
hitSoundsCheckbox.Current.Value = true;
waveformCheckbox.Current.Value = true;
timeline.WaveformVisible.BindTo(waveformCheckbox.Current);
}
}
}

View File

@ -0,0 +1,174 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using OpenTK;
namespace osu.Game.Screens.Edit.Screens.Compose.Timeline
{
public class ZoomableScrollContainer : ScrollContainer
{
/// <summary>
/// The time to zoom into/out of a point.
/// All user scroll input will be overwritten during the zoom transform.
/// </summary>
public double ZoomDuration;
/// <summary>
/// The easing with which to transform the zoom.
/// </summary>
public Easing ZoomEasing;
private readonly Container zoomedContent;
protected override Container<Drawable> Content => zoomedContent;
private float currentZoom = 1;
public ZoomableScrollContainer()
: base(Direction.Horizontal)
{
base.Content.Add(zoomedContent = new Container { RelativeSizeAxes = Axes.Y });
}
private int minZoom = 1;
/// <summary>
/// The minimum zoom level allowed.
/// </summary>
public int MinZoom
{
get => minZoom;
set
{
if (value < 1)
throw new ArgumentException($"{nameof(MinZoom)} must be >= 1.", nameof(value));
minZoom = value;
if (Zoom < value)
Zoom = value;
}
}
private int maxZoom = 60;
/// <summary>
/// The maximum zoom level allowed.
/// </summary>
public int MaxZoom
{
get => maxZoom;
set
{
if (value < 1)
throw new ArgumentException($"{nameof(MaxZoom)} must be >= 1.", nameof(value));
maxZoom = value;
if (Zoom > value)
Zoom = value;
}
}
/// <summary>
/// Gets or sets the content zoom level of this <see cref="ZoomableScrollContainer"/>.
/// </summary>
public float Zoom
{
get => zoomTarget;
set
{
value = MathHelper.Clamp(value, MinZoom, MaxZoom);
if (IsLoaded)
setZoomTarget(value, ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), zoomedContent).X);
else
currentZoom = zoomTarget = value;
}
}
protected override void Update()
{
base.Update();
zoomedContent.Width = DrawWidth * currentZoom;
}
protected override bool OnScroll(InputState state)
{
if (!state.Keyboard.ControlPressed)
return base.OnScroll(state);
setZoomTarget(zoomTarget + state.Mouse.ScrollDelta.X, zoomedContent.ToLocalSpace(state.Mouse.NativeState.Position).X);
return true;
}
private float zoomTarget = 1;
private void setZoomTarget(float newZoom, float focusPoint)
{
zoomTarget = MathHelper.Clamp(newZoom, MinZoom, MaxZoom);
transformZoomTo(zoomTarget, focusPoint, ZoomDuration, ZoomEasing);
}
private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None)
=> this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing));
private class TransformZoom : Transform<float, ZoomableScrollContainer>
{
/// <summary>
/// The focus point in absolute coordinates local to the content.
/// </summary>
private readonly float focusPoint;
/// <summary>
/// The size of the content.
/// </summary>
private readonly float contentSize;
/// <summary>
/// The scroll offset at the start of the transform.
/// </summary>
private readonly float scrollOffset;
/// <summary>
/// Transforms <see cref="TimeTimelinem"/> to a new value.
/// </summary>
/// <param name="focusPoint">The focus point in absolute coordinates local to the content.</param>
/// <param name="contentSize">The size of the content.</param>
/// <param name="scrollOffset">The scroll offset at the start of the transform.</param>
public TransformZoom(float focusPoint, float contentSize, float scrollOffset)
{
this.focusPoint = focusPoint;
this.contentSize = contentSize;
this.scrollOffset = scrollOffset;
}
public override string TargetMember => nameof(currentZoom);
private float valueAt(double time)
{
if (time < StartTime) return StartValue;
if (time >= EndTime) return EndValue;
return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
}
protected override void Apply(ZoomableScrollContainer d, double time)
{
float newZoom = valueAt(time);
float focusOffset = focusPoint - scrollOffset;
float expectedWidth = d.DrawWidth * newZoom;
float targetOffset = expectedWidth * (focusPoint / contentSize) - focusOffset;
d.currentZoom = newZoom;
d.ScrollTo(targetOffset, false);
}
protected override void ReadIntoStartValue(ZoomableScrollContainer d) => StartValue = d.currentZoom;
}
}
}

View File

@ -18,7 +18,8 @@ namespace osu.Game.Screens.Play
{ {
private const int duration = 100; private const int duration = 100;
private Bindable<bool> showKeyCounter; public readonly Bindable<bool> Visible = new Bindable<bool>(true);
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
public KeyCounterCollection() public KeyCounterCollection()
{ {
@ -46,9 +47,10 @@ namespace osu.Game.Screens.Play
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
showKeyCounter = config.GetBindable<bool>(OsuSetting.KeyOverlay); config.BindWith(OsuSetting.KeyOverlay, configVisibility);
showKeyCounter.ValueChanged += keyCounterVisibility => this.FadeTo(keyCounterVisibility ? 1 : 0, duration);
showKeyCounter.TriggerChange(); Visible.BindValueChanged(_ => updateVisibility());
configVisibility.BindValueChanged(_ => updateVisibility(), true);
} }
//further: change default values here and in KeyCounter if needed, instead of passing them in every constructor //further: change default values here and in KeyCounter if needed, instead of passing them in every constructor
@ -111,6 +113,8 @@ namespace osu.Game.Screens.Play
} }
} }
private void updateVisibility() => this.FadeTo(Visible.Value || configVisibility.Value ? 1 : 0, duration);
public override bool HandleKeyboardInput => receptor == null; public override bool HandleKeyboardInput => receptor == null;
public override bool HandleMouseInput => receptor == null; public override bool HandleMouseInput => receptor == null;

View File

@ -229,6 +229,7 @@ namespace osu.Game.Screens.Play
}; };
hudOverlay.HoldToQuit.Action = Exit; hudOverlay.HoldToQuit.Action = Exit;
hudOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
if (ShowStoryboard) if (ShowStoryboard)
initializeStoryboard(false); initializeStoryboard(false);

View File

@ -16,7 +16,8 @@ using osu.Game.Rulesets.Objects;
namespace osu.Game.Tests.Beatmaps namespace osu.Game.Tests.Beatmaps
{ {
[TestFixture] [TestFixture]
public abstract class BeatmapConversionTest<TConvertValue> public abstract class BeatmapConversionTest<TConvertMapping, TConvertValue>
where TConvertMapping : ConvertMapping<TConvertValue>, IEquatable<TConvertMapping>, new()
where TConvertValue : IEquatable<TConvertValue> where TConvertValue : IEquatable<TConvertValue>
{ {
private const string resource_namespace = "Testing.Beatmaps"; private const string resource_namespace = "Testing.Beatmaps";
@ -59,9 +60,13 @@ namespace osu.Game.Tests.Beatmaps
else if (objectCounter >= expectedMapping.Objects.Count) else if (objectCounter >= expectedMapping.Objects.Count)
Assert.Fail($"The conversion generated a hitobject, but should not have, for hitobject at time: {ourMapping.StartTime}:\n" Assert.Fail($"The conversion generated a hitobject, but should not have, for hitobject at time: {ourMapping.StartTime}:\n"
+ $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n"); + $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n");
else if (!EqualityComparer<TConvertValue>.Default.Equals(expectedMapping.Objects[objectCounter], ourMapping.Objects[objectCounter])) else if (!expectedMapping.Equals(ourMapping))
Assert.Fail($"The conversion mapping differed for object at time {expectedMapping.StartTime}:\n"
+ $"Expected {JsonConvert.SerializeObject(expectedMapping)}\n"
+ $"Received: {JsonConvert.SerializeObject(ourMapping)}\n");
else if (!expectedMapping.Objects[objectCounter].Equals(ourMapping.Objects[objectCounter]))
{ {
Assert.Fail($"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}\n" Assert.Fail($"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}:\n"
+ $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n" + $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n"
+ $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n"); + $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n");
} }
@ -84,19 +89,22 @@ namespace osu.Game.Tests.Beatmaps
beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo(); beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo();
var result = new ConvertResult(); var result = new ConvertResult();
var converter = rulesetInstance.CreateBeatmapConverter(beatmap); var converter = rulesetInstance.CreateBeatmapConverter(beatmap);
converter.ObjectConverted += (orig, converted) => converter.ObjectConverted += (orig, converted) =>
{ {
converted.ForEach(h => h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty)); converted.ForEach(h => h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty));
var mapping = new ConvertMapping { StartTime = orig.StartTime }; var mapping = CreateConvertMapping();
mapping.StartTime = orig.StartTime;
foreach (var obj in converted) foreach (var obj in converted)
mapping.Objects.AddRange(CreateConvertValue(obj)); mapping.Objects.AddRange(CreateConvertValue(obj));
result.Mappings.Add(mapping); result.Mappings.Add(mapping);
}; };
converter.Convert(); IBeatmap convertedBeatmap = converter.Convert();
rulesetInstance.CreateBeatmapProcessor(convertedBeatmap)?.PostProcess();
return result; return result;
} }
@ -128,21 +136,54 @@ namespace osu.Game.Tests.Beatmaps
return Assembly.LoadFrom(Path.Combine(localPath, $"{ResourceAssembly}.dll")).GetManifestResourceStream($@"{ResourceAssembly}.Resources.{name}"); return Assembly.LoadFrom(Path.Combine(localPath, $"{ResourceAssembly}.dll")).GetManifestResourceStream($@"{ResourceAssembly}.Resources.{name}");
} }
protected abstract IEnumerable<TConvertValue> CreateConvertValue(HitObject hitObject); /// <summary>
protected abstract Ruleset CreateRuleset(); /// Creates the conversion mapping for a <see cref="HitObject"/>. A conversion mapping stores important information about the conversion process.
/// This is generated _after_ the <see cref="HitObject"/> has been converted.
/// <para>
/// This should be used to validate the integrity of the conversion process after a conversion has occurred.
/// </para>
/// </summary>
protected virtual TConvertMapping CreateConvertMapping() => new TConvertMapping();
private class ConvertMapping /// <summary>
{ /// Creates the conversion value for a <see cref="HitObject"/>. A conversion value stores information about the converted <see cref="HitObject"/>.
[JsonProperty] /// <para>
public double StartTime; /// This should be used to validate the integrity of the converted <see cref="HitObject"/>.
[JsonProperty] /// </para>
public List<TConvertValue> Objects = new List<TConvertValue>(); /// </summary>
} /// <param name="hitObject">The converted <see cref="HitObject"/>.</param>
protected abstract IEnumerable<TConvertValue> CreateConvertValue(HitObject hitObject);
/// <summary>
/// Creates the <see cref="Ruleset"/> applicable to this <see cref="BeatmapConversionTest{TConvertMapping,TConvertValue}"/>.
/// </summary>
/// <returns></returns>
protected abstract Ruleset CreateRuleset();
private class ConvertResult private class ConvertResult
{ {
[JsonProperty] [JsonProperty]
public List<ConvertMapping> Mappings = new List<ConvertMapping>(); public List<TConvertMapping> Mappings = new List<TConvertMapping>();
} }
} }
public abstract class BeatmapConversionTest<TConvertValue> : BeatmapConversionTest<ConvertMapping<TConvertValue>, TConvertValue>
where TConvertValue : IEquatable<TConvertValue>
{
}
public class ConvertMapping<TConvertValue> : IEquatable<ConvertMapping<TConvertValue>>
where TConvertValue : IEquatable<TConvertValue>
{
[JsonProperty]
public double StartTime;
[JsonIgnore]
public List<TConvertValue> Objects = new List<TConvertValue>();
[JsonProperty("Objects")]
private List<TConvertValue> setObjects { set => Objects = value; }
public virtual bool Equals(ConvertMapping<TConvertValue> other) => StartTime.Equals(other?.StartTime);
}
} }

View File

@ -1,9 +1,11 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Lists;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -51,6 +53,28 @@ namespace osu.Game.Tests.Visual
Player p = null; Player p = null;
AddStep(r.Name, () => p = loadPlayerFor(r)); AddStep(r.Name, () => p = loadPlayerFor(r));
AddUntilStep(() => ContinueCondition(p)); AddUntilStep(() => ContinueCondition(p));
AddAssert("no leaked beatmaps", () =>
{
p = null;
GC.Collect();
GC.WaitForPendingFinalizers();
int count = 0;
workingWeakReferences.ForEachAlive(_ => count++);
return count == 1;
});
AddAssert("no leaked players", () =>
{
GC.Collect();
GC.WaitForPendingFinalizers();
int count = 0;
playerWeakReferences.ForEachAlive(_ => count++);
return count == 1;
});
} }
} }
} }
@ -59,21 +83,32 @@ namespace osu.Game.Tests.Visual
protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo); protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo);
private readonly WeakList<WorkingBeatmap> workingWeakReferences = new WeakList<WorkingBeatmap>();
private readonly WeakList<Player> playerWeakReferences = new WeakList<Player>();
private Player loadPlayerFor(RulesetInfo ri) => loadPlayerFor(ri.CreateInstance()); private Player loadPlayerFor(RulesetInfo ri) => loadPlayerFor(ri.CreateInstance());
private Player loadPlayerFor(Ruleset r) private Player loadPlayerFor(Ruleset r)
{ {
var beatmap = CreateBeatmap(r); var beatmap = CreateBeatmap(r);
var working = new TestWorkingBeatmap(beatmap);
Beatmap.Value = new TestWorkingBeatmap(beatmap); workingWeakReferences.Add(working);
Beatmap.Value = working;
Beatmap.Value.Mods.Value = new[] { r.GetAllMods().First(m => m is ModNoFail) }; Beatmap.Value.Mods.Value = new[] { r.GetAllMods().First(m => m is ModNoFail) };
if (Player != null) Player?.Exit();
Remove(Player);
var player = CreatePlayer(r); var player = CreatePlayer(r);
LoadComponentAsync(player, LoadScreen); playerWeakReferences.Add(player);
LoadComponentAsync(player, p =>
{
Player = p;
LoadScreen(p);
});
return player; return player;
} }

View File

@ -18,7 +18,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="ppy.osu.Framework" Version="2018.608.0" /> <PackageReference Include="ppy.osu.Framework" Version="2018.611.1" />
<PackageReference Include="SharpCompress" Version="0.18.1" /> <PackageReference Include="SharpCompress" Version="0.18.1" />
<PackageReference Include="NUnit" Version="3.10.1" /> <PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />