1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 05:22:54 +08:00

Merge branch 'master' into player-loader-star-rating

This commit is contained in:
Dean Herbert 2021-05-15 15:39:57 +09:00 committed by GitHub
commit dc56250a3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
200 changed files with 6303 additions and 1230 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.507.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.513.0" />
</ItemGroup>
</Project>

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
InternalChildren = new Drawable[]
{
explosion = new LegacyRollingCounter(skin, LegacyFont.Combo)
explosion = new LegacyRollingCounter(LegacyFont.Combo)
{
Alpha = 0.65f,
Blending = BlendingParameters.Additive,
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
Origin = Anchor.Centre,
Scale = new Vector2(1.5f),
},
counter = new LegacyRollingCounter(skin, LegacyFont.Combo)
counter = new LegacyRollingCounter(LegacyFont.Combo)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.2);
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.5);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);

View File

@ -6,6 +6,7 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Mania.UI;
@ -24,6 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
[Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; }
/// <summary>
/// Gets the samples that are played by this object during gameplay.
/// </summary>
public ISampleInfo[] GetGameplaySamples() => Samples.Samples;
protected override float SamplePlaybackPosition
{
get

View File

@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Mania.UI
public const float COLUMN_WIDTH = 80;
public const float SPECIAL_COLUMN_WIDTH = 70;
/// <summary>
/// For hitsounds played by this <see cref="Column"/> (i.e. not as a result of hitting a hitobject),
/// a certain number of samples are allowed to be played concurrently so that it feels better when spam-pressing the key.
/// </summary>
private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY;
/// <summary>
/// The index of this column as part of the whole playfield.
/// </summary>
@ -38,6 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy;
private readonly Container<SkinnableSound> hitSounds;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
@ -64,6 +71,12 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both
},
background,
hitSounds = new Container<SkinnableSound>
{
Name = "Column samples pool",
RelativeSizeAxes = Axes.Both,
Children = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray()
},
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
@ -120,6 +133,8 @@ namespace osu.Game.Rulesets.Mania.UI
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
}
private int nextHitSoundIndex;
public bool OnPressed(ManiaAction action)
{
if (action != Action.Value)
@ -131,7 +146,15 @@ namespace osu.Game.Rulesets.Mania.UI
HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
HitObjectContainer.Objects.LastOrDefault();
nextObject?.PlaySamples();
if (nextObject is DrawableManiaHitObject maniaObject)
{
var hitSound = hitSounds[nextHitSoundIndex];
hitSound.Samples = maniaObject.GetGameplaySamples();
hitSound.Play();
nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds;
}
return true;
}

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Edit.Checks;
@ -224,12 +225,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
private void assertOk(IBeatmap beatmap)
{
Assert.That(check.Run(beatmap, new TestWorkingBeatmap(beatmap)), Is.Empty);
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
Assert.That(check.Run(context), Is.Empty);
}
private void assertOffscreenCircle(IBeatmap beatmap)
{
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
@ -237,7 +240,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
private void assertOffscreenSlider(IBeatmap beatmap)
{
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider);

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
Child = new SkinProvidingContainer(new DefaultSkin())
Child = new SkinProvidingContainer(new DefaultSkin(null))
{
RelativeSizeAxes = Axes.Both,
Child = drawableHitCircle = new DrawableHitCircle(hitCircle)

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@ -31,9 +31,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks
new IssueTemplateOffscreenSlider(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
foreach (var hitobject in playableBeatmap.HitObjects)
foreach (var hitobject in context.Beatmap.HitObjects)
{
switch (hitobject)
{

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Edit.Checks;
@ -17,9 +16,9 @@ namespace osu.Game.Rulesets.Osu.Edit
new CheckOffscreenObjects()
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
return checks.SelectMany(check => check.Run(playableBeatmap, workingBeatmap));
return checks.SelectMany(check => check.Run(context));
}
}
}

View File

@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 115,
},
bonusCounter = new LegacySpriteText(source, LegacyFont.Score)
bonusCounter = new LegacySpriteText(LegacyFont.Score)
{
Alpha = 0f,
Anchor = Anchor.TopCentre,
@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Scale = new Vector2(SPRITE_SCALE),
Position = new Vector2(-87, 445 + spm_hide_offset),
},
spmCounter = new LegacySpriteText(source, LegacyFont.Score)
spmCounter = new LegacySpriteText(LegacyFont.Score)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight,

View File

@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (!this.HasFont(LegacyFont.HitCircle))
return null;
return new LegacySpriteText(Source, LegacyFont.HitCircle)
return new LegacySpriteText(LegacyFont.HitCircle)
{
// stable applies a blanket 0.8x scale to hitcircle fonts
Scale = new Vector2(0.8f),

View File

@ -5,6 +5,7 @@ using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
using osu.Game.Skinning;
@ -27,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
}));
AddToggleStep("Toggle passing", passing => this.ChildrenOfType<LegacyTaikoScroller>().ForEach(s => s.LastResult.Value =
new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Great : HitResult.Miss }));
new JudgementResult(new HitObject(), new Judgement()) { Type = passing ? HitResult.Great : HitResult.Miss }));
AddToggleStep("toggle playback direction", reversed => this.reversed = reversed);
}

View File

@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
base.Update();
// store X before checking wide enough so if we perform layout there is no positional discrepancy.
float currentX = (InternalChildren?.FirstOrDefault()?.X ?? 0) - (float)Clock.ElapsedFrameTime * 0.1f;
float currentX = (InternalChildren.FirstOrDefault()?.X ?? 0) - (float)Clock.ElapsedFrameTime * 0.1f;
// ensure we have enough sprites
if (!InternalChildren.Any()

View File

@ -10,7 +10,7 @@ using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
public class BeatmapDifficultyManagerTest
public class BeatmapDifficultyCacheTest
{
[Test]
public void TestKeyEqualsWithDifferentModInstances()

View File

@ -12,6 +12,7 @@ using osu.Framework.Platform;
using osu.Game.IPC;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
@ -264,7 +265,7 @@ namespace osu.Game.Tests.Beatmaps.IO
// change filename
var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First());
firstFile.MoveTo(Path.Combine(firstFile.DirectoryName, $"{firstFile.Name}-changed{firstFile.Extension}"));
firstFile.MoveTo(Path.Combine(firstFile.DirectoryName.AsNonNull(), $"{firstFile.Name}-changed{firstFile.Extension}"));
using (var zip = ZipArchive.Create())
{

View File

@ -6,6 +6,7 @@ using Moq;
using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
@ -40,23 +41,23 @@ namespace osu.Game.Tests.Editing.Checks
mock.SetupGet(w => w.Beatmap).Returns(beatmap);
mock.SetupGet(w => w.Track).Returns((Track)null);
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
Assert.That(check.Run(new BeatmapVerifierContext(beatmap, mock.Object)), Is.Empty);
}
[Test]
public void TestAcceptable()
{
var mock = getMockWorkingBeatmap(192);
var context = getContext(192);
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestNullBitrate()
{
var mock = getMockWorkingBeatmap(null);
var context = getContext(null);
var issues = check.Run(beatmap, mock.Object).ToList();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateNoBitrate);
@ -65,9 +66,9 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
public void TestZeroBitrate()
{
var mock = getMockWorkingBeatmap(0);
var context = getContext(0);
var issues = check.Run(beatmap, mock.Object).ToList();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateNoBitrate);
@ -76,9 +77,9 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
public void TestTooHighBitrate()
{
var mock = getMockWorkingBeatmap(320);
var context = getContext(320);
var issues = check.Run(beatmap, mock.Object).ToList();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooHighBitrate);
@ -87,14 +88,19 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
public void TestTooLowBitrate()
{
var mock = getMockWorkingBeatmap(64);
var context = getContext(64);
var issues = check.Run(beatmap, mock.Object).ToList();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckAudioQuality.IssueTemplateTooLowBitrate);
}
private BeatmapVerifierContext getContext(int? audioBitrate)
{
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(audioBitrate).Object);
}
/// <summary>
/// Returns the mock of the working beatmap with the given audio properties.
/// </summary>

View File

@ -9,6 +9,7 @@ using Moq;
using NUnit.Framework;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using FileInfo = osu.Game.IO.FileInfo;
@ -53,25 +54,25 @@ namespace osu.Game.Tests.Editing.Checks
{
// While this is a problem, it is out of scope for this check and is caught by a different one.
beatmap.Metadata.BackgroundFile = null;
var mock = getMockWorkingBeatmap(null, System.Array.Empty<byte>());
var context = getContext(null, System.Array.Empty<byte>());
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestAcceptable()
{
var mock = getMockWorkingBeatmap(new Texture(1920, 1080));
var context = getContext(new Texture(1920, 1080));
Assert.That(check.Run(beatmap, mock.Object), Is.Empty);
Assert.That(check.Run(context), Is.Empty);
}
[Test]
public void TestTooHighResolution()
{
var mock = getMockWorkingBeatmap(new Texture(3840, 2160));
var context = getContext(new Texture(3840, 2160));
var issues = check.Run(beatmap, mock.Object).ToList();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooHighResolution);
@ -80,9 +81,9 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
public void TestLowResolution()
{
var mock = getMockWorkingBeatmap(new Texture(640, 480));
var context = getContext(new Texture(640, 480));
var issues = check.Run(beatmap, mock.Object).ToList();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateLowResolution);
@ -91,9 +92,9 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
public void TestTooLowResolution()
{
var mock = getMockWorkingBeatmap(new Texture(100, 100));
var context = getContext(new Texture(100, 100));
var issues = check.Run(beatmap, mock.Object).ToList();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooLowResolution);
@ -102,14 +103,19 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
public void TestTooUncompressed()
{
var mock = getMockWorkingBeatmap(new Texture(1920, 1080), new byte[1024 * 1024 * 3]);
var context = getContext(new Texture(1920, 1080), new byte[1024 * 1024 * 3]);
var issues = check.Run(beatmap, mock.Object).ToList();
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckBackgroundQuality.IssueTemplateTooUncompressed);
}
private BeatmapVerifierContext getContext(Texture background, [CanBeNull] byte[] fileBytes = null)
{
return new BeatmapVerifierContext(beatmap, getMockWorkingBeatmap(background, fileBytes).Object);
}
/// <summary>
/// Returns the mock of the working beatmap with the given background and filesize.
/// </summary>

View File

@ -6,11 +6,13 @@ using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
@ -105,7 +107,7 @@ namespace osu.Game.Tests.Editing.Checks
new HitCircle { StartTime = 300 }
};
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
var issues = check.Run(getContext(hitobjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(3));
Assert.That(issues.Where(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent).ToList(), Has.Count.EqualTo(2));
@ -164,12 +166,12 @@ namespace osu.Game.Tests.Editing.Checks
private void assertOk(List<HitObject> hitobjects)
{
Assert.That(check.Run(getPlayableBeatmap(hitobjects), null), Is.Empty);
Assert.That(check.Run(getContext(hitobjects)), Is.Empty);
}
private void assertConcurrentSame(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
var issues = check.Run(getContext(hitobjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
@ -177,18 +179,16 @@ namespace osu.Game.Tests.Editing.Checks
private void assertConcurrentDifferent(List<HitObject> hitobjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
var issues = check.Run(getContext(hitobjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent));
}
private IBeatmap getPlayableBeatmap(List<HitObject> hitobjects)
private BeatmapVerifierContext getContext(List<HitObject> hitobjects)
{
return new Beatmap<HitObject>
{
HitObjects = hitobjects
};
var beatmap = new Beatmap<HitObject> { HitObjects = hitobjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps;
@ -45,7 +46,8 @@ namespace osu.Game.Tests.Editing.Checks
[Test]
public void TestBackgroundSetAndInFiles()
{
Assert.That(check.Run(beatmap, new TestWorkingBeatmap(beatmap)), Is.Empty);
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
Assert.That(check.Run(context), Is.Empty);
}
[Test]
@ -53,7 +55,8 @@ namespace osu.Game.Tests.Editing.Checks
{
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckFilePresence.IssueTemplateDoesNotExist);
@ -64,7 +67,8 @@ namespace osu.Game.Tests.Editing.Checks
{
beatmap.Metadata.BackgroundFile = null;
var issues = check.Run(beatmap, new TestWorkingBeatmap(beatmap)).ToList();
var context = new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
var issues = check.Run(context).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.Single().Template is CheckFilePresence.IssueTemplateNoneSet);

View File

@ -7,10 +7,12 @@ using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Editing.Checks
{
@ -100,12 +102,12 @@ namespace osu.Game.Tests.Editing.Checks
}, count: 2);
// Start and end are 2 ms and 1.25 ms off respectively, hence two different issues in one object.
var hitobjects = new List<HitObject>
var hitObjects = new List<HitObject>
{
getSliderMock(startTime: 98, endTime: 398.75d).Object
};
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(2));
Assert.That(issues.Any(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateSmallUnsnap));
@ -122,34 +124,36 @@ namespace osu.Game.Tests.Editing.Checks
return mockSlider;
}
private void assertOk(List<HitObject> hitobjects)
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getPlayableBeatmap(hitobjects), null), Is.Empty);
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assert1Ms(List<HitObject> hitobjects, int count = 1)
private void assert1Ms(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateSmallUnsnap));
}
private void assert2Ms(List<HitObject> hitobjects, int count = 1)
private void assert2Ms(List<HitObject> hitObjects, int count = 1)
{
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(count));
Assert.That(issues.All(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateLargeUnsnap));
}
private IBeatmap getPlayableBeatmap(List<HitObject> hitobjects)
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
return new Beatmap<HitObject>
var beatmap = new Beatmap<HitObject>
{
ControlPointInfo = cpi,
HitObjects = hitobjects
HitObjects = hitObjects
};
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -146,7 +146,7 @@ namespace osu.Game.Tests.Mods
if (isValid)
Assert.IsNull(invalid);
else
Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
public abstract class CustomMod1 : Mod

View File

@ -4,6 +4,7 @@
using System.Linq;
using Humanizer;
using NUnit.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
using osu.Game.Tests.Visual.Multiplayer;
@ -34,7 +35,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
changeState(6, MultiplayerUserState.WaitingForLoad);
checkPlayingUserCount(6);
AddStep("another user left", () => Client.RemoveUser(Client.Room?.Users.Last().User));
AddStep("another user left", () => Client.RemoveUser((Client.Room?.Users.Last().User).AsNonNull()));
checkPlayingUserCount(5);
AddStep("leave room", () => Client.LeaveRoom());
@ -53,9 +54,9 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
Client.RoomSetupAction = room =>
{
room.State = MultiplayerRoomState.Playing;
room.Users.Add(new MultiplayerRoomUser(55)
room.Users.Add(new MultiplayerRoomUser(PLAYER_1_ID)
{
User = new User { Id = 55 },
User = new User { Id = PLAYER_1_ID },
State = MultiplayerUserState.Playing
});
};

View File

@ -0,0 +1,223 @@
// 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 NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.OnlinePlay
{
[HeadlessTest]
public class TestSceneCatchUpSyncManager : OsuTestScene
{
private TestManualClock master;
private CatchUpSyncManager syncManager;
private TestSpectatorPlayerClock player1;
private TestSpectatorPlayerClock player2;
[SetUp]
public void Setup()
{
syncManager = new CatchUpSyncManager(master = new TestManualClock());
syncManager.AddPlayerClock(player1 = new TestSpectatorPlayerClock(1));
syncManager.AddPlayerClock(player2 = new TestSpectatorPlayerClock(2));
Schedule(() => Child = syncManager);
}
[Test]
public void TestMasterClockStartsWhenAllPlayerClocksHaveFrames()
{
setWaiting(() => player1, false);
assertMasterState(false);
assertPlayerClockState(() => player1, false);
assertPlayerClockState(() => player2, false);
setWaiting(() => player2, false);
assertMasterState(true);
assertPlayerClockState(() => player1, true);
assertPlayerClockState(() => player2, true);
}
[Test]
public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime()
{
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
assertMasterState(false);
}
[Test]
public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime()
{
setWaiting(() => player1, false);
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
assertMasterState(true);
}
[Test]
public void TestPlayerClockDoesNotCatchUpWhenSlightlyOutOfSync()
{
setAllWaiting(false);
setMasterTime(CatchUpSyncManager.SYNC_TARGET + 1);
assertCatchingUp(() => player1, false);
}
[Test]
public void TestPlayerClockStartsCatchingUpWhenTooFarBehind()
{
setAllWaiting(false);
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1);
assertCatchingUp(() => player1, true);
assertCatchingUp(() => player2, true);
}
[Test]
public void TestPlayerClockKeepsCatchingUpWhenSlightlyOutOfSync()
{
setAllWaiting(false);
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1);
setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET + 1);
assertCatchingUp(() => player1, true);
}
[Test]
public void TestPlayerClockStopsCatchingUpWhenInSync()
{
setAllWaiting(false);
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 2);
setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET);
assertCatchingUp(() => player1, false);
assertCatchingUp(() => player2, true);
}
[Test]
public void TestPlayerClockDoesNotStopWhenSlightlyAhead()
{
setAllWaiting(false);
setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET);
assertCatchingUp(() => player1, false);
assertPlayerClockState(() => player1, true);
}
[Test]
public void TestPlayerClockStopsWhenTooFarAheadAndStartsWhenBackInSync()
{
setAllWaiting(false);
setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET - 1);
// This is a silent catchup, where IsCatchingUp = false but IsRunning = false also.
assertCatchingUp(() => player1, false);
assertPlayerClockState(() => player1, false);
setMasterTime(1);
assertCatchingUp(() => player1, false);
assertPlayerClockState(() => player1, true);
}
[Test]
public void TestInSyncPlayerClockDoesNotStartIfWaitingOnFrames()
{
setAllWaiting(false);
assertPlayerClockState(() => player1, true);
setWaiting(() => player1, true);
assertPlayerClockState(() => player1, false);
}
private void setWaiting(Func<TestSpectatorPlayerClock> playerClock, bool waiting)
=> AddStep($"set player clock {playerClock().Id} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting);
private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () =>
{
player1.WaitingOnFrames.Value = waiting;
player2.WaitingOnFrames.Value = waiting;
});
private void setMasterTime(double time)
=> AddStep($"set master = {time}", () => master.Seek(time));
/// <summary>
/// clock.Time = master.Time - offsetFromMaster
/// </summary>
private void setPlayerClockTime(Func<TestSpectatorPlayerClock> playerClock, double offsetFromMaster)
=> AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
private void assertMasterState(bool running)
=> AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running);
private void assertCatchingUp(Func<TestSpectatorPlayerClock> playerClock, bool catchingUp) =>
AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
private void assertPlayerClockState(Func<TestSpectatorPlayerClock> playerClock, bool running)
=> AddAssert($"player clock {playerClock().Id} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
private class TestSpectatorPlayerClock : TestManualClock, ISpectatorPlayerClock
{
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
public bool IsCatchingUp { get; set; }
public IFrameBasedClock Source
{
set => throw new NotImplementedException();
}
public readonly int Id;
public TestSpectatorPlayerClock(int id)
{
Id = id;
WaitingOnFrames.BindValueChanged(waiting =>
{
if (waiting.NewValue)
Stop();
else
Start();
});
}
public void ProcessFrame()
{
}
public double ElapsedFrameTime => 0;
public double FramesPerSecond => 0;
public FrameTimeInfo TimeInfo => default;
}
private class TestManualClock : ManualClock, IAdjustableClock
{
public void Start() => IsRunning = true;
public void Stop() => IsRunning = false;
public bool Seek(double position)
{
CurrentTime = position;
return true;
}
public void Reset()
{
}
public void ResetSpeedAdjustments()
{
}
}
}
}

View File

@ -1,45 +1,50 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneComposeSelectBox : OsuTestScene
public class TestSceneComposeSelectBox : OsuManualInputManagerTestScene
{
private Container selectionArea;
private SelectionBox selectionBox;
public TestSceneComposeSelectBox()
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectionBox selectionBox = null;
AddStep("create box", () =>
Child = selectionArea = new Container
Child = selectionArea = new Container
{
Size = new Vector2(400),
Position = -new Vector2(150),
Anchor = Anchor.Centre,
Children = new Drawable[]
{
Size = new Vector2(400),
Position = -new Vector2(150),
Anchor = Anchor.Centre,
Children = new Drawable[]
selectionBox = new SelectionBox
{
selectionBox = new SelectionBox
{
CanRotate = true,
CanScaleX = true,
CanScaleY = true,
RelativeSizeAxes = Axes.Both,
OnRotation = handleRotation,
OnScale = handleScale
}
CanRotate = true,
CanScaleX = true,
CanScaleY = true,
OnRotation = handleRotation,
OnScale = handleScale
}
});
}
};
AddToggleStep("toggle rotation", state => selectionBox.CanRotate = state);
AddToggleStep("toggle x", state => selectionBox.CanScaleX = state);
AddToggleStep("toggle y", state => selectionBox.CanScaleY = state);
}
InputManager.MoveMouseTo(selectionBox);
InputManager.ReleaseButton(MouseButton.Left);
});
private bool handleScale(Vector2 amount, Anchor reference)
{
@ -68,5 +73,115 @@ namespace osu.Game.Tests.Visual.Editing
selectionArea.Rotation += angle;
return true;
}
[Test]
public void TestRotationHandleShownOnHover()
{
SelectionBoxRotationHandle rotationHandle = null;
AddStep("retrieve rotation handle", () => rotationHandle = this.ChildrenOfType<SelectionBoxRotationHandle>().First());
AddAssert("handle hidden", () => rotationHandle.Alpha == 0);
AddStep("hover over handle", () => InputManager.MoveMouseTo(rotationHandle));
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox));
AddUntilStep("handle hidden", () => rotationHandle.Alpha == 0);
}
[Test]
public void TestRotationHandleShownOnHoveringClosestScaleHandler()
{
SelectionBoxRotationHandle rotationHandle = null;
AddStep("retrieve rotation handle", () => rotationHandle = this.ChildrenOfType<SelectionBoxRotationHandle>().First());
AddAssert("rotation handle hidden", () => rotationHandle.Alpha == 0);
AddStep("hover over closest scale handle", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == rotationHandle.Anchor));
});
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox));
AddUntilStep("handle hidden", () => rotationHandle.Alpha == 0);
}
[Test]
public void TestHoverRotationHandleFromScaleHandle()
{
SelectionBoxRotationHandle rotationHandle = null;
AddStep("retrieve rotation handle", () => rotationHandle = this.ChildrenOfType<SelectionBoxRotationHandle>().First());
AddAssert("rotation handle hidden", () => rotationHandle.Alpha == 0);
AddStep("hover over closest scale handle", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == rotationHandle.Anchor));
});
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddAssert("rotation handle not hovered", () => !rotationHandle.IsHovered);
AddStep("hover over rotation handle", () => InputManager.MoveMouseTo(rotationHandle));
AddAssert("rotation handle still shown", () => rotationHandle.Alpha == 1);
AddAssert("rotation handle hovered", () => rotationHandle.IsHovered);
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox));
AddUntilStep("handle hidden", () => rotationHandle.Alpha == 0);
}
[Test]
public void TestHoldingScaleHandleHidesCorrespondingRotationHandle()
{
SelectionBoxRotationHandle rotationHandle = null;
AddStep("retrieve rotation handle", () => rotationHandle = this.ChildrenOfType<SelectionBoxRotationHandle>().First());
AddAssert("rotation handle hidden", () => rotationHandle.Alpha == 0);
AddStep("hover over closest scale handle", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == rotationHandle.Anchor));
});
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddStep("hold scale handle", () => InputManager.PressButton(MouseButton.Left));
AddUntilStep("rotation handle hidden", () => rotationHandle.Alpha == 0);
int i;
ScheduledDelegate mouseMove = null;
AddStep("start dragging", () =>
{
i = 0;
mouseMove = Scheduler.AddDelayed(() =>
{
InputManager.MoveMouseTo(selectionBox.ScreenSpaceDrawQuad.TopLeft + Vector2.One * (5 * ++i));
}, 100, true);
});
AddAssert("rotation handle still hidden", () => rotationHandle.Alpha == 0);
AddStep("end dragging", () => mouseMove.Cancel());
AddAssert("rotation handle still hidden", () => rotationHandle.Alpha == 0);
AddStep("unhold left", () => InputManager.ReleaseButton(MouseButton.Left));
AddUntilStep("rotation handle shown", () => rotationHandle.Alpha == 1);
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox, new Vector2(20)));
AddUntilStep("rotation handle hidden", () => rotationHandle.Alpha == 0);
}
/// <summary>
/// Tests that hovering over two handles instantaneously from one to another does not crash or cause issues to the visibility state.
/// </summary>
[Test]
public void TestHoverOverTwoHandlesInstantaneously()
{
AddStep("hover over top-left scale handle", () =>
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == Anchor.TopLeft)));
AddStep("hover over top-right scale handle", () =>
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == Anchor.TopRight)));
AddUntilStep("top-left rotation handle hidden", () =>
this.ChildrenOfType<SelectionBoxRotationHandle>().Single(r => r.Anchor == Anchor.TopLeft).Alpha == 0);
AddUntilStep("top-right rotation handle shown", () =>
this.ChildrenOfType<SelectionBoxRotationHandle>().Single(r => r.Anchor == Anchor.TopRight).Alpha == 1);
}
}
}

View File

@ -50,9 +50,9 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) },
new HitCircle { StartTime = 300, Position = new Vector2(100) },
new HitCircle { StartTime = 400, Position = new Vector2(150) },
new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) },
}));
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects));
@ -95,9 +95,9 @@ namespace osu.Game.Tests.Visual.Editing
var addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) },
new HitCircle { StartTime = 300, Position = new Vector2(100) },
new HitCircle { StartTime = 400, Position = new Vector2(150) },
new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) },
};
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects));
@ -131,9 +131,9 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[]
{
new HitCircle { StartTime = 100 },
new HitCircle { StartTime = 200, Position = new Vector2(50) },
new HitCircle { StartTime = 300, Position = new Vector2(100) },
new HitCircle { StartTime = 400, Position = new Vector2(150) },
new HitCircle { StartTime = 200, Position = new Vector2(100) },
new HitCircle { StartTime = 300, Position = new Vector2(200) },
new HitCircle { StartTime = 400, Position = new Vector2(300) },
}));
moveMouseToObject(() => addedObjects[0]);

View File

@ -1,22 +1,18 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneComboCounter : SkinnableTestScene
{
private IEnumerable<SkinnableComboCounter> comboCounters => CreatedDrawables.OfType<SkinnableComboCounter>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[Cached]
@ -25,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create combo counters", () => SetContents(() => new SkinnableComboCounter()));
AddStep("Create combo counters", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.ComboCounter))));
}
[Test]

View File

@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
@ -50,9 +52,9 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
AddUntilStep("layer fade is visible", () => layer.Child.Alpha > 0.1f);
AddUntilStep("layer fade is visible", () => layer.ChildrenOfType<Container>().First().Alpha > 0.1f);
AddStep("set health to 1", () => layer.Current.Value = 1f);
AddUntilStep("layer fade is invisible", () => !layer.Child.IsPresent);
AddUntilStep("layer fade is invisible", () => !layer.ChildrenOfType<Container>().First().IsPresent);
}
[Test]

View File

@ -146,7 +146,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create overlay", () =>
{
hudOverlay = new HUDOverlay(scoreProcessor, null, Array.Empty<Mod>());
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));

View File

@ -2,15 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Judgements;
using osu.Framework.Utils;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Scoring;
@ -20,14 +21,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneHitErrorMeter : OsuTestScene
{
private BarHitErrorMeter barMeter;
private BarHitErrorMeter barMeter2;
private BarHitErrorMeter barMeter3;
private ColourHitErrorMeter colourMeter;
private ColourHitErrorMeter colourMeter2;
private ColourHitErrorMeter colourMeter3;
private HitWindows hitWindows;
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
public TestSceneHitErrorMeter()
{
recreateDisplay(new OsuHitWindows(), 5);
@ -105,40 +103,40 @@ namespace osu.Game.Tests.Visual.Gameplay
}
});
Add(barMeter = new BarHitErrorMeter(hitWindows, true)
Add(new BarHitErrorMeter(hitWindows, true)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
});
Add(barMeter2 = new BarHitErrorMeter(hitWindows, false)
Add(new BarHitErrorMeter(hitWindows, false)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
});
Add(barMeter3 = new BarHitErrorMeter(hitWindows, true)
Add(new BarHitErrorMeter(hitWindows, true)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
Rotation = 270,
});
Add(colourMeter = new ColourHitErrorMeter(hitWindows)
Add(new ColourHitErrorMeter(hitWindows)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 50 }
});
Add(colourMeter2 = new ColourHitErrorMeter(hitWindows)
Add(new ColourHitErrorMeter(hitWindows)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 }
});
Add(colourMeter3 = new ColourHitErrorMeter(hitWindows)
Add(new ColourHitErrorMeter(hitWindows)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.CentreLeft,
@ -149,18 +147,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private void newJudgement(double offset = 0)
{
var judgement = new JudgementResult(new HitObject(), new Judgement())
scoreProcessor.ApplyResult(new JudgementResult(new HitCircle { HitWindows = hitWindows }, new Judgement())
{
TimeOffset = offset == 0 ? RNG.Next(-150, 150) : offset,
Type = HitResult.Perfect,
};
barMeter.OnNewJudgement(judgement);
barMeter2.OnNewJudgement(judgement);
barMeter3.OnNewJudgement(judgement);
colourMeter.OnNewJudgement(judgement);
colourMeter2.OnNewJudgement(judgement);
colourMeter3.OnNewJudgement(judgement);
});
}
}
}

View File

@ -2,9 +2,12 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Gameplay
@ -13,14 +16,20 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private SkinEditor skinEditor;
[Resolved]
private SkinManager skinManager { get; set; }
protected override bool Autoplay => true;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add editor overlay", () =>
AddStep("reload skin editor", () =>
{
skinEditor?.Expire();
Player.ScaleTo(SkinEditorOverlay.VISIBLE_TARGET_SCALE);
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
});
}

View File

@ -0,0 +1,26 @@
// 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.Framework.Graphics;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning.Editor;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinEditorComponentsList : SkinnableTestScene
{
[Test]
public void TestToggleEditor()
{
AddStep("show available components", () => SetContents(() => new SkinComponentToolbox(300)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}));
}
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
}
}

View File

@ -1,13 +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.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
@ -32,12 +30,13 @@ namespace osu.Game.Tests.Visual.Gameplay
SetContents(() =>
{
var ruleset = new OsuRuleset();
var mods = new[] { ruleset.GetAutoplayMod() };
var working = CreateWorkingBeatmap(ruleset.RulesetInfo);
var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo);
var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap);
var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap, mods);
var hudOverlay = new HUDOverlay(scoreProcessor, drawableRuleset, Array.Empty<Mod>())
var hudOverlay = new HUDOverlay(drawableRuleset, mods)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -7,7 +7,7 @@ using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public void SetUpSteps()
{
AddStep("Set initial accuracy", () => scoreProcessor.Accuracy.Value = 1);
AddStep("Create accuracy counters", () => SetContents(() => new SkinnableAccuracyCounter()));
AddStep("Create accuracy counters", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter))));
}
[Test]

View File

@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
SetContents(() =>
{
hudOverlay = new HUDOverlay(scoreProcessor, null, Array.Empty<Mod>());
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually.
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));

View File

@ -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.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
@ -12,14 +10,12 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableHealthDisplay : SkinnableTestScene
{
private IEnumerable<SkinnableHealthDisplay> healthDisplays => CreatedDrawables.OfType<SkinnableHealthDisplay>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[Cached(typeof(HealthProcessor))]
@ -28,10 +24,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create health displays", () =>
{
SetContents(() => new SkinnableHealthDisplay());
});
AddStep("Create health displays", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.HealthDisplay))));
AddStep(@"Reset all", delegate
{
healthProcessor.Health.Value = 1;

View File

@ -1,23 +1,18 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinnableScoreCounter : SkinnableTestScene
{
private IEnumerable<SkinnableScoreCounter> scoreCounters => CreatedDrawables.OfType<SkinnableScoreCounter>();
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
[Cached]
@ -26,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Create score counters", () => SetContents(() => new SkinnableScoreCounter()));
AddStep("Create score counters", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.ScoreCounter))));
}
[Test]
@ -40,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestVeryLargeScore()
{
AddStep("set large score", () => scoreCounters.ForEach(counter => scoreProcessor.TotalScore.Value = 1_000_000_000));
AddStep("set large score", () => scoreProcessor.TotalScore.Value = 1_000_000_000);
}
}
}

View File

@ -1,34 +1,32 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSpectator : ScreenTestScene
{
private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" };
[Cached(typeof(SpectatorStreamingClient))]
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
@ -214,9 +212,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(beatmapId ?? importedBeatmapId));
private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(beatmapId ?? importedBeatmapId));
private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
private void checkPaused(bool state) =>
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
@ -225,89 +223,17 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("send frames", () =>
{
testSpectatorStreamingClient.SendFrames(nextFrame, count);
testSpectatorStreamingClient.SendFrames(streamingUser.Id, nextFrame, count);
nextFrame += count;
});
}
private void loadSpectatingScreen()
{
AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(testSpectatorStreamingClient.StreamingUser)));
AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser)));
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
}
public class TestSpectatorStreamingClient : SpectatorStreamingClient
{
public readonly User StreamingUser = new User { Id = 55, Username = "Test user" };
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
private int beatmapId;
public TestSpectatorStreamingClient()
: base(new DevelopmentEndpointConfiguration())
{
}
public void StartPlay(int beatmapId)
{
this.beatmapId = beatmapId;
sendState(beatmapId);
}
public void EndPlay(int beatmapId)
{
((ISpectatorClient)this).UserFinishedPlaying(StreamingUser.Id, new SpectatorState
{
BeatmapID = beatmapId,
RulesetID = 0,
});
sentState = false;
}
private bool sentState;
public void SendFrames(int index, int count)
{
var frames = new List<LegacyReplayFrame>();
for (int i = index; i < index + count; i++)
{
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
}
var bundle = new FrameDataBundle(new ScoreInfo(), frames);
((ISpectatorClient)this).UserSentFrames(StreamingUser.Id, bundle);
if (!sentState)
sendState(beatmapId);
}
public override void WatchUser(int userId)
{
if (!PlayingUsers.Contains(userId) && sentState)
{
// usually the server would do this.
sendState(beatmapId);
}
base.WatchUser(userId);
}
private void sendState(int beatmapId)
{
sentState = true;
((ISpectatorClient)this).UserBeganPlaying(StreamingUser.Id, new SpectatorState
{
BeatmapID = beatmapId,
RulesetID = 0,
});
}
}
internal class TestUserLookupCache : UserLookupCache
{
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User

View File

@ -11,20 +11,17 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerSpectatorLeaderboard : MultiplayerTestScene
public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
{
[Cached(typeof(SpectatorStreamingClient))]
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
@ -37,11 +34,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
private readonly Dictionary<int, ManualClock> clocks = new Dictionary<int, ManualClock>
{
{ 55, new ManualClock() },
{ 56, new ManualClock() }
{ PLAYER_1_ID, new ManualClock() },
{ PLAYER_2_ID, new ManualClock() }
};
public TestSceneMultiplayerSpectatorLeaderboard()
public TestSceneMultiSpectatorLeaderboard()
{
base.Content.AddRange(new Drawable[]
{
@ -54,7 +51,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUpSteps]
public new void SetUpSteps()
{
MultiplayerSpectatorLeaderboard leaderboard = null;
MultiSpectatorLeaderboard leaderboard = null;
AddStep("reset", () =>
{
@ -78,7 +75,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add);
});
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
@ -95,46 +92,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("send frames", () =>
{
// For user 55, send frames in sets of 1.
// For user 56, send frames in sets of 10.
// For player 1, send frames in sets of 1.
// For player 2, send frames in sets of 10.
for (int i = 0; i < 100; i++)
{
streamingClient.SendFrames(55, i, 1);
streamingClient.SendFrames(PLAYER_1_ID, i, 1);
if (i % 10 == 0)
streamingClient.SendFrames(56, i, 10);
streamingClient.SendFrames(PLAYER_2_ID, i, 10);
}
});
assertCombo(55, 1);
assertCombo(56, 10);
assertCombo(PLAYER_1_ID, 1);
assertCombo(PLAYER_2_ID, 10);
// Advance to a point where only user 55's frame changes.
// Advance to a point where only user player 1's frame changes.
setTime(500);
assertCombo(55, 5);
assertCombo(56, 10);
assertCombo(PLAYER_1_ID, 5);
assertCombo(PLAYER_2_ID, 10);
// Advance to a point where both user's frame changes.
setTime(1100);
assertCombo(55, 11);
assertCombo(56, 20);
assertCombo(PLAYER_1_ID, 11);
assertCombo(PLAYER_2_ID, 20);
// Advance user 56 only to a point where its frame changes.
setTime(56, 2100);
assertCombo(55, 11);
assertCombo(56, 30);
// Advance user player 2 only to a point where its frame changes.
setTime(PLAYER_2_ID, 2100);
assertCombo(PLAYER_1_ID, 11);
assertCombo(PLAYER_2_ID, 30);
// Advance both users beyond their last frame
setTime(101 * 100);
assertCombo(55, 100);
assertCombo(56, 100);
assertCombo(PLAYER_1_ID, 100);
assertCombo(PLAYER_2_ID, 100);
}
[Test]
public void TestNoFrames()
{
assertCombo(55, 0);
assertCombo(56, 0);
assertCombo(PLAYER_1_ID, 0);
assertCombo(PLAYER_2_ID, 0);
}
private void setTime(double time) => AddStep($"set time {time}", () =>
@ -149,71 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void assertCombo(int userId, int expectedCombo)
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
private class TestSpectatorStreamingClient : SpectatorStreamingClient
{
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
private readonly Dictionary<int, bool> userSentStateDictionary = new Dictionary<int, bool>();
public TestSpectatorStreamingClient()
: base(new DevelopmentEndpointConfiguration())
{
}
public void StartPlay(int userId, int beatmapId)
{
userBeatmapDictionary[userId] = beatmapId;
userSentStateDictionary[userId] = false;
sendState(userId, beatmapId);
}
public void EndPlay(int userId, int beatmapId)
{
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
{
BeatmapID = beatmapId,
RulesetID = 0,
});
userSentStateDictionary[userId] = false;
}
public void SendFrames(int userId, int index, int count)
{
var frames = new List<LegacyReplayFrame>();
for (int i = index; i < index + count; i++)
{
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
}
var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
((ISpectatorClient)this).UserSentFrames(userId, bundle);
if (!userSentStateDictionary[userId])
sendState(userId, userBeatmapDictionary[userId]);
}
public override void WatchUser(int userId)
{
if (userSentStateDictionary[userId])
{
// usually the server would do this.
sendState(userId, userBeatmapDictionary[userId]);
}
base.WatchUser(userId);
}
private void sendState(int userId, int beatmapId)
{
((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
{
BeatmapID = beatmapId,
RulesetID = 0,
});
userSentStateDictionary[userId] = true;
}
}
private class TestUserLookupCache : UserLookupCache
{
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)

View File

@ -0,0 +1,313 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.Spectator;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
{
[Cached(typeof(SpectatorStreamingClient))]
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestUserLookupCache();
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private BeatmapManager beatmapManager { get; set; }
private MultiSpectatorScreen spectatorScreen;
private readonly List<int> playingUserIds = new List<int>();
private readonly Dictionary<int, int> nextFrame = new Dictionary<int, int>();
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
private int importedBeatmapId;
[BackgroundDependencyLoader]
private void load()
{
importedSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result;
importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
importedBeatmapId = importedBeatmap.OnlineBeatmapID ?? -1;
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("reset sent frames", () => nextFrame.Clear());
AddStep("add streaming client", () =>
{
Remove(streamingClient);
Add(streamingClient);
});
AddStep("finish previous gameplay", () =>
{
foreach (var id in playingUserIds)
streamingClient.EndPlay(id, importedBeatmapId);
playingUserIds.Clear();
});
}
[Test]
public void TestDelayedStart()
{
AddStep("start players silently", () =>
{
Client.CurrentMatchPlayingUserIds.Add(PLAYER_1_ID);
Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID);
playingUserIds.Add(PLAYER_1_ID);
playingUserIds.Add(PLAYER_2_ID);
nextFrame[PLAYER_1_ID] = 0;
nextFrame[PLAYER_2_ID] = 0;
});
loadSpectateScreen(false);
AddWaitStep("wait a bit", 10);
AddStep("load player first_player_id", () => streamingClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1);
AddWaitStep("wait a bit", 10);
AddStep("load player second_player_id", () => streamingClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
}
[Test]
public void TestGeneral()
{
int[] userIds = Enumerable.Range(0, 4).Select(i => PLAYER_1_ID + i).ToArray();
start(userIds);
loadSpectateScreen();
sendFrames(userIds, 1000);
AddWaitStep("wait a bit", 20);
}
[Test]
public void TestPlayersMustStartSimultaneously()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();
// Send frames for one player only, both should remain paused.
sendFrames(PLAYER_1_ID, 20);
checkPausedInstant(PLAYER_1_ID, true);
checkPausedInstant(PLAYER_2_ID, true);
// Send frames for the other player, both should now start playing.
sendFrames(PLAYER_2_ID, 20);
checkPausedInstant(PLAYER_1_ID, false);
checkPausedInstant(PLAYER_2_ID, false);
}
[Test]
public void TestPlayersDoNotStartSimultaneouslyIfBufferingForMaximumStartDelay()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();
// Send frames for one player only, both should remain paused.
sendFrames(PLAYER_1_ID, 1000);
checkPausedInstant(PLAYER_1_ID, true);
checkPausedInstant(PLAYER_2_ID, true);
// Wait for the start delay seconds...
AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
// Player 1 should start playing by itself, player 2 should remain paused.
checkPausedInstant(PLAYER_1_ID, false);
checkPausedInstant(PLAYER_2_ID, true);
}
[Test]
public void TestPlayersContinueWhileOthersBuffer()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();
// Send initial frames for both players. A few more for player 1.
sendFrames(PLAYER_1_ID, 20);
sendFrames(PLAYER_2_ID, 10);
checkPausedInstant(PLAYER_1_ID, false);
checkPausedInstant(PLAYER_2_ID, false);
// Eventually player 2 will pause, player 1 must remain running.
checkPaused(PLAYER_2_ID, true);
checkPausedInstant(PLAYER_1_ID, false);
// Eventually both players will run out of frames and should pause.
checkPaused(PLAYER_1_ID, true);
checkPausedInstant(PLAYER_2_ID, true);
// Send more frames for the first player only. Player 1 should start playing with player 2 remaining paused.
sendFrames(PLAYER_1_ID, 20);
checkPausedInstant(PLAYER_2_ID, true);
checkPausedInstant(PLAYER_1_ID, false);
// Send more frames for the second player. Both should be playing
sendFrames(PLAYER_2_ID, 20);
checkPausedInstant(PLAYER_2_ID, false);
checkPausedInstant(PLAYER_1_ID, false);
}
[Test]
public void TestPlayersCatchUpAfterFallingBehind()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();
// Send initial frames for both players. A few more for player 1.
sendFrames(PLAYER_1_ID, 1000);
sendFrames(PLAYER_2_ID, 10);
checkPausedInstant(PLAYER_1_ID, false);
checkPausedInstant(PLAYER_2_ID, false);
// Eventually player 2 will run out of frames and should pause.
checkPaused(PLAYER_2_ID, true);
AddWaitStep("wait a few more frames", 10);
// Send more frames for player 2. It should unpause.
sendFrames(PLAYER_2_ID, 1000);
checkPausedInstant(PLAYER_2_ID, false);
// Player 2 should catch up to player 1 after unpausing.
waitForCatchup(PLAYER_2_ID);
AddWaitStep("wait a bit", 10);
}
[Test]
public void TestMostInSyncUserIsAudioSource()
{
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
loadSpectateScreen();
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, true);
sendFrames(PLAYER_1_ID, 10);
sendFrames(PLAYER_2_ID, 20);
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
checkPaused(PLAYER_1_ID, true);
assertMuted(PLAYER_1_ID, true);
assertMuted(PLAYER_2_ID, false);
sendFrames(PLAYER_1_ID, 100);
waitForCatchup(PLAYER_1_ID);
checkPaused(PLAYER_2_ID, true);
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
sendFrames(PLAYER_2_ID, 100);
waitForCatchup(PLAYER_2_ID);
assertMuted(PLAYER_1_ID, false);
assertMuted(PLAYER_2_ID, true);
}
private void loadSpectateScreen(bool waitForPlayerLoad = true)
{
AddStep("load screen", () =>
{
Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap);
Ruleset.Value = importedBeatmap.Ruleset;
LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUserIds.ToArray()));
});
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
}
private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
private void start(int[] userIds, int? beatmapId = null)
{
AddStep("start play", () =>
{
foreach (int id in userIds)
{
Client.CurrentMatchPlayingUserIds.Add(id);
streamingClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id);
nextFrame[id] = 0;
}
});
}
private void finish(int userId, int? beatmapId = null)
{
AddStep("end play", () =>
{
streamingClient.EndPlay(userId, beatmapId ?? importedBeatmapId);
playingUserIds.Remove(userId);
nextFrame.Remove(userId);
});
}
private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count);
private void sendFrames(int[] userIds, int count = 10)
{
AddStep("send frames", () =>
{
foreach (int id in userIds)
{
streamingClient.SendFrames(id, nextFrame[id], count);
nextFrame[id] += count;
}
});
}
private void checkPaused(int userId, bool state)
=> AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
private void checkPausedInstant(int userId, bool state)
=> AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
private void assertMuted(int userId, bool muted)
=> AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted);
private void waitForCatchup(int userId)
=> AddUntilStep($"{userId} not catching up", () => !getInstance(userId).GameplayClock.IsCatchingUp);
private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType<Player>().Single();
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
internal class TestUserLookupCache : UserLookupCache
{
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
{
return Task.FromResult(new User
{
Id = lookup,
Username = $"User {lookup}"
});
}
}
}
}

View File

@ -1,9 +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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -11,7 +27,158 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private TestMultiplayer multiplayerScreen;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private TestMultiplayerClient client => multiplayerScreen.Client;
private Room room => client.APIRoom;
public TestSceneMultiplayer()
{
loadMultiplayer();
}
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
}
[SetUp]
public void Setup() => Schedule(() =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
});
[Test]
public void TestUserSetToIdleWhenBeatmapDeleted()
{
loadMultiplayer();
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("set user ready", () => client.ChangeState(MultiplayerUserState.Ready));
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
}
[Test]
public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap()
{
loadMultiplayer();
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("join other user (ready, host)", () =>
{
client.AddUser(new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" });
client.TransferHost(MultiplayerTestScene.PLAYER_1_ID);
client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready);
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddStep("click spectate button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerSpectateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("start match externally", () => client.StartMatch());
AddAssert("play not started", () => multiplayerScreen.IsCurrentScreen());
}
[Test]
public void TestLocalPlayStartsWhileSpectatingWhenBeatmapBecomesAvailable()
{
loadMultiplayer();
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddStep("join other user (ready, host)", () =>
{
client.AddUser(new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" });
client.TransferHost(MultiplayerTestScene.PLAYER_1_ID);
client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready);
});
AddStep("click spectate button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerSpectateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("start match externally", () => client.StartMatch());
AddStep("restore beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
});
AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen());
}
private void createRoom(Func<Room> room)
{
AddStep("open room", () =>
{
multiplayerScreen.OpenNewRoom(room());
});
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
AddStep("create room", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for join", () => client.Room != null);
}
private void loadMultiplayer()
{
AddStep("show", () =>
{

View File

@ -6,14 +6,12 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
@ -22,6 +20,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Online;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -30,7 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private const int users = 16;
[Cached(typeof(SpectatorStreamingClient))]
private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(users);
private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming();
[Cached(typeof(UserLookupCache))]
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
@ -71,7 +70,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
for (int i = 0; i < users; i++)
streamingClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
Client.CurrentMatchPlayingUserIds.Clear();
Client.CurrentMatchPlayingUserIds.AddRange(streamingClient.PlayingUsers);
@ -114,30 +114,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
}
public class TestMultiplayerStreaming : SpectatorStreamingClient
public class TestMultiplayerStreaming : TestSpectatorStreamingClient
{
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
private readonly int totalUsers;
public TestMultiplayerStreaming(int totalUsers)
: base(new DevelopmentEndpointConfiguration())
{
this.totalUsers = totalUsers;
}
public void Start(int beatmapId)
{
for (int i = 0; i < totalUsers; i++)
{
((ISpectatorClient)this).UserBeganPlaying(i, new SpectatorState
{
BeatmapID = beatmapId,
RulesetID = 0,
});
}
}
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
public void RandomlyUpdateState()

View File

@ -119,8 +119,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("join other user (ready)", () =>
{
Client.AddUser(new User { Id = 55 });
Client.ChangeUserState(55, MultiplayerUserState.Ready);
Client.AddUser(new User { Id = PLAYER_1_ID });
Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready);
});
AddStep("click spectate button", () =>

View File

@ -120,9 +120,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
});
[Test]
public void TestEnabledWhenRoomOpen()
[TestCase(MultiplayerRoomState.Open)]
[TestCase(MultiplayerRoomState.WaitingForLoad)]
[TestCase(MultiplayerRoomState.Playing)]
public void TestEnabledWhenRoomOpenOrInGameplay(MultiplayerRoomState roomState)
{
AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState));
assertSpectateButtonEnablement(true);
}
@ -137,12 +140,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
}
[TestCase(MultiplayerRoomState.WaitingForLoad)]
[TestCase(MultiplayerRoomState.Playing)]
[TestCase(MultiplayerRoomState.Closed)]
public void TestDisabledDuringGameplayOrClosed(MultiplayerRoomState roomState)
public void TestDisabledWhenClosed(MultiplayerRoomState roomState)
{
AddStep($"change user to {roomState}", () => Client.ChangeRoomState(roomState));
AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState));
assertSpectateButtonEnablement(false);
}
@ -156,8 +157,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestReadyButtonEnabledWhenHostAndUsersReady()
{
AddStep("add user", () => Client.AddUser(new User { Id = 55 }));
AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready));
AddStep("add user", () => Client.AddUser(new User { Id = PLAYER_1_ID }));
AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
addClickSpectateButtonStep();
assertReadyButtonEnablement(true);
@ -168,11 +169,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("add user and transfer host", () =>
{
Client.AddUser(new User { Id = 55 });
Client.TransferHost(55);
Client.AddUser(new User { Id = PLAYER_1_ID });
Client.TransferHost(PLAYER_1_ID);
});
AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready));
AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
addClickSpectateButtonStep();
assertReadyButtonEnablement(false);

View File

@ -12,7 +12,7 @@ using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Online.Spectator;
using osu.Game.Overlays.Dashboard;
using osu.Game.Tests.Visual.Gameplay;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Online
public class TestSceneCurrentlyPlayingDisplay : OsuTestScene
{
[Cached(typeof(SpectatorStreamingClient))]
private TestSceneSpectator.TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSceneSpectator.TestSpectatorStreamingClient();
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
private CurrentlyPlayingDisplay currentlyPlaying;

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -45,14 +46,14 @@ namespace osu.Game.Tests.Visual.Online
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
args.NewItems.Cast<Mod>().ForEach(mod => selectedMods.Add(new OsuSpriteText
args.NewItems.AsNonNull().Cast<Mod>().ForEach(mod => selectedMods.Add(new OsuSpriteText
{
Text = mod.Acronym,
}));
break;
case NotifyCollectionChangedAction.Remove:
args.OldItems.Cast<Mod>().ForEach(mod =>
args.OldItems.AsNonNull().Cast<Mod>().ForEach(mod =>
{
foreach (var selected in selectedMods)
{

View File

@ -11,11 +11,13 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
using osuTK.Input;
@ -24,8 +26,6 @@ namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsRoomSubScreen : RoomTestScene
{
protected override bool UseOnlineAPI => true;
[Cached(typeof(IRoomManager))]
private readonly TestRoomManager roomManager = new TestRoomManager();
@ -41,6 +41,18 @@ namespace osu.Game.Tests.Visual.Playlists
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait();
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case CreateRoomScoreRequest createRoomScoreRequest:
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
return true;
}
return false;
};
}
[SetUpSteps]
@ -59,12 +71,16 @@ namespace osu.Game.Tests.Visual.Playlists
Room.Name.Value = "my awesome room";
Room.Host.Value = new User { Id = 2, Username = "peppy" };
Room.RecentParticipants.Add(Room.Host.Value);
Room.EndDate.Value = DateTimeOffset.Now.AddMinutes(5);
Room.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo }
});
});
AddStep("start match", () => match.ChildrenOfType<PlaylistsReadyButton>().First().Click());
AddUntilStep("player loader loaded", () => Stack.CurrentScreen is PlayerLoader);
}
[Test]

View File

@ -1,6 +1,7 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
@ -20,16 +21,20 @@ namespace osu.Game.Tests.Visual.Ranking
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
ChildrenEnumerable = new[]
{
new StarRatingDisplay(new StarDifficulty(1.23, 0)) { Anchor = Anchor.Centre, Origin = Anchor.Centre },
new StarRatingDisplay(new StarDifficulty(2.34, 0)) { Anchor = Anchor.Centre, Origin = Anchor.Centre },
new StarRatingDisplay(new StarDifficulty(3.45, 0)) { Anchor = Anchor.Centre, Origin = Anchor.Centre },
new StarRatingDisplay(new StarDifficulty(4.56, 0)) { Anchor = Anchor.Centre, Origin = Anchor.Centre },
new StarRatingDisplay(new StarDifficulty(5.67, 0)) { Anchor = Anchor.Centre, Origin = Anchor.Centre },
new StarRatingDisplay(new StarDifficulty(6.78, 0)) { Anchor = Anchor.Centre, Origin = Anchor.Centre },
new StarRatingDisplay(new StarDifficulty(10.11, 0)) { Anchor = Anchor.Centre, Origin = Anchor.Centre },
}
1.23,
2.34,
3.45,
4.56,
5.67,
6.78,
10.11,
}.Select(starRating => new StarRatingDisplay(new StarDifficulty(starRating, 0))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
})
});
}

View File

@ -264,7 +264,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void moveLogoFacade()
{
if (!(logoFacade?.Transforms).Any() && !(transferContainer?.Transforms).Any())
if (!logoFacade.Transforms.Any() && !transferContainer.Transforms.Any())
{
Random random = new Random();
trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300);

View File

@ -0,0 +1,240 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneOsuMarkdownContainer : OsuTestScene
{
private OsuMarkdownContainer markdownContainer;
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Orange);
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[]
{
new Box
{
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = markdownContainer = new OsuMarkdownContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
};
});
[Test]
public void TestEmphases()
{
AddStep("Emphases", () =>
{
markdownContainer.Text = @"_italic with underscore_
*italic with asterisk*
__bold with underscore__
**bold with asterisk**
*__italic with asterisk, bold with underscore__*
_**italic with underscore, bold with asterisk**_";
});
}
[Test]
public void TestHeading()
{
AddStep("Add Heading", () =>
{
markdownContainer.Text = @"# Header 1
## Header 2
### Header 3
#### Header 4
##### Header 5";
});
}
[Test]
public void TestLink()
{
AddStep("Add Link", () =>
{
markdownContainer.Text = "[Welcome to osu!](https://osu.ppy.sh)";
});
}
[Test]
public void TestLinkWithInlineText()
{
AddStep("Add Link with inline text", () =>
{
markdownContainer.Text = "Hey, [welcome to osu!](https://osu.ppy.sh) Please enjoy the game.";
});
}
[Test]
public void TestInlineCode()
{
AddStep("Add inline code", () =>
{
markdownContainer.Text = "This is `inline code` text";
});
}
[Test]
public void TestParagraph()
{
AddStep("Add paragraph", () =>
{
markdownContainer.Text = @"first paragraph
second paragraph
third paragraph";
});
}
[Test]
public void TestFencedCodeBlock()
{
AddStep("Add Code Block", () =>
{
markdownContainer.Text = @"```markdown
# Markdown code block
This is markdown code block.
```";
});
}
[Test]
public void TestSeparator()
{
AddStep("Add Seperator", () =>
{
markdownContainer.Text = @"Line above
---
Line below";
});
}
[Test]
public void TestQuote()
{
AddStep("Add quote", () =>
{
markdownContainer.Text =
@"> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.";
});
}
[Test]
public void TestTable()
{
AddStep("Add Table", () =>
{
markdownContainer.Text =
@"| Left Aligned | Center Aligned | Right Aligned |
| :------------------- | :--------------------: | ---------------------:|
| Long Align Left Text | Long Align Center Text | Long Align Right Text |
| Align Left | Align Center | Align Right |
| Left | Center | Right |";
});
}
[Test]
public void TestUnorderedList()
{
AddStep("Add Unordered List", () =>
{
markdownContainer.Text = @"- First item level 1
- Second item level 1
- First item level 2
- First item level 3
- Second item level 3
- Third item level 3
- First item level 4
- Second item level 4
- Third item level 4
- Second item level 2
- Third item level 2
- Third item level 1";
});
}
[Test]
public void TestOrderedList()
{
AddStep("Add Ordered List", () =>
{
markdownContainer.Text = @"1. First item level 1
2. Second item level 1
1. First item level 2
1. First item level 3
2. Second item level 3
3. Third item level 3
1. First item level 4
2. Second item level 4
3. Third item level 4
2. Second item level 2
3. Third item level 2
3. Third item level 1";
});
}
[Test]
public void TestLongMixedList()
{
AddStep("Add long mixed list", () =>
{
markdownContainer.Text = @"1. The osu! World Cup is a country-based team tournament played on the osu! game mode.
- While this competition is planned as a 4 versus 4 setup, this may change depending on the number of incoming registrations.
2. Beatmap scoring is based on Score V2.
3. The beatmaps for each round will be announced by the map selectors in advance on the Sunday before the actual matches take place. Only these beatmaps will be used during the respective matches.
- One beatmap will be a tiebreaker beatmap. This beatmap will only be played in case of a tie. **The only exception to this is the Qualifiers pool.**
4. The match schedule will be settled by the Tournament Management (see the [scheduling instructions](#scheduling-instructions)).
5. If no staff or referee is available, the match will be postponed.
6. Use of the Visual Settings to alter background dim or disable beatmap elements like storyboards and skins are allowed.
7. If the beatmap ends in a draw, the map will be nullified and replayed.
8. If a player disconnects, their scores will not be counted towards their team's total.
- Disconnects within 30 seconds or 25% of the beatmap length (whichever happens first) after beatmap begin can be aborted and/or rematched. This is up to the referee's discretion.
9. Beatmaps cannot be reused in the same match unless the map was nullified.
10. If less than the minimum required players attend, the maximum time the match can be postponed is 10 minutes.
11. Exchanging players during a match is allowed without limitations.
- **If a map rematch is required, exchanging players is not allowed. With the referee's discretion, an exception can be made if the previous roster is unavailable to play.**
12. Lag is not a valid reason to nullify a beatmap.
13. All players are supposed to keep the match running fluently and without delays. Penalties can be issued to the players if they cause excessive match delays.
14. If a player disconnects between maps and the team cannot provide a replacement, the match can be delayed 10 minutes at maximum.
15. All players and referees must be treated with respect. Instructions of the referees and tournament Management are to be followed. Decisions labeled as final are not to be objected.
16. Disrupting the match by foul play, insulting and provoking other players or referees, delaying the match or other deliberate inappropriate misbehavior is strictly prohibited.
17. The multiplayer chatrooms are subject to the [osu! community rules](/wiki/Rules).
- Breaking the chat rules will result in a silence. Silenced players can not participate in multiplayer matches and must be exchanged for the time being.
18. **The seeding method will be revealed after all the teams have played their Qualifier rounds.**
19. Unexpected incidents are handled by the tournament management. Referees may allow higher tolerance depending on the circumstances. This is up to their discretion.
20. Penalties for violating the tournament rules may include:
- Exclusion of specific players for one beatmap
- Exclusion of specific players for an entire match
- Declaring the match as Lost by Default
- Disqualification from the entire tournament
- Disqualification from the current and future official tournaments until appealed
- Any modification of these rules will be announced.";
});
}
}
}

View File

@ -7,6 +7,7 @@ using System.Linq;
using JetBrains.Annotations;
using Microsoft.Win32;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
@ -77,8 +78,8 @@ namespace osu.Game.Tournament.IPC
using (var stream = IPCStorage.GetStream(file_ipc_filename))
using (var sr = new StreamReader(stream))
{
var beatmapId = int.Parse(sr.ReadLine());
var mods = int.Parse(sr.ReadLine());
var beatmapId = int.Parse(sr.ReadLine().AsNonNull());
var mods = int.Parse(sr.ReadLine().AsNonNull());
if (lastBeatmapId != beatmapId)
{
@ -124,7 +125,7 @@ namespace osu.Game.Tournament.IPC
using (var stream = IPCStorage.GetStream(file_ipc_state_filename))
using (var sr = new StreamReader(stream))
{
State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine());
State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine().AsNonNull());
}
}
catch (Exception)

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading;
@ -83,6 +82,12 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
if (beatmap.Metadata != null)
beatmap.Metadata.AuthorID = res.AuthorID;
if (beatmap.BeatmapSet.Metadata != null)
beatmap.BeatmapSet.Metadata.AuthorID = res.AuthorID;
LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
}
}
@ -157,7 +162,7 @@ namespace osu.Game.Beatmaps
using (var cmd = db.CreateCommand())
{
cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path";
cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path";
cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash));
cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value));
@ -174,6 +179,12 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0);
beatmap.OnlineBeatmapID = reader.GetInt32(1);
if (beatmap.Metadata != null)
beatmap.Metadata.AuthorID = reader.GetInt32(3);
if (beatmap.BeatmapSet.Metadata != null)
beatmap.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3);
LogForModel(set, $"Cached local retrieval for {beatmap}.");
return true;
}
@ -194,17 +205,6 @@ namespace osu.Game.Beatmaps
cacheDownloadRequest?.Dispose();
updateScheduler?.Dispose();
}
[Serializable]
[SuppressMessage("ReSharper", "InconsistentNaming")]
private class CachedOnlineBeatmapLookup
{
public int approved { get; set; }
public int? beatmapset_id { get; set; }
public int? beatmap_id { get; set; }
}
}
}
}

View File

@ -34,6 +34,21 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
public List<BeatmapSetInfo> BeatmapSets { get; set; }
/// <summary>
/// Helper property to deserialize a username to <see cref="User"/>.
/// </summary>
[JsonProperty(@"user_id")]
[Column("AuthorID")]
public int AuthorID
{
get => Author?.Id ?? 1;
set
{
Author ??= new User();
Author.Id = value;
}
}
/// <summary>
/// Helper property to deserialize a username to <see cref="User"/>.
/// </summary>
@ -42,7 +57,11 @@ namespace osu.Game.Beatmaps
public string AuthorString
{
get => Author?.Username;
set => Author = new User { Username = value };
set
{
Author ??= new User();
Author.Username = value;
}
}
/// <summary>

View File

@ -324,7 +324,7 @@ namespace osu.Game.Beatmaps
public bool SkinLoaded => skin.IsResultAvailable;
public ISkin Skin => skin.Value;
protected virtual ISkin GetSkin() => new DefaultSkin();
protected virtual ISkin GetSkin() => new DefaultSkin(null);
private readonly RecyclableLazy<ISkin> skin;
public abstract Stream GetStream(string storagePath);

View File

@ -472,7 +472,7 @@ namespace osu.Game.Database
}
/// <summary>
/// Delete new file.
/// Delete an existing file.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="file">The existing file to be deleted.</param>

View File

@ -3,8 +3,10 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Threading;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Extensions
@ -43,5 +45,23 @@ namespace osu.Game.Extensions
/// <returns>The delta vector in Parent's coordinates.</returns>
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
public static SkinnableInfo CreateSkinnableInfo(this Drawable component) => new SkinnableInfo(component);
public static void ApplySkinnableInfo(this Drawable component, SkinnableInfo info)
{
// todo: can probably make this better via deserialisation directly using a common interface.
component.Position = info.Position;
component.Rotation = info.Rotation;
component.Scale = info.Scale;
component.Anchor = info.Anchor;
component.Origin = info.Origin;
if (component is Container container)
{
foreach (var child in info.Children)
container.Add(child.CreateInstance());
}
}
}
}

View File

@ -6,6 +6,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Extensions.ObjectExtensions;
namespace osu.Game.Extensions
{
@ -50,7 +51,7 @@ namespace osu.Game.Extensions
}
else if (continuationTask.IsFaulted)
{
tcs.TrySetException(continuationTask.Exception);
tcs.TrySetException(continuationTask.Exception.AsNonNull());
}
else
{

View File

@ -0,0 +1,31 @@
// 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;
namespace osu.Game.Extensions
{
internal static class TypeExtensions
{
/// <summary>
/// Returns <paramref name="type"/>'s <see cref="Type.AssemblyQualifiedName"/>
/// with the assembly version, culture and public key token values removed.
/// </summary>
/// <remarks>
/// This method is usually used in extensibility scenarios (i.e. for custom rulesets or skins)
/// when a version-agnostic identifier associated with a C# class - potentially originating from
/// an external assembly - is needed.
/// Leaving only the type and assembly names in such a scenario allows to preserve compatibility
/// across assembly versions.
/// </remarks>
internal static string GetInvariantInstantiationInfo(this Type type)
{
string assemblyQualifiedName = type.AssemblyQualifiedName;
if (assemblyQualifiedName == null)
throw new ArgumentException($"{type}'s assembly-qualified name is null. Ensure that it is a concrete type and not a generic type parameter.", nameof(type));
return string.Join(',', assemblyQualifiedName.Split(',').Take(2));
}
}
}

View File

@ -0,0 +1,82 @@
// 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 Markdig;
using Markdig.Extensions.AutoIdentifiers;
using Markdig.Extensions.Tables;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownContainer : MarkdownContainer
{
public OsuMarkdownContainer()
{
LineSpacing = 21;
}
protected override void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level)
{
switch (markdownObject)
{
case YamlFrontMatterBlock _:
// Don't parse YAML Frontmatter
break;
case ListItemBlock listItemBlock:
var isOrdered = ((ListBlock)listItemBlock.Parent).IsOrdered;
var childContainer = CreateListItem(listItemBlock, level, isOrdered);
container.Add(childContainer);
foreach (var single in listItemBlock)
base.AddMarkdownComponent(single, childContainer.Content, level);
break;
default:
base.AddMarkdownComponent(markdownObject, container, level);
break;
}
}
public override SpriteText CreateSpriteText() => new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14),
};
public override MarkdownTextFlowContainer CreateTextFlow() => new OsuMarkdownTextFlowContainer();
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new OsuMarkdownHeading(headingBlock);
protected override MarkdownFencedCodeBlock CreateFencedCodeBlock(FencedCodeBlock fencedCodeBlock) => new OsuMarkdownFencedCodeBlock(fencedCodeBlock);
protected override MarkdownSeparator CreateSeparator(ThematicBreakBlock thematicBlock) => new OsuMarkdownSeparator();
protected override MarkdownQuoteBlock CreateQuoteBlock(QuoteBlock quoteBlock) => new OsuMarkdownQuoteBlock(quoteBlock);
protected override MarkdownTable CreateTable(Table table) => new OsuMarkdownTable(table);
protected override MarkdownList CreateList(ListBlock listBlock) => new MarkdownList
{
Padding = new MarginPadding(0)
};
protected virtual OsuMarkdownListItem CreateListItem(ListItemBlock listItemBlock, int level, bool isOrdered)
{
if (isOrdered)
return new OsuMarkdownOrderedListItem(listItemBlock.Order);
return new OsuMarkdownUnorderedListItem(level);
}
protected override MarkdownPipeline CreateBuilder()
=> new MarkdownPipelineBuilder().UseAutoIdentifiers(AutoIdentifierOptions.GitHub)
.UseEmojiAndSmiley()
.UseYamlFrontMatter()
.UseAdvancedExtensions().Build();
}
}

View File

@ -0,0 +1,45 @@
// 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 Markdig.Syntax;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownFencedCodeBlock : MarkdownFencedCodeBlock
{
// TODO : change to monospace font for this component
public OsuMarkdownFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
: base(fencedCodeBlock)
{
}
protected override Drawable CreateBackground() => new CodeBlockBackground();
public override MarkdownTextFlowContainer CreateTextFlow() => new CodeBlockTextFlowContainer();
private class CodeBlockBackground : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
Colour = colourProvider.Background6;
}
}
private class CodeBlockTextFlowContainer : OsuMarkdownTextFlowContainer
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Light1;
Margin = new MarginPadding(10);
}
}
}
}

View File

@ -0,0 +1,75 @@
// 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 Markdig.Syntax;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownHeading : MarkdownHeading
{
private readonly int level;
public OsuMarkdownHeading(HeadingBlock headingBlock)
: base(headingBlock)
{
level = headingBlock.Level;
}
public override MarkdownTextFlowContainer CreateTextFlow() => new HeadingTextFlowContainer
{
Weight = GetFontWeightByLevel(level),
};
protected override float GetFontSizeByLevel(int level)
{
// Reference for this font size
// https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/bem/osu-md.less#L9
// https://github.com/ppy/osu-web/blob/376cac43a051b9c85ce95e2c446099be187b3e45/resources/assets/less/variables.less#L161
const float base_font_size = 14;
switch (level)
{
case 1:
return 30 / base_font_size;
case 2:
return 26 / base_font_size;
case 3:
return 20 / base_font_size;
case 4:
return 18 / base_font_size;
case 5:
return 16 / base_font_size;
default:
return 1;
}
}
protected virtual FontWeight GetFontWeightByLevel(int level)
{
switch (level)
{
case 1:
case 2:
return FontWeight.SemiBold;
default:
return FontWeight.Bold;
}
}
private class HeadingTextFlowContainer : OsuMarkdownTextFlowContainer
{
public FontWeight Weight { get; set; }
protected override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(weight: Weight));
}
}
}

View File

@ -0,0 +1,48 @@
// 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 Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownLinkText : MarkdownLinkText
{
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
private SpriteText spriteText;
public OsuMarkdownLinkText(string text, LinkInline linkInline)
: base(text, linkInline)
{
}
[BackgroundDependencyLoader]
private void load()
{
spriteText.Colour = colourProvider.Light2;
}
public override SpriteText CreateSpriteText()
{
return spriteText = base.CreateSpriteText();
}
protected override bool OnHover(HoverEvent e)
{
spriteText.Colour = colourProvider.Light1;
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
spriteText.Colour = colourProvider.Light2;
base.OnHoverLost(e);
}
}
}

View File

@ -0,0 +1,44 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osuTK;
namespace osu.Game.Graphics.Containers.Markdown
{
public abstract class OsuMarkdownListItem : CompositeDrawable
{
[Resolved]
private IMarkdownTextComponent parentTextComponent { get; set; }
public FillFlowContainer Content { get; private set; }
protected OsuMarkdownListItem()
{
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
CreateMarker(),
Content = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10, 10),
}
};
}
protected virtual SpriteText CreateMarker() => parentTextComponent.CreateSpriteText();
}
}

View File

@ -0,0 +1,27 @@
// 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.Graphics;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownOrderedListItem : OsuMarkdownListItem
{
private const float left_padding = 30;
private readonly int order;
public OsuMarkdownOrderedListItem(int order)
{
this.order = order;
Padding = new MarginPadding { Left = left_padding };
}
protected override SpriteText CreateMarker() => base.CreateMarker().With(t =>
{
t.X = -left_padding;
t.Text = $"{order}.";
});
}
}

View File

@ -0,0 +1,44 @@
// 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 Markdig.Syntax;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownQuoteBlock : MarkdownQuoteBlock
{
public OsuMarkdownQuoteBlock(QuoteBlock quoteBlock)
: base(quoteBlock)
{
}
protected override Drawable CreateBackground() => new QuoteBackground();
public override MarkdownTextFlowContainer CreateTextFlow()
{
return base.CreateTextFlow().With(f => f.Margin = new MarginPadding
{
Vertical = 10,
Horizontal = 20,
});
}
private class QuoteBackground : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Anchor = Anchor.CentreLeft;
Origin = Anchor.CentreLeft;
RelativeSizeAxes = Axes.Y;
Width = 2;
Colour = colourProvider.Content2;
}
}
}
}

View File

@ -0,0 +1,27 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownSeparator : MarkdownSeparator
{
protected override Drawable CreateSeparator() => new Separator();
private class Separator : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
Height = 1;
Colour = colourProvider.Background3;
}
}
}
}

View File

@ -0,0 +1,18 @@
// 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 Markdig.Extensions.Tables;
using osu.Framework.Graphics.Containers.Markdown;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownTable : MarkdownTable
{
public OsuMarkdownTable(Table table)
: base(table)
{
}
protected override MarkdownTableCell CreateTableCell(TableCell cell, TableColumnDefinition definition, bool isHeading) => new OsuMarkdownTableCell(cell, definition, isHeading);
}
}

View File

@ -0,0 +1,77 @@
// 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 Markdig.Extensions.Tables;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownTableCell : MarkdownTableCell
{
private readonly bool isHeading;
public OsuMarkdownTableCell(TableCell cell, TableColumnDefinition definition, bool isHeading)
: base(cell, definition)
{
this.isHeading = isHeading;
Masking = false;
BorderThickness = 0;
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(CreateBorder(isHeading));
}
public override MarkdownTextFlowContainer CreateTextFlow() => new TableCellTextFlowContainer
{
Weight = isHeading ? FontWeight.Bold : FontWeight.Regular,
Padding = new MarginPadding(10),
};
protected virtual Box CreateBorder(bool isHeading)
{
if (isHeading)
return new TableHeadBorder();
return new TableBodyBorder();
}
private class TableHeadBorder : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background3;
RelativeSizeAxes = Axes.X;
Height = 2;
Anchor = Anchor.BottomLeft;
Origin = Anchor.BottomLeft;
}
}
private class TableBodyBorder : Box
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Colour = colourProvider.Background4;
RelativeSizeAxes = Axes.X;
Height = 1;
}
}
private class TableCellTextFlowContainer : OsuMarkdownTextFlowContainer
{
public FontWeight Weight { get; set; }
protected override SpriteText CreateSpriteText() => base.CreateSpriteText().With(t => t.Font = t.Font.With(weight: Weight));
}
}
}

View File

@ -0,0 +1,28 @@
// 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 Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownTextFlowContainer : MarkdownTextFlowContainer
{
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
protected override void AddLinkText(string text, LinkInline linkInline)
=> AddDrawable(new OsuMarkdownLinkText(text, linkInline));
// TODO : Add background (colour B6) and change font to monospace
protected override void AddCodeInLine(CodeInline codeInline)
=> AddText(codeInline.Content, t => { t.Colour = colourProvider.Light1; });
protected override SpriteText CreateEmphasisedSpriteText(bool bold, bool italic)
=> CreateSpriteText().With(t => t.Font = t.Font.With(weight: bold ? FontWeight.Bold : FontWeight.Regular, italics: italic));
}
}

View File

@ -0,0 +1,51 @@
// 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.Graphics;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Containers.Markdown
{
public class OsuMarkdownUnorderedListItem : OsuMarkdownListItem
{
private const float left_padding = 20;
private readonly int level;
public OsuMarkdownUnorderedListItem(int level)
{
this.level = level;
Padding = new MarginPadding { Left = left_padding };
}
protected override SpriteText CreateMarker() => base.CreateMarker().With(t =>
{
t.Text = GetTextMarker(level);
t.Font = t.Font.With(size: t.Font.Size / 2);
t.Origin = Anchor.Centre;
t.X = -left_padding / 2;
t.Y = t.Font.Size;
});
/// <summary>
/// Get text marker based on <paramref name="level"/>
/// </summary>
/// <param name="level">The markdown level of current list item.</param>
/// <returns>The marker string of this list item</returns>
protected virtual string GetTextMarker(int level)
{
switch (level)
{
case 1:
return "●";
case 2:
return "○";
default:
return "■";
}
}
}
}

View File

@ -0,0 +1,18 @@
// 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.Allocation;
namespace osu.Game.Graphics.UserInterface
{
public class DangerousTriangleButton : TriangleButton
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.PinkDark;
Triangles.ColourDark = colours.PinkDarker;
Triangles.ColourLight = colours.Pink;
}
}
}

View File

@ -38,6 +38,7 @@ namespace osu.Game.IO.Legacy
/// <summary> Reads a string from the buffer. Overrides the base implementation so it can cope with nulls. </summary>
public override string ReadString()
{
// ReSharper disable once AssignNullToNotNullAttribute
if (ReadByte() == 0) return null;
return base.ReadString();

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using osu.Framework.Extensions.ObjectExtensions;
namespace osu.Game.IO.Serialization.Converters
{
@ -60,7 +61,7 @@ namespace osu.Game.IO.Serialization.Converters
throw new JsonException("Expected $type token.");
var typeName = lookupTable[(int)tok["$type"]];
var instance = (T)Activator.CreateInstance(Type.GetType(typeName));
var instance = (T)Activator.CreateInstance(Type.GetType(typeName).AsNonNull());
serializer.Populate(itemReader, instance);
list.Add(instance);

View File

@ -48,7 +48,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.ToggleSkinEditor),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
new KeyBinding(InputKey.Escape, GlobalAction.Back),
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),

View File

@ -6,13 +6,14 @@ using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Input.Bindings;
namespace osu.Game.Input
{
/// <summary>
/// Track whether the end-user is in an idle state, based on their last interaction with the game.
/// </summary>
public class IdleTracker : Component, IKeyBindingHandler<PlatformAction>, IHandleGlobalKeyboardInput
public class IdleTracker : Component, IKeyBindingHandler<PlatformAction>, IKeyBindingHandler<GlobalAction>, IHandleGlobalKeyboardInput
{
private readonly double timeToIdle;
@ -58,6 +59,10 @@ namespace osu.Game.Input
public void OnReleased(PlatformAction action) => updateLastInteractionTime();
public bool OnPressed(GlobalAction action) => updateLastInteractionTime();
public void OnReleased(GlobalAction action) => updateLastInteractionTime();
protected override bool Handle(UIEvent e)
{
switch (e)

View File

@ -0,0 +1,508 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using osu.Game.Database;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20210511060743_AddSkinInstantiationInfo")]
partial class AddSkinInstantiationInfo
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<float>("ApproachRate");
b.Property<float>("CircleSize");
b.Property<float>("DrainRate");
b.Property<float>("OverallDifficulty");
b.Property<double>("SliderMultiplier");
b.Property<double>("SliderTickRate");
b.HasKey("ID");
b.ToTable("BeatmapDifficulty");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("AudioLeadIn");
b.Property<double>("BPM");
b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor");
b.Property<int>("BeatmapSetInfoID");
b.Property<bool>("Countdown");
b.Property<double>("DistanceSpacing");
b.Property<bool>("EpilepsyWarning");
b.Property<int>("GridSize");
b.Property<string>("Hash");
b.Property<bool>("Hidden");
b.Property<double>("Length");
b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapID");
b.Property<string>("Path");
b.Property<int>("RulesetID");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");
b.Property<double>("StarDifficulty");
b.Property<int>("Status");
b.Property<string>("StoredBookmarks");
b.Property<double>("TimelineZoom");
b.Property<string>("Version");
b.Property<bool>("WidescreenStoryboard");
b.HasKey("ID");
b.HasIndex("BaseDifficultyID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("Hash");
b.HasIndex("MD5Hash");
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("BeatmapInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Artist");
b.Property<string>("ArtistUnicode");
b.Property<string>("AudioFile");
b.Property<string>("AuthorString")
.HasColumnName("Author");
b.Property<string>("BackgroundFile");
b.Property<int>("PreviewTime");
b.Property<string>("Source");
b.Property<string>("Tags");
b.Property<string>("Title");
b.Property<string>("TitleUnicode");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.HasKey("ID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("FileInfoID");
b.ToTable("BeatmapSetFileInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset>("DateAdded");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapSetID");
b.Property<bool>("Protected");
b.Property<int>("Status");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapSetID")
.IsUnique();
b.ToTable("BeatmapSetInfo");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Key")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<int?>("SkinInfoID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("SkinInfoID");
b.HasIndex("RulesetID", "Variant");
b.ToTable("Settings");
});
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Hash");
b.Property<int>("ReferenceCount");
b.HasKey("ID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("ReferenceCount");
b.ToTable("FileInfo");
});
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntAction")
.HasColumnName("Action");
b.Property<string>("KeysString")
.HasColumnName("Keys");
b.Property<int?>("RulesetID");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("IntAction");
b.HasIndex("RulesetID", "Variant");
b.ToTable("KeyBinding");
});
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("Available");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.Property<string>("ShortName");
b.HasKey("ID");
b.HasIndex("Available");
b.HasIndex("ShortName")
.IsUnique();
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int?>("ScoreInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("ScoreInfoID");
b.ToTable("ScoreFileInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("Accuracy")
.HasColumnType("DECIMAL(1,4)");
b.Property<int>("BeatmapInfoID");
b.Property<int>("Combo");
b.Property<DateTimeOffset>("Date");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int>("MaxCombo");
b.Property<string>("ModsJson")
.HasColumnName("Mods");
b.Property<long?>("OnlineScoreID");
b.Property<double?>("PP");
b.Property<int>("Rank");
b.Property<int>("RulesetID");
b.Property<string>("StatisticsJson")
.HasColumnName("Statistics");
b.Property<long>("TotalScore");
b.Property<int?>("UserID")
.HasColumnName("UserID");
b.Property<string>("UserString")
.HasColumnName("User");
b.HasKey("ID");
b.HasIndex("BeatmapInfoID");
b.HasIndex("OnlineScoreID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("ScoreInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
.WithMany()
.HasForeignKey("BaseDifficultyID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
.WithMany("Beatmaps")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("Beatmaps")
.HasForeignKey("MetadataID");
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
.WithMany("Files")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Settings")
.HasForeignKey("SkinInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Scoring.ScoreInfo")
.WithMany("Files")
.HasForeignKey("ScoreInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
.WithMany("Scores")
.HasForeignKey("BeatmapInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
{
public partial class AddSkinInstantiationInfo : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "InstantiationInfo",
table: "SkinInfo",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "InstantiationInfo",
table: "SkinInfo");
}
}
}

View File

@ -0,0 +1,511 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using osu.Game.Database;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20210514062639_AddAuthorIdToBeatmapMetadata")]
partial class AddAuthorIdToBeatmapMetadata
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<float>("ApproachRate");
b.Property<float>("CircleSize");
b.Property<float>("DrainRate");
b.Property<float>("OverallDifficulty");
b.Property<double>("SliderMultiplier");
b.Property<double>("SliderTickRate");
b.HasKey("ID");
b.ToTable("BeatmapDifficulty");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("AudioLeadIn");
b.Property<double>("BPM");
b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor");
b.Property<int>("BeatmapSetInfoID");
b.Property<bool>("Countdown");
b.Property<double>("DistanceSpacing");
b.Property<bool>("EpilepsyWarning");
b.Property<int>("GridSize");
b.Property<string>("Hash");
b.Property<bool>("Hidden");
b.Property<double>("Length");
b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapID");
b.Property<string>("Path");
b.Property<int>("RulesetID");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");
b.Property<double>("StarDifficulty");
b.Property<int>("Status");
b.Property<string>("StoredBookmarks");
b.Property<double>("TimelineZoom");
b.Property<string>("Version");
b.Property<bool>("WidescreenStoryboard");
b.HasKey("ID");
b.HasIndex("BaseDifficultyID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("Hash");
b.HasIndex("MD5Hash");
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("BeatmapInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Artist");
b.Property<string>("ArtistUnicode");
b.Property<string>("AudioFile");
b.Property<int>("AuthorID")
.HasColumnName("AuthorID");
b.Property<string>("AuthorString")
.HasColumnName("Author");
b.Property<string>("BackgroundFile");
b.Property<int>("PreviewTime");
b.Property<string>("Source");
b.Property<string>("Tags");
b.Property<string>("Title");
b.Property<string>("TitleUnicode");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.HasKey("ID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("FileInfoID");
b.ToTable("BeatmapSetFileInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset>("DateAdded");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapSetID");
b.Property<bool>("Protected");
b.Property<int>("Status");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapSetID")
.IsUnique();
b.ToTable("BeatmapSetInfo");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Key")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<int?>("SkinInfoID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("SkinInfoID");
b.HasIndex("RulesetID", "Variant");
b.ToTable("Settings");
});
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Hash");
b.Property<int>("ReferenceCount");
b.HasKey("ID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("ReferenceCount");
b.ToTable("FileInfo");
});
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntAction")
.HasColumnName("Action");
b.Property<string>("KeysString")
.HasColumnName("Keys");
b.Property<int?>("RulesetID");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("IntAction");
b.HasIndex("RulesetID", "Variant");
b.ToTable("KeyBinding");
});
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("Available");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.Property<string>("ShortName");
b.HasKey("ID");
b.HasIndex("Available");
b.HasIndex("ShortName")
.IsUnique();
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int?>("ScoreInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("ScoreInfoID");
b.ToTable("ScoreFileInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("Accuracy")
.HasColumnType("DECIMAL(1,4)");
b.Property<int>("BeatmapInfoID");
b.Property<int>("Combo");
b.Property<DateTimeOffset>("Date");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int>("MaxCombo");
b.Property<string>("ModsJson")
.HasColumnName("Mods");
b.Property<long?>("OnlineScoreID");
b.Property<double?>("PP");
b.Property<int>("Rank");
b.Property<int>("RulesetID");
b.Property<string>("StatisticsJson")
.HasColumnName("Statistics");
b.Property<long>("TotalScore");
b.Property<int?>("UserID")
.HasColumnName("UserID");
b.Property<string>("UserString")
.HasColumnName("User");
b.HasKey("ID");
b.HasIndex("BeatmapInfoID");
b.HasIndex("OnlineScoreID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("ScoreInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
.WithMany()
.HasForeignKey("BaseDifficultyID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
.WithMany("Beatmaps")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("Beatmaps")
.HasForeignKey("MetadataID");
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
.WithMany("Files")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Settings")
.HasForeignKey("SkinInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Scoring.ScoreInfo")
.WithMany("Files")
.HasForeignKey("ScoreInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
.WithMany("Scores")
.HasForeignKey("BeatmapInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
{
public partial class AddAuthorIdToBeatmapMetadata : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "AuthorID",
table: "BeatmapMetadata",
nullable: false,
defaultValue: 0);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AuthorID",
table: "BeatmapMetadata");
}
}
}

View File

@ -126,6 +126,9 @@ namespace osu.Game.Migrations
b.Property<string>("AudioFile");
b.Property<int>("AuthorID")
.HasColumnName("AuthorID");
b.Property<string>("AuthorString")
.HasColumnName("Author");
@ -141,8 +144,6 @@ namespace osu.Game.Migrations
b.Property<string>("TitleUnicode");
b.Property<string>("VideoFile");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
@ -352,7 +353,7 @@ namespace osu.Game.Migrations
b.Property<long>("TotalScore");
b.Property<long?>("UserID")
b.Property<int?>("UserID")
.HasColumnName("UserID");
b.Property<string>("UserString")
@ -402,6 +403,8 @@ namespace osu.Game.Migrations
b.Property<string>("Hash");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.HasKey("ID");

View File

@ -270,7 +270,7 @@ namespace osu.Game.Online.API
{
try
{
return JObject.Parse(req.GetResponseString()).SelectToken("form_error", true).AsNonNull().ToObject<RegistrationRequest.RegistrationRequestErrors>();
return JObject.Parse(req.GetResponseString().AsNonNull()).SelectToken("form_error", true).AsNonNull().ToObject<RegistrationRequest.RegistrationRequestErrors>();
}
catch
{

View File

@ -96,9 +96,6 @@ namespace osu.Game.Online.Multiplayer
if (!IsConnected.Value)
return Task.CompletedTask;
if (newState == MultiplayerUserState.Spectating)
return Task.CompletedTask; // Not supported yet.
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
}

View File

@ -24,7 +24,13 @@ namespace osu.Game.Online.Spectator
[Key(2)]
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods) && RulesetID == other?.RulesetID;
public bool Equals(SpectatorState other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID;
}
public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}";
}

View File

@ -142,7 +142,11 @@ namespace osu.Game.Online.Spectator
if (!playingUsers.Contains(userId))
playingUsers.Add(userId);
playingUserStates[userId] = state;
// UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched.
// This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29).
// We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations.
if (watchingUsers.Contains(userId))
playingUserStates[userId] = state;
}
OnUserBeganPlaying?.Invoke(userId, state);
@ -230,7 +234,7 @@ namespace osu.Game.Online.Spectator
connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId);
}
public void StopWatchingUser(int userId)
public virtual void StopWatchingUser(int userId)
{
lock (userLock)
{

View File

@ -82,6 +82,21 @@ namespace osu.Game
private SkinEditorOverlay skinEditor;
private Container overlayContent;
private Container rightFloatingOverlayContent;
private Container leftFloatingOverlayContent;
private Container topMostOverlayContent;
private ScalingContainer screenContainer;
private Container screenOffsetContainer;
[Resolved]
private FrameworkConfigManager frameworkConfig { get; set; }
[Cached]
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender();
@ -597,28 +612,35 @@ namespace osu.Game
ActionRequested = action => volume.Adjust(action),
ScrollActionRequested = (action, amount, isPrecise) => volume.Adjust(action, amount, isPrecise),
},
screenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays)
screenOffsetContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
receptor = new BackButton.Receptor(),
ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
BackButton = new BackButton(receptor)
screenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = () =>
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
var currentScreen = ScreenStack.CurrentScreen as IOsuScreen;
receptor = new BackButton.Receptor(),
ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both },
BackButton = new BackButton(receptor)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Action = () =>
{
var currentScreen = ScreenStack.CurrentScreen as IOsuScreen;
if (currentScreen?.AllowBackButton == true && !currentScreen.OnBackButton())
ScreenStack.Exit();
if (currentScreen?.AllowBackButton == true && !currentScreen.OnBackButton())
ScreenStack.Exit();
}
},
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
}
},
logoContainer = new Container { RelativeSizeAxes = Axes.Both },
}
},
overlayContent = new Container { RelativeSizeAxes = Axes.Both },
@ -768,7 +790,7 @@ namespace osu.Game
if (notifications.State.Value == Visibility.Visible)
offset -= Toolbar.HEIGHT / 2;
screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint);
screenOffsetContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint);
}
Settings.State.ValueChanged += _ => updateScreenOffset();
@ -935,19 +957,6 @@ namespace osu.Game
{
}
private Container overlayContent;
private Container rightFloatingOverlayContent;
private Container leftFloatingOverlayContent;
private Container topMostOverlayContent;
[Resolved]
private FrameworkConfigManager frameworkConfig { get; set; }
private ScalingContainer screenContainer;
protected override bool OnExiting()
{
if (ScreenStack.CurrentScreen is Loader)
@ -966,7 +975,7 @@ namespace osu.Game
{
base.UpdateAfterChildren();
screenContainer.Padding = new MarginPadding { Top = ToolbarOffset };
screenOffsetContainer.Padding = new MarginPadding { Top = ToolbarOffset };
overlayContent.Padding = new MarginPadding { Top = ToolbarOffset };
MenuCursorContainer.CanShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false;

View File

@ -339,22 +339,13 @@ namespace osu.Game.Overlays.KeyBinding
}
}
public class ClearButton : TriangleButton
public class ClearButton : DangerousTriangleButton
{
public ClearButton()
{
Text = "Clear";
Size = new Vector2(80, 20);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.Pink;
Triangles.ColourDark = colours.PinkDark;
Triangles.ColourLight = colours.PinkLight;
}
}
public class KeyButton : Container

View File

@ -11,7 +11,6 @@ using osu.Game.Input;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
using osuTK;
using osu.Game.Graphics;
namespace osu.Game.Overlays.KeyBinding
{
@ -55,10 +54,10 @@ namespace osu.Game.Overlays.KeyBinding
}
}
public class ResetButton : TriangleButton
public class ResetButton : DangerousTriangleButton
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
Text = "Reset all bindings in section";
RelativeSizeAxes = Axes.X;
@ -66,10 +65,6 @@ namespace osu.Game.Overlays.KeyBinding
Height = 20;
Content.CornerRadius = 5;
BackgroundColour = colours.PinkDark;
Triangles.ColourDark = colours.PinkDarker;
Triangles.ColourLight = colours.Pink;
}
}
}

View File

@ -69,13 +69,15 @@ namespace osu.Game.Overlays.Toolbar
base.LoadComplete();
Current.BindDisabledChanged(disabled => this.FadeColour(disabled ? Color4.Gray : Color4.White, 300), true);
Current.BindValueChanged(_ => moveLineToCurrent(), true);
Current.BindValueChanged(_ => moveLineToCurrent());
// Scheduled to allow the button flow layout to be computed before the line position is updated
ScheduleAfterChildren(moveLineToCurrent);
}
private bool hasInitialPosition;
// Scheduled to allow the flow layout to be computed before the line position is updated
private void moveLineToCurrent() => ScheduleAfterChildren(() =>
private void moveLineToCurrent()
{
if (SelectedTab != null)
{
@ -86,7 +88,7 @@ namespace osu.Game.Overlays.Toolbar
hasInitialPosition = true;
}
});
}
public override bool HandleNonPositionalInput => !Current.Disabled && base.HandleNonPositionalInput;

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Edit.Checks.Components;
@ -29,9 +28,9 @@ namespace osu.Game.Rulesets.Edit
new CheckConcurrentObjects()
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
return checks.SelectMany(check => check.Run(playableBeatmap, workingBeatmap));
return checks.SelectMany(check => check.Run(context));
}
}
}

View File

@ -0,0 +1,38 @@
// 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.Beatmaps;
#nullable enable
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// Represents the context provided by the beatmap verifier to the checks it runs.
/// Contains information about what is being checked and how it should be checked.
/// </summary>
public class BeatmapVerifierContext
{
/// <summary>
/// The playable beatmap instance of the current beatmap.
/// </summary>
public readonly IBeatmap Beatmap;
/// <summary>
/// The working beatmap instance of the current beatmap.
/// </summary>
public readonly IWorkingBeatmap WorkingBeatmap;
/// <summary>
/// The difficulty level which the current beatmap is considered to be.
/// </summary>
public DifficultyRating InterpretedDifficulty;
public BeatmapVerifierContext(IBeatmap beatmap, IWorkingBeatmap workingBeatmap, DifficultyRating difficultyRating = DifficultyRating.ExpertPlus)
{
Beatmap = beatmap;
WorkingBeatmap = workingBeatmap;
InterpretedDifficulty = difficultyRating;
}
}
}

View File

@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Edit.Checks
{
protected override CheckCategory Category => CheckCategory.Audio;
protected override string TypeOfFile => "audio";
protected override string GetFilename(IBeatmap playableBeatmap) => playableBeatmap.Metadata?.AudioFile;
protected override string GetFilename(IBeatmap beatmap) => beatmap.Metadata?.AudioFile;
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
@ -26,13 +25,13 @@ namespace osu.Game.Rulesets.Edit.Checks
new IssueTemplateNoBitrate(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var audioFile = playableBeatmap.Metadata?.AudioFile;
var audioFile = context.Beatmap.Metadata?.AudioFile;
if (audioFile == null)
yield break;
var track = workingBeatmap.Track;
var track = context.WorkingBeatmap.Track;
if (track?.Bitrate == null || track.Bitrate.Value == 0)
yield return new IssueTemplateNoBitrate(this).Create();

View File

@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Edit.Checks
{
protected override CheckCategory Category => CheckCategory.Resources;
protected override string TypeOfFile => "background";
protected override string GetFilename(IBeatmap playableBeatmap) => playableBeatmap.Metadata?.BackgroundFile;
protected override string GetFilename(IBeatmap beatmap) => beatmap.Metadata?.BackgroundFile;
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit.Checks
@ -30,13 +29,13 @@ namespace osu.Game.Rulesets.Edit.Checks
new IssueTemplateTooUncompressed(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var backgroundFile = playableBeatmap.Metadata?.BackgroundFile;
var backgroundFile = context.Beatmap.Metadata?.BackgroundFile;
if (backgroundFile == null)
yield break;
var texture = workingBeatmap.Background;
var texture = context.WorkingBeatmap.Background;
if (texture == null)
yield break;
@ -48,8 +47,8 @@ namespace osu.Game.Rulesets.Edit.Checks
else if (texture.Width < low_width || texture.Height < low_height)
yield return new IssueTemplateLowResolution(this).Create(texture.Width, texture.Height);
string storagePath = playableBeatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile);
double filesizeMb = workingBeatmap.GetStream(storagePath).Length / (1024d * 1024d);
string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile);
double filesizeMb = context.WorkingBeatmap.GetStream(storagePath).Length / (1024d * 1024d);
if (filesizeMb > max_filesize_mb)
yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb);

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -22,15 +21,17 @@ namespace osu.Game.Rulesets.Edit.Checks
new IssueTemplateConcurrentDifferent(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
for (int i = 0; i < playableBeatmap.HitObjects.Count - 1; ++i)
{
var hitobject = playableBeatmap.HitObjects[i];
var hitObjects = context.Beatmap.HitObjects;
for (int j = i + 1; j < playableBeatmap.HitObjects.Count; ++j)
for (int i = 0; i < hitObjects.Count - 1; ++i)
{
var hitobject = hitObjects[i];
for (int j = i + 1; j < hitObjects.Count; ++j)
{
var nextHitobject = playableBeatmap.HitObjects[j];
var nextHitobject = hitObjects[j];
// Accounts for rulesets with hitobjects separated by columns, such as Mania.
// In these cases we only care about concurrent objects within the same column.

View File

@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Edit.Checks
{
protected abstract CheckCategory Category { get; }
protected abstract string TypeOfFile { get; }
protected abstract string GetFilename(IBeatmap playableBeatmap);
protected abstract string GetFilename(IBeatmap beatmap);
public CheckMetadata Metadata => new CheckMetadata(Category, $"Missing {TypeOfFile}");
@ -21,9 +21,9 @@ namespace osu.Game.Rulesets.Edit.Checks
new IssueTemplateDoesNotExist(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var filename = GetFilename(playableBeatmap);
var filename = GetFilename(context.Beatmap);
if (filename == null)
{
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Edit.Checks
}
// If the file is set, also make sure it still exists.
var storagePath = playableBeatmap.BeatmapInfo.BeatmapSet.GetPathForFile(filename);
var storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(filename);
if (storagePath != null)
yield break;

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -22,11 +21,11 @@ namespace osu.Game.Rulesets.Edit.Checks
new IssueTemplateSmallUnsnap(this)
};
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
var controlPointInfo = playableBeatmap.ControlPointInfo;
var controlPointInfo = context.Beatmap.ControlPointInfo;
foreach (var hitobject in playableBeatmap.HitObjects)
foreach (var hitobject in context.Beatmap.HitObjects)
{
double startUnsnap = hitobject.StartTime - controlPointInfo.GetClosestSnappedTime(hitobject.StartTime);
string startPostfix = hitobject is IHasDuration ? "start" : "";

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Edit.Checks.Components
{
@ -24,8 +23,7 @@ namespace osu.Game.Rulesets.Edit.Checks.Components
/// <summary>
/// Runs this check and returns any issues detected for the provided beatmap.
/// </summary>
/// <param name="playableBeatmap">The playable beatmap of the beatmap to run the check on.</param>
/// <param name="workingBeatmap">The working beatmap of the beatmap to run the check on.</param>
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap);
/// <param name="context">The beatmap verifier context associated with the beatmap.</param>
public IEnumerable<Issue> Run(BeatmapVerifierContext context);
}
}

View File

@ -18,7 +18,6 @@ namespace osu.Game.Rulesets.Edit.Checks.Components
/// <summary> An error occurred and a complete check could not be made. </summary>
Error,
// TODO: Negligible issues should be hidden by default.
/// <summary> A possible mistake so minor/unlikely that it can often be safely ignored. </summary>
Negligible,
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Rulesets.Edit
@ -12,6 +11,6 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public interface IBeatmapVerifier
{
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap);
public IEnumerable<Issue> Run(BeatmapVerifierContext context);
}
}

View File

@ -0,0 +1,32 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Edit
{
public class ScrollingToolboxGroup : ToolboxGroup
{
protected readonly OsuScrollContainer Scroll;
protected override Container<Drawable> Content { get; }
public ScrollingToolboxGroup(string title, float scrollAreaHeight)
: base(title)
{
base.Content.Add(Scroll = new OsuScrollContainer
{
RelativeSizeAxes = Axes.X,
Height = scrollAreaHeight,
Child = Content = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
},
});
}
}
}

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