mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 01:52:55 +08:00
Merge branch 'master' into legacy-spinner-bonus
This commit is contained in:
commit
c4048c9831
2
.github/ISSUE_TEMPLATE/01-bug-issues.md
vendored
2
.github/ISSUE_TEMPLATE/01-bug-issues.md
vendored
@ -13,4 +13,6 @@ about: Issues regarding encountered bugs.
|
||||
*please attach logs here, which are located at:*
|
||||
- `%AppData%/osu/logs` *(on Windows),*
|
||||
- `~/.local/share/osu/logs` *(on Linux & macOS).*
|
||||
- `Android/Data/sh.ppy.osulazer/logs` *(on Android)*,
|
||||
- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
|
||||
-->
|
||||
|
2
.github/ISSUE_TEMPLATE/02-crash-issues.md
vendored
2
.github/ISSUE_TEMPLATE/02-crash-issues.md
vendored
@ -13,6 +13,8 @@ about: Issues regarding crashes or permanent freezes.
|
||||
*please attach logs here, which are located at:*
|
||||
- `%AppData%/osu/logs` *(on Windows),*
|
||||
- `~/.local/share/osu/logs` *(on Linux & macOS).*
|
||||
- `Android/Data/sh.ppy.osulazer/logs` *(on Android)*,
|
||||
- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
|
||||
-->
|
||||
|
||||
**Computer Specifications:**
|
||||
|
@ -30,7 +30,7 @@
|
||||
<Rule Id="CA1819" Action="None" />
|
||||
<Rule Id="CA1822" Action="None" />
|
||||
<Rule Id="CA1823" Action="None" />
|
||||
<Rule Id="CA2007" Action="None" />
|
||||
<Rule Id="CA2007" Action="Warning" />
|
||||
<Rule Id="CA2214" Action="None" />
|
||||
<Rule Id="CA2227" Action="None" />
|
||||
</Rules>
|
||||
|
@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.226.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.309.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -100,15 +100,15 @@ namespace osu.Android
|
||||
// copy to an arbitrary-access memory stream to be able to proceed with the import.
|
||||
var copy = new MemoryStream();
|
||||
using (var stream = ContentResolver.OpenInputStream(uri))
|
||||
await stream.CopyToAsync(copy);
|
||||
await stream.CopyToAsync(copy).ConfigureAwait(false);
|
||||
|
||||
lock (tasks)
|
||||
{
|
||||
tasks.Add(new ImportTask(copy, filename));
|
||||
}
|
||||
}));
|
||||
})).ConfigureAwait(false);
|
||||
|
||||
await game.Import(tasks.ToArray());
|
||||
await game.Import(tasks.ToArray()).ConfigureAwait(false);
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Desktop.Updater
|
||||
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
|
||||
}
|
||||
|
||||
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync();
|
||||
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
|
||||
|
||||
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
|
||||
{
|
||||
@ -51,9 +51,9 @@ namespace osu.Desktop.Updater
|
||||
|
||||
try
|
||||
{
|
||||
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
|
||||
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false);
|
||||
|
||||
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
|
||||
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
|
||||
|
||||
if (info.ReleasesToApply.Count == 0)
|
||||
{
|
||||
@ -79,12 +79,12 @@ namespace osu.Desktop.Updater
|
||||
|
||||
try
|
||||
{
|
||||
await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f);
|
||||
await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
notification.Progress = 0;
|
||||
notification.Text = @"Installing update...";
|
||||
|
||||
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
|
||||
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false);
|
||||
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
updatePending = true;
|
||||
@ -97,7 +97,7 @@ namespace osu.Desktop.Updater
|
||||
|
||||
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
|
||||
// try again without deltas.
|
||||
await checkForUpdateAsync(false, notification);
|
||||
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
|
||||
scheduleRecheck = false;
|
||||
}
|
||||
else
|
||||
@ -116,7 +116,7 @@ namespace osu.Desktop.Updater
|
||||
if (scheduleRecheck)
|
||||
{
|
||||
// check again in 30 minutes.
|
||||
Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
|
||||
Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap)
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
|
||||
{
|
||||
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
|
||||
|
||||
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
new Movement(halfCatcherWidth),
|
||||
new Movement(mods, halfCatcherWidth),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
{
|
||||
@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
private float lastDistanceMoved;
|
||||
private double lastStrainTime;
|
||||
|
||||
public Movement(float halfCatcherWidth)
|
||||
public Movement(Mod[] mods, float halfCatcherWidth)
|
||||
: base(mods)
|
||||
{
|
||||
HalfCatcherWidth = halfCatcherWidth;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
|
||||
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
|
||||
{
|
||||
new Strain(((ManiaBeatmap)beatmap).TotalColumns)
|
||||
new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
|
||||
};
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
||||
private double individualStrain;
|
||||
private double overallStrain;
|
||||
|
||||
public Strain(int totalColumns)
|
||||
public Strain(Mod[] mods, int totalColumns)
|
||||
: base(mods)
|
||||
{
|
||||
holdEndTimes = new double[totalColumns];
|
||||
individualStrains = new double[totalColumns];
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -79,10 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
}
|
||||
}
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
|
||||
{
|
||||
new Aim(),
|
||||
new Speed()
|
||||
new Aim(mods),
|
||||
new Speed(mods)
|
||||
};
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
@ -17,6 +18,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
private const double angle_bonus_begin = Math.PI / 3;
|
||||
private const double timing_threshold = 107;
|
||||
|
||||
public Aim(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double SkillMultiplier => 26.25;
|
||||
protected override double StrainDecayBase => 0.15;
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
@ -27,6 +28,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
private const double max_speed_bonus = 45; // ~330BPM
|
||||
private const double speed_balancing_factor = 40;
|
||||
|
||||
public Speed(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
|
@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.Judgements
|
||||
/// </example>
|
||||
public float RateAdjustedRotation;
|
||||
|
||||
/// <summary>
|
||||
/// Time instant at which the spin was started (the first user input which caused an increase in spin).
|
||||
/// </summary>
|
||||
public double? TimeStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Time instant at which the spinner has been completed (the user has executed all required spins).
|
||||
/// Will be null if all required spins haven't been completed.
|
||||
|
@ -159,6 +159,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateStartTimeStateTransforms()
|
||||
{
|
||||
base.UpdateStartTimeStateTransforms();
|
||||
|
||||
if (Result?.TimeStarted is double startTime)
|
||||
{
|
||||
using (BeginAbsoluteSequence(startTime))
|
||||
fadeInCounter();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||
{
|
||||
base.UpdateHitStateTransforms(state);
|
||||
@ -263,13 +274,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
|
||||
SpmCounter.FadeIn(HitObject.TimeFadeIn);
|
||||
{
|
||||
Result.TimeStarted ??= Time.Current;
|
||||
fadeInCounter();
|
||||
}
|
||||
|
||||
SpmCounter.SetRotation(Result.RateAdjustedRotation);
|
||||
// don't update after end time to avoid the rate display dropping during fade out.
|
||||
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
|
||||
if (Time.Current <= HitObject.EndTime)
|
||||
SpmCounter.SetRotation(Result.RateAdjustedRotation);
|
||||
|
||||
updateBonusScore();
|
||||
}
|
||||
|
||||
private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
|
||||
|
||||
private int wholeSpins;
|
||||
|
||||
private void updateBonusScore()
|
||||
|
@ -37,9 +37,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
AddInternal(scaleContainer = new Container
|
||||
{
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Anchor = Anchor.Centre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Y = SPINNER_Y_CENTRE,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
glow = new Sprite
|
||||
|
@ -37,35 +37,34 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-background"),
|
||||
Scale = new Vector2(SPRITE_SCALE)
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Y = SPINNER_Y_CENTRE,
|
||||
},
|
||||
disc = new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-circle"),
|
||||
Scale = new Vector2(SPRITE_SCALE)
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Y = SPINNER_Y_CENTRE,
|
||||
},
|
||||
new LegacyCoordinatesContainer
|
||||
metre = new Container
|
||||
{
|
||||
Child = metre = new Container
|
||||
AutoSizeAxes = Axes.Both,
|
||||
// this anchor makes no sense, but that's what stable uses.
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET },
|
||||
Masking = true,
|
||||
Child = metreSprite = new Sprite
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
// this anchor makes no sense, but that's what stable uses.
|
||||
Texture = source.GetTexture("spinner-metre"),
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Margin = new MarginPadding { Top = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET },
|
||||
Masking = true,
|
||||
Child = metreSprite = new Sprite
|
||||
{
|
||||
Texture = source.GetTexture("spinner-metre"),
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Scale = new Vector2(SPRITE_SCALE)
|
||||
}
|
||||
Scale = new Vector2(SPRITE_SCALE)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public abstract class LegacySpinner : CompositeDrawable
|
||||
{
|
||||
public const float SPRITE_SCALE = 0.625f;
|
||||
/// <remarks>
|
||||
/// All constants are in osu!stable's gamefield space, which is shifted 16px downwards.
|
||||
/// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space.
|
||||
/// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable)
|
||||
/// </remarks>
|
||||
protected const float SPINNER_TOP_OFFSET = 45f - 16f;
|
||||
|
||||
protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f;
|
||||
|
||||
protected const float SPRITE_SCALE = 0.625f;
|
||||
|
||||
protected DrawableSpinner DrawableSpinner { get; private set; }
|
||||
|
||||
@ -26,33 +35,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableHitObject, ISkinSource source)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
// osu!stable positions spinner components in window-space (as opposed to gamefield-space). This is a 640x480 area taking up the entire screen.
|
||||
// In lazer, the gamefield-space positional transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to make this area take up the entire window space.
|
||||
Size = new Vector2(640, 480);
|
||||
Position = new Vector2(0, -8f);
|
||||
|
||||
DrawableSpinner = (DrawableSpinner)drawableHitObject;
|
||||
|
||||
AddInternal(new LegacyCoordinatesContainer
|
||||
AddRangeInternal(new[]
|
||||
{
|
||||
Depth = float.MinValue,
|
||||
Children = new Drawable[]
|
||||
spin = new Sprite
|
||||
{
|
||||
spin = new Sprite
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-spin"),
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 335,
|
||||
},
|
||||
clear = new Sprite
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = source.GetTexture("spinner-clear"),
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 115,
|
||||
},
|
||||
}
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Depth = float.MinValue,
|
||||
Texture = source.GetTexture("spinner-spin"),
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Y = SPINNER_TOP_OFFSET + 335,
|
||||
},
|
||||
clear = new Sprite
|
||||
{
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Depth = float.MinValue,
|
||||
Texture = source.GetTexture("spinner-clear"),
|
||||
Scale = new Vector2(SPRITE_SCALE),
|
||||
Y = SPINNER_TOP_OFFSET + 115,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -129,33 +142,5 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
if (DrawableSpinner != null)
|
||||
DrawableSpinner.ApplyCustomUpdateState -= UpdateStateTransforms;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Container"/> simulating osu!stable's absolute screen-space,
|
||||
/// for perfect placements of legacy spinner components with legacy coordinates.
|
||||
/// </summary>
|
||||
public class LegacyCoordinatesContainer : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// An offset that simulates stable's spinner top offset,
|
||||
/// for positioning some legacy spinner components perfectly as in stable.
|
||||
/// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners)
|
||||
/// </summary>
|
||||
public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE);
|
||||
|
||||
public LegacyCoordinatesContainer()
|
||||
{
|
||||
// legacy spinners relied heavily on absolute screen-space coordinate values.
|
||||
// wrap everything in a container simulating absolute coords to preserve alignment
|
||||
// as there are skins that depend on it.
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
Size = new Vector2(640, 480);
|
||||
|
||||
// since legacy coordinates were on screen-space, they were accounting for the playfield shift offset.
|
||||
// therefore cancel it from here.
|
||||
Position = new Vector2(0, -8f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<Import Project="..\osu.TestProject.props" />
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// </summary>
|
||||
private int currentMonoLength;
|
||||
|
||||
public Colour(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
// changing from/to a drum roll or a swell does not constitute a colour change.
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
@ -47,6 +48,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// </summary>
|
||||
private int notesSinceRhythmChange;
|
||||
|
||||
public Rhythm(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
// drum rolls and swells are exempt.
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
@ -48,8 +49,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Stamina"/> skill.
|
||||
/// </summary>
|
||||
/// <param name="mods">Mods for use in skill calculations.</param>
|
||||
/// <param name="rightHand">Whether this instance is performing calculations for the right hand.</param>
|
||||
public Stamina(bool rightHand)
|
||||
public Stamina(Mod[] mods, bool rightHand)
|
||||
: base(mods)
|
||||
{
|
||||
hand = rightHand ? 1 : 0;
|
||||
}
|
||||
|
@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
}
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
|
||||
{
|
||||
new Colour(),
|
||||
new Rhythm(),
|
||||
new Stamina(true),
|
||||
new Stamina(false),
|
||||
new Colour(mods),
|
||||
new Rhythm(mods),
|
||||
new Stamina(mods, true),
|
||||
new Stamina(mods, false),
|
||||
};
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
// if a taiko skin is providing explosion sprites, hide the judgements completely
|
||||
if (hasExplosion.Value)
|
||||
return Drawable.Empty();
|
||||
return Drawable.Empty().With(d => d.Expire());
|
||||
}
|
||||
|
||||
if (!(component is TaikoSkinComponent taikoComponent))
|
||||
@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
// suppress the default kiai explosion if the skin brings its own sprites.
|
||||
// the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield.
|
||||
if (hasExplosion.Value)
|
||||
return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue);
|
||||
return Drawable.Empty().With(d => d.Expire());
|
||||
|
||||
return null;
|
||||
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
@ -12,16 +11,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
/// </summary>
|
||||
public class DrawableTaikoJudgement : DrawableJudgement
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new judgement text.
|
||||
/// </summary>
|
||||
/// <param name="judgedObject">The object which is being judged.</param>
|
||||
/// <param name="result">The judgement to visualise.</param>
|
||||
public DrawableTaikoJudgement(JudgementResult result, DrawableHitObject judgedObject)
|
||||
: base(result, judgedObject)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ApplyHitAnimations()
|
||||
{
|
||||
this.MoveToY(-100, 500);
|
||||
|
@ -2,10 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -17,6 +19,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Taiko.Judgements;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@ -38,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
internal Drawable HitTarget;
|
||||
private SkinnableDrawable mascot;
|
||||
|
||||
private readonly IDictionary<HitResult, DrawablePool<DrawableTaikoJudgement>> judgementPools = new Dictionary<HitResult, DrawablePool<DrawableTaikoJudgement>>();
|
||||
|
||||
private ProxyContainer topLevelHitContainer;
|
||||
private Container rightArea;
|
||||
private Container leftArea;
|
||||
@ -159,6 +164,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
RegisterPool<Swell, DrawableSwell>(5);
|
||||
RegisterPool<SwellTick, DrawableSwellTick>(100);
|
||||
|
||||
var hitWindows = new TaikoHitWindows();
|
||||
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
|
||||
judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
|
||||
|
||||
AddRangeInternal(judgementPools.Values);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -283,13 +294,15 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
break;
|
||||
|
||||
default:
|
||||
judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject)
|
||||
judgementContainer.Add(judgementPools[result.Type].Get(j =>
|
||||
{
|
||||
Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft,
|
||||
Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre,
|
||||
RelativePositionAxes = Axes.X,
|
||||
X = result.IsHit ? judgedObject.Position.X : 0,
|
||||
});
|
||||
j.Apply(result, judgedObject);
|
||||
|
||||
j.Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft;
|
||||
j.Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre;
|
||||
j.RelativePositionAxes = Axes.X;
|
||||
j.X = result.IsHit ? judgedObject.Position.X : 0;
|
||||
}));
|
||||
|
||||
var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
|
||||
addExplosion(judgedObject, result.Type, type);
|
||||
|
@ -20,6 +20,9 @@
|
||||
<ItemGroup>
|
||||
<None Include="Properties\AndroidManifest.xml" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);CA2007</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\osu.Game.Tests\**\Beatmaps\**\*.cs">
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
@ -71,7 +74,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
@ -21,6 +21,9 @@
|
||||
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);CA2007</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj">
|
||||
<Project>{2A66DD92-ADB1-4994-89E2-C94E04ACDA0D}</Project>
|
||||
@ -45,7 +48,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
|
||||
</Project>
|
||||
</Project>
|
||||
|
@ -212,7 +212,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap)
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
@ -4,8 +4,10 @@
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual.Filtering
|
||||
{
|
||||
@ -214,5 +216,31 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
|
||||
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomRulesetCriteria([Values(null, true, false)] bool? matchCustomCriteria)
|
||||
{
|
||||
var beatmap = getExampleBeatmap();
|
||||
|
||||
var customCriteria = matchCustomCriteria is bool match ? new CustomCriteria(match) : null;
|
||||
var criteria = new FilterCriteria { RulesetCriteria = customCriteria };
|
||||
var carouselItem = new CarouselBeatmap(beatmap);
|
||||
carouselItem.Filter(criteria);
|
||||
|
||||
Assert.AreEqual(matchCustomCriteria == false, carouselItem.Filtered.Value);
|
||||
}
|
||||
|
||||
private class CustomCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
private readonly bool match;
|
||||
|
||||
public CustomCriteria(bool shouldMatch)
|
||||
{
|
||||
match = shouldMatch;
|
||||
}
|
||||
|
||||
public bool Matches(BeatmapInfo beatmap) => match;
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,9 @@
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual.Filtering
|
||||
{
|
||||
@ -194,5 +196,63 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOperatorParsing()
|
||||
{
|
||||
const string query = "artist=><something";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("><something", filterCriteria.Artist.SearchTerm);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnrecognisedKeywordIsIgnored()
|
||||
{
|
||||
const string query = "unrecognised=keyword";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText);
|
||||
}
|
||||
|
||||
[TestCase("cs=nope")]
|
||||
[TestCase("bpm>=bad")]
|
||||
[TestCase("divisor<nah")]
|
||||
[TestCase("status=noidea")]
|
||||
public void TestInvalidKeywordValueIsIgnored(string query)
|
||||
{
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual(query, filterCriteria.SearchText);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomKeywordIsParsed()
|
||||
{
|
||||
var customCriteria = new CustomFilterCriteria();
|
||||
const string query = "custom=readme unrecognised=keyword";
|
||||
var filterCriteria = new FilterCriteria { RulesetCriteria = customCriteria };
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("readme", customCriteria.CustomValue);
|
||||
Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText.Trim());
|
||||
}
|
||||
|
||||
private class CustomFilterCriteria : IRulesetFilterCriteria
|
||||
{
|
||||
public string CustomValue { get; set; }
|
||||
|
||||
public bool Matches(BeatmapInfo beatmap) => true;
|
||||
|
||||
public bool TryParseCustomKeywordCriteria(string key, Operator op, string value)
|
||||
{
|
||||
if (key == "custom" && op == Operator.Equal)
|
||||
{
|
||||
CustomValue = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35
osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs
Normal file
35
osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs
Normal file
@ -0,0 +1,35 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.OnlinePlay
|
||||
{
|
||||
[HeadlessTest]
|
||||
public class StatefulMultiplayerClientTest : MultiplayerTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestUserAddedOnJoin()
|
||||
{
|
||||
var user = new User { Id = 33 };
|
||||
|
||||
AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
|
||||
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserRemovedOnLeave()
|
||||
{
|
||||
var user = new User { Id = 44 };
|
||||
|
||||
AddStep("add user", () => Client.AddUser(user));
|
||||
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
|
||||
|
||||
AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
|
||||
AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
|
||||
}
|
||||
}
|
||||
}
|
@ -46,11 +46,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestCase(0, 0)]
|
||||
[TestCase(-1000, -1000)]
|
||||
[TestCase(-10000, -10000)]
|
||||
public void TestStoryboardProducesCorrectStartTime(double firstStoryboardEvent, double expectedStartTime)
|
||||
public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(double firstStoryboardEvent, double expectedStartTime)
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
|
||||
sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
|
||||
|
||||
storyboard.GetLayer("Background").Add(sprite);
|
||||
@ -64,6 +65,43 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
[TestCase(1000, 0, false)]
|
||||
[TestCase(0, 0, false)]
|
||||
[TestCase(-1000, -1000, false)]
|
||||
[TestCase(-10000, -10000, false)]
|
||||
[TestCase(1000, 0, true)]
|
||||
[TestCase(0, 0, true)]
|
||||
[TestCase(-1000, -1000, true)]
|
||||
[TestCase(-10000, -10000, true)]
|
||||
public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop)
|
||||
{
|
||||
var storyboard = new Storyboard();
|
||||
|
||||
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
|
||||
|
||||
// these should be ignored as we have an alpha visibility blocker proceeding this command.
|
||||
sprite.TimelineGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1);
|
||||
var loopGroup = sprite.AddLoop(-20000, 50);
|
||||
loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1);
|
||||
|
||||
var target = addEventToLoop ? loopGroup : sprite.TimelineGroup;
|
||||
target.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
|
||||
|
||||
// these should be ignored due to being in the future.
|
||||
sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
|
||||
loopGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
|
||||
|
||||
storyboard.GetLayer("Background").Add(sprite);
|
||||
|
||||
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard);
|
||||
|
||||
AddAssert($"first frame is {expectedStartTime}", () =>
|
||||
{
|
||||
Debug.Assert(player.FirstFrameClockTime != null);
|
||||
return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms);
|
||||
});
|
||||
}
|
||||
|
||||
private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
|
||||
{
|
||||
AddStep("create player", () =>
|
||||
|
@ -1,46 +1,41 @@
|
||||
// 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.Game.Online.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneMultiplayer : MultiplayerTestScene
|
||||
public class TestSceneMultiplayer : ScreenTestScene
|
||||
{
|
||||
private TestMultiplayer multiplayerScreen;
|
||||
|
||||
public TestSceneMultiplayer()
|
||||
{
|
||||
var multi = new TestMultiplayer();
|
||||
AddStep("show", () =>
|
||||
{
|
||||
multiplayerScreen = new TestMultiplayer();
|
||||
|
||||
AddStep("show", () => LoadScreen(multi));
|
||||
AddUntilStep("wait for loaded", () => multi.IsLoaded);
|
||||
}
|
||||
// Needs to be added at a higher level since the multiplayer screen becomes non-current.
|
||||
Child = multiplayerScreen.Client;
|
||||
|
||||
[Test]
|
||||
public void TestOneUserJoinedMultipleTimes()
|
||||
{
|
||||
var user = new User { Id = 33 };
|
||||
LoadScreen(multiplayerScreen);
|
||||
});
|
||||
|
||||
AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3);
|
||||
|
||||
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOneUserLeftMultipleTimes()
|
||||
{
|
||||
var user = new User { Id = 44 };
|
||||
|
||||
AddStep("add user", () => Client.AddUser(user));
|
||||
AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2);
|
||||
|
||||
AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3);
|
||||
AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1);
|
||||
AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded);
|
||||
}
|
||||
|
||||
private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer
|
||||
{
|
||||
[Cached(typeof(StatefulMultiplayerClient))]
|
||||
public readonly TestMultiplayerClient Client;
|
||||
|
||||
public TestMultiplayer()
|
||||
{
|
||||
Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager);
|
||||
}
|
||||
|
||||
protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager();
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,10 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
private MultiplayerMatchSubScreen screen;
|
||||
|
||||
[Cached]
|
||||
private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
|
||||
|
||||
public TestSceneMultiplayerMatchSubScreen()
|
||||
: base(false)
|
||||
{
|
||||
|
@ -1,10 +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;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@ -21,15 +24,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||
{
|
||||
roomManager.CreateRoom(new Room { Name = { Value = "1" } });
|
||||
roomManager.CreateRoom(createRoom(r => r.Name.Value = "1"));
|
||||
roomManager.PartRoom();
|
||||
roomManager.CreateRoom(new Room { Name = { Value = "2" } });
|
||||
roomManager.CreateRoom(createRoom(r => r.Name.Value = "2"));
|
||||
roomManager.PartRoom();
|
||||
roomManager.ClearRooms();
|
||||
});
|
||||
});
|
||||
|
||||
AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
|
||||
AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
|
||||
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
|
||||
}
|
||||
|
||||
@ -40,16 +43,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||
{
|
||||
roomManager.CreateRoom(new Room());
|
||||
roomManager.CreateRoom(createRoom());
|
||||
roomManager.PartRoom();
|
||||
roomManager.CreateRoom(new Room());
|
||||
roomManager.CreateRoom(createRoom());
|
||||
roomManager.PartRoom();
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("disconnect", () => roomContainer.Client.Disconnect());
|
||||
|
||||
AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0);
|
||||
AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0);
|
||||
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
|
||||
}
|
||||
|
||||
@ -60,9 +63,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||
{
|
||||
roomManager.CreateRoom(new Room());
|
||||
roomManager.CreateRoom(createRoom());
|
||||
roomManager.PartRoom();
|
||||
roomManager.CreateRoom(new Room());
|
||||
roomManager.CreateRoom(createRoom());
|
||||
roomManager.PartRoom();
|
||||
});
|
||||
});
|
||||
@ -70,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("disconnect", () => roomContainer.Client.Disconnect());
|
||||
AddStep("connect", () => roomContainer.Client.Connect());
|
||||
|
||||
AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2);
|
||||
AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2);
|
||||
AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value);
|
||||
}
|
||||
|
||||
@ -81,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||
{
|
||||
roomManager.CreateRoom(new Room());
|
||||
roomManager.CreateRoom(createRoom());
|
||||
roomManager.ClearRooms();
|
||||
});
|
||||
});
|
||||
|
||||
AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0);
|
||||
AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0);
|
||||
AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value);
|
||||
}
|
||||
|
||||
@ -97,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||
{
|
||||
roomManager.CreateRoom(new Room());
|
||||
roomManager.CreateRoom(createRoom());
|
||||
});
|
||||
});
|
||||
|
||||
@ -111,7 +114,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||
{
|
||||
roomManager.CreateRoom(new Room());
|
||||
roomManager.CreateRoom(createRoom());
|
||||
roomManager.PartRoom();
|
||||
});
|
||||
});
|
||||
@ -126,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
createRoomManager().With(d => d.OnLoadComplete += _ =>
|
||||
{
|
||||
var r = new Room();
|
||||
var r = createRoom();
|
||||
roomManager.CreateRoom(r);
|
||||
roomManager.PartRoom();
|
||||
roomManager.JoinRoom(r);
|
||||
@ -136,6 +139,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null);
|
||||
}
|
||||
|
||||
private Room createRoom(Action<Room> initFunc = null)
|
||||
{
|
||||
var room = new Room();
|
||||
|
||||
room.Name.Value = "test room";
|
||||
room.Playlist.Add(new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
|
||||
Ruleset = { Value = Ruleset.Value }
|
||||
});
|
||||
|
||||
initFunc?.Invoke(room);
|
||||
return room;
|
||||
}
|
||||
|
||||
private TestMultiplayerRoomManager createRoomManager()
|
||||
{
|
||||
Child = roomContainer = new TestMultiplayerRoomContainer
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Username = "flyte",
|
||||
Id = 3103765,
|
||||
IsOnline = true,
|
||||
CurrentModeRank = 1111,
|
||||
Statistics = new UserStatistics { GlobalRank = 1111 },
|
||||
Country = new Country { FlagName = "JP" },
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
|
||||
},
|
||||
@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Username = "peppy",
|
||||
Id = 2,
|
||||
IsOnline = false,
|
||||
CurrentModeRank = 2222,
|
||||
Statistics = new UserStatistics { GlobalRank = 2222 },
|
||||
Country = new Country { FlagName = "AU" },
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
IsSupporter = true,
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
Room.RecentParticipants.Add(new User
|
||||
{
|
||||
Username = "peppy",
|
||||
CurrentModeRank = 1234,
|
||||
Statistics = new UserStatistics { GlobalRank = 1234 },
|
||||
Id = 2
|
||||
});
|
||||
}
|
||||
|
43
osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
Normal file
43
osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs
Normal file
@ -0,0 +1,43 @@
|
||||
// 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.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Settings
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneSettingsItem : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestRestoreDefaultValueButtonVisibility()
|
||||
{
|
||||
TestSettingsTextBox textBox = null;
|
||||
|
||||
AddStep("create settings item", () => Child = textBox = new TestSettingsTextBox
|
||||
{
|
||||
Current = new Bindable<string>
|
||||
{
|
||||
Default = "test",
|
||||
Value = "test"
|
||||
}
|
||||
});
|
||||
AddAssert("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
|
||||
|
||||
AddStep("change value from default", () => textBox.Current.Value = "non-default");
|
||||
AddUntilStep("restore button shown", () => textBox.RestoreDefaultValueButton.Alpha > 0);
|
||||
|
||||
AddStep("restore default", () => textBox.Current.SetDefault());
|
||||
AddUntilStep("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
|
||||
}
|
||||
|
||||
private class TestSettingsTextBox : SettingsTextBox
|
||||
{
|
||||
public new Drawable RestoreDefaultValueButton => this.ChildrenOfType<RestoreDefaultValueButton>().Single();
|
||||
}
|
||||
}
|
||||
}
|
@ -3,16 +3,19 @@
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="Moq" Version="4.16.0" />
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Project">
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Code Analysis">
|
||||
<CodeAnalysisRuleSet>tests.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" />
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />
|
||||
|
6
osu.Game.Tests/tests.ruleset
Normal file
6
osu.Game.Tests/tests.ruleset
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RuleSet Name="osu! Rule Set" Description=" " ToolsVersion="16.0">
|
||||
<Rules AnalyzerId="Microsoft.CodeQuality.Analyzers" RuleNamespace="Microsoft.CodeQuality.Analyzers">
|
||||
<Rule Id="CA2007" Action="None" />
|
||||
</Rules>
|
||||
</RuleSet>
|
@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
</ItemGroup>
|
||||
|
@ -150,7 +150,9 @@ namespace osu.Game.Tournament
|
||||
{
|
||||
foreach (var p in t.Players)
|
||||
{
|
||||
if (string.IsNullOrEmpty(p.Username) || p.Statistics?.GlobalRank == null)
|
||||
if (string.IsNullOrEmpty(p.Username)
|
||||
|| p.Statistics?.GlobalRank == null
|
||||
|| p.Statistics?.CountryRank == null)
|
||||
{
|
||||
PopulateUser(p, immediate: true);
|
||||
addedInfo = true;
|
||||
|
@ -20,6 +20,7 @@ using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Database;
|
||||
@ -155,7 +156,7 @@ namespace osu.Game.Beatmaps
|
||||
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
|
||||
|
||||
if (onlineLookupQueue != null)
|
||||
await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken);
|
||||
await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
|
||||
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
|
||||
@ -311,6 +312,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
|
||||
|
||||
// best effort; may be higher than expected.
|
||||
GlobalStatistics.Get<int>(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count();
|
||||
|
||||
return working;
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -34,8 +33,6 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
protected AudioManager AudioManager { get; }
|
||||
|
||||
private static readonly GlobalStatistic<int> total_count = GlobalStatistics.Get<int>(nameof(Beatmaps), $"Total {nameof(WorkingBeatmap)}s");
|
||||
|
||||
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
|
||||
{
|
||||
AudioManager = audioManager;
|
||||
@ -47,8 +44,6 @@ namespace osu.Game.Beatmaps
|
||||
waveform = new RecyclableLazy<Waveform>(GetWaveform);
|
||||
storyboard = new RecyclableLazy<Storyboard>(GetStoryboard);
|
||||
skin = new RecyclableLazy<ISkin>(GetSkin);
|
||||
|
||||
total_count.Value++;
|
||||
}
|
||||
|
||||
protected virtual Track GetVirtualTrack(double emptyLength = 0)
|
||||
@ -331,11 +326,6 @@ namespace osu.Game.Beatmaps
|
||||
protected virtual ISkin GetSkin() => new DefaultSkin();
|
||||
private readonly RecyclableLazy<ISkin> skin;
|
||||
|
||||
~WorkingBeatmap()
|
||||
{
|
||||
total_count.Value--;
|
||||
}
|
||||
|
||||
public class RecyclableLazy<T>
|
||||
{
|
||||
private Lazy<T> lazy;
|
||||
|
@ -124,7 +124,7 @@ namespace osu.Game.Collections
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
using (var stream = stable.GetStream(database_name))
|
||||
await Import(stream);
|
||||
await Import(stream).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@ namespace osu.Game.Collections
|
||||
PostNotification?.Invoke(notification);
|
||||
|
||||
var collections = readCollections(stream, notification);
|
||||
await importCollections(collections);
|
||||
await importCollections(collections).ConfigureAwait(false);
|
||||
|
||||
notification.CompletionText = $"Imported {collections.Count} collections";
|
||||
notification.State = ProgressNotificationState.Completed;
|
||||
|
@ -22,7 +22,6 @@ using osu.Game.IO.Archives;
|
||||
using osu.Game.IPC;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using FileInfo = osu.Game.IO.FileInfo;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
@ -163,7 +162,7 @@ namespace osu.Game.Database
|
||||
|
||||
try
|
||||
{
|
||||
var model = await Import(task, isLowPriorityImport, notification.CancellationToken);
|
||||
var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
lock (imported)
|
||||
{
|
||||
@ -183,7 +182,7 @@ namespace osu.Game.Database
|
||||
{
|
||||
Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database);
|
||||
}
|
||||
}));
|
||||
})).ConfigureAwait(false);
|
||||
|
||||
if (imported.Count == 0)
|
||||
{
|
||||
@ -226,7 +225,7 @@ namespace osu.Game.Database
|
||||
|
||||
TModel import;
|
||||
using (ArchiveReader reader = task.GetReader())
|
||||
import = await Import(reader, lowPriority, cancellationToken);
|
||||
import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// We may or may not want to delete the file depending on where it is stored.
|
||||
// e.g. reconstructing/repairing database with items from default storage.
|
||||
@ -358,7 +357,7 @@ namespace osu.Game.Database
|
||||
item.Files = archive != null ? createFileInfos(archive, Files) : new List<TFileModel>();
|
||||
item.Hash = ComputeHash(item, archive);
|
||||
|
||||
await Populate(item, archive, cancellationToken);
|
||||
await Populate(item, archive, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
|
||||
{
|
||||
@ -410,7 +409,7 @@ namespace osu.Game.Database
|
||||
|
||||
flushEvents(true);
|
||||
return item;
|
||||
}, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap();
|
||||
}, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Exports an item to a legacy (.zip based) package.
|
||||
@ -621,7 +620,7 @@ namespace osu.Game.Database
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create all required <see cref="FileInfo"/>s for the provided archive, adding them to the global file store.
|
||||
/// Create all required <see cref="IO.FileInfo"/>s for the provided archive, adding them to the global file store.
|
||||
/// </summary>
|
||||
private List<TFileModel> createFileInfos(ArchiveReader reader, FileStore files)
|
||||
{
|
||||
@ -699,7 +698,7 @@ namespace osu.Game.Database
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()));
|
||||
return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()).ConfigureAwait(false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -54,10 +54,5 @@ namespace osu.Game.Database
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~DatabaseWriteUsage()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ namespace osu.Game.Database
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
// This gets scheduled back to the update thread, but we want the import to run in the background.
|
||||
var imported = await Import(notification, new ImportTask(filename));
|
||||
var imported = await Import(notification, new ImportTask(filename)).ConfigureAwait(false);
|
||||
|
||||
// for now a failed import will be marked as a failed download for simplicity.
|
||||
if (!imported.Any())
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Database
|
||||
if (CheckExists(lookup, out TValue performance))
|
||||
return performance;
|
||||
|
||||
var computed = await ComputeValueAsync(lookup, token);
|
||||
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
|
||||
|
||||
if (computed != null || CacheNullValues)
|
||||
cache[lookup] = computed;
|
||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Database
|
||||
public Task<User> GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token);
|
||||
|
||||
protected override async Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
|
||||
=> await queryUser(lookup);
|
||||
=> await queryUser(lookup).ConfigureAwait(false);
|
||||
|
||||
private readonly Queue<(int id, TaskCompletionSource<User>)> pendingUserTasks = new Queue<(int, TaskCompletionSource<User>)>();
|
||||
private Task pendingRequestTask;
|
||||
|
@ -103,7 +103,7 @@ namespace osu.Game.Graphics
|
||||
}
|
||||
}
|
||||
|
||||
using (var image = await host.TakeScreenshotAsync())
|
||||
using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false)
|
||||
cursorVisibility.Value = true;
|
||||
@ -116,13 +116,13 @@ namespace osu.Game.Graphics
|
||||
switch (screenshotFormat.Value)
|
||||
{
|
||||
case ScreenshotFormat.Png:
|
||||
await image.SaveAsPngAsync(stream);
|
||||
await image.SaveAsPngAsync(stream).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case ScreenshotFormat.Jpg:
|
||||
const int jpeg_quality = 92;
|
||||
|
||||
await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality });
|
||||
await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.IO.Archives
|
||||
return null;
|
||||
|
||||
byte[] buffer = new byte[input.Length];
|
||||
await input.ReadAsync(buffer);
|
||||
await input.ReadAsync(buffer).ConfigureAwait(false);
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
@ -33,12 +33,12 @@ namespace osu.Game.IPC
|
||||
if (importer == null)
|
||||
{
|
||||
// we want to contact a remote osu! to handle the import.
|
||||
await SendMessageAsync(new ArchiveImportMessage { Path = path });
|
||||
await SendMessageAsync(new ArchiveImportMessage { Path = path }).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (importer.HandledExtensions.Contains(Path.GetExtension(path)?.ToLowerInvariant()))
|
||||
await importer.Import(path);
|
||||
await importer.Import(path).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,6 @@ using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ExceptionExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@ -247,14 +246,8 @@ namespace osu.Game.Online.API
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public IHubClientConnector GetHubConnector(string clientName, string endpoint)
|
||||
{
|
||||
// disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805.
|
||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS)
|
||||
return null;
|
||||
|
||||
return new HubClientConnector(clientName, endpoint, this, versionHash);
|
||||
}
|
||||
public IHubClientConnector GetHubConnector(string clientName, string endpoint) =>
|
||||
new HubClientConnector(clientName, endpoint, this, versionHash);
|
||||
|
||||
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
|
||||
{
|
||||
|
@ -79,7 +79,7 @@ namespace osu.Game.Online
|
||||
{
|
||||
cancelExistingConnect();
|
||||
|
||||
if (!await connectionLock.WaitAsync(10000))
|
||||
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
|
||||
throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
|
||||
|
||||
try
|
||||
@ -88,7 +88,7 @@ namespace osu.Game.Online
|
||||
{
|
||||
// ensure any previous connection was disposed.
|
||||
// this will also create a new cancellation token source.
|
||||
await disconnect(false);
|
||||
await disconnect(false).ConfigureAwait(false);
|
||||
|
||||
// this token will be valid for the scope of this connection.
|
||||
// if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
|
||||
@ -103,7 +103,7 @@ namespace osu.Game.Online
|
||||
// importantly, rebuild the connection each attempt to get an updated access token.
|
||||
CurrentConnection = buildConnection(cancellationToken);
|
||||
|
||||
await CurrentConnection.StartAsync(cancellationToken);
|
||||
await CurrentConnection.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Logger.Log($"{clientName} connected!", LoggingTarget.Network);
|
||||
isConnected.Value = true;
|
||||
@ -119,7 +119,7 @@ namespace osu.Game.Online
|
||||
Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network);
|
||||
|
||||
// retry on any failure.
|
||||
await Task.Delay(5000, cancellationToken);
|
||||
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -174,14 +174,14 @@ namespace osu.Game.Online
|
||||
|
||||
if (takeLock)
|
||||
{
|
||||
if (!await connectionLock.WaitAsync(10000))
|
||||
if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
|
||||
throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (CurrentConnection != null)
|
||||
await CurrentConnection.DisposeAsync();
|
||||
await CurrentConnection.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -9,7 +9,9 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
@ -121,6 +123,29 @@ namespace osu.Game.Online.Multiplayer
|
||||
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
|
||||
}
|
||||
|
||||
protected override Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<BeatmapSetInfo>();
|
||||
var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
|
||||
|
||||
req.Success += res =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
tcs.SetCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
tcs.SetResult(res.ToBeatmapSet(Rulesets));
|
||||
};
|
||||
|
||||
req.Failure += e => tcs.SetException(e);
|
||||
|
||||
API.Queue(req);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
@ -17,8 +17,6 @@ using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osu.Game.Rulesets;
|
||||
@ -71,7 +69,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// <summary>
|
||||
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
|
||||
/// </summary>
|
||||
public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id);
|
||||
public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>.
|
||||
@ -85,15 +83,15 @@ namespace osu.Game.Online.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
protected IAPIProvider API { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
protected RulesetStore Rulesets { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
// Only exists for compatibility with old osu-server-spectator build.
|
||||
// Todo: Can be removed on 2021/02/26.
|
||||
private long defaultPlaylistItemId;
|
||||
@ -133,12 +131,12 @@ namespace osu.Game.Online.Multiplayer
|
||||
Debug.Assert(room.RoomID.Value != null);
|
||||
|
||||
// Join the server-side room.
|
||||
var joinedRoom = await JoinRoom(room.RoomID.Value.Value);
|
||||
var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false);
|
||||
Debug.Assert(joinedRoom != null);
|
||||
|
||||
// Populate users.
|
||||
Debug.Assert(joinedRoom.Users != null);
|
||||
await Task.WhenAll(joinedRoom.Users.Select(PopulateUser));
|
||||
await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false);
|
||||
|
||||
// Update the stored room (must be done on update thread for thread-safety).
|
||||
await scheduleAsync(() =>
|
||||
@ -146,11 +144,11 @@ namespace osu.Game.Online.Multiplayer
|
||||
Room = joinedRoom;
|
||||
apiRoom = room;
|
||||
defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0;
|
||||
}, cancellationSource.Token);
|
||||
}, cancellationSource.Token).ConfigureAwait(false);
|
||||
|
||||
// Update room settings.
|
||||
await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token);
|
||||
}, cancellationSource.Token);
|
||||
await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token).ConfigureAwait(false);
|
||||
}, cancellationSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -180,8 +178,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
return joinOrLeaveTaskChain.Add(async () =>
|
||||
{
|
||||
await scheduledReset;
|
||||
await LeaveRoomInternal();
|
||||
await scheduledReset.ConfigureAwait(false);
|
||||
await LeaveRoomInternal().ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
@ -239,11 +237,11 @@ namespace osu.Game.Online.Multiplayer
|
||||
switch (localUser.State)
|
||||
{
|
||||
case MultiplayerUserState.Idle:
|
||||
await ChangeState(MultiplayerUserState.Ready);
|
||||
await ChangeState(MultiplayerUserState.Ready).ConfigureAwait(false);
|
||||
return;
|
||||
|
||||
case MultiplayerUserState.Ready:
|
||||
await ChangeState(MultiplayerUserState.Idle);
|
||||
await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);
|
||||
return;
|
||||
|
||||
default:
|
||||
@ -309,7 +307,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
if (Room == null)
|
||||
return;
|
||||
|
||||
await PopulateUser(user);
|
||||
await PopulateUser(user).ConfigureAwait(false);
|
||||
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
@ -488,7 +486,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// Populates the <see cref="User"/> for a given <see cref="MultiplayerRoomUser"/>.
|
||||
/// </summary>
|
||||
/// <param name="multiplayerUser">The <see cref="MultiplayerRoomUser"/> to populate.</param>
|
||||
protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID);
|
||||
protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the local room settings with the given <see cref="MultiplayerRoomSettings"/>.
|
||||
@ -515,30 +513,26 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
RoomUpdated?.Invoke();
|
||||
|
||||
var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId);
|
||||
req.Success += res =>
|
||||
GetOnlineBeatmapSet(settings.BeatmapID, cancellationToken).ContinueWith(set => Schedule(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
updatePlaylist(settings, res);
|
||||
};
|
||||
|
||||
api.Queue(req);
|
||||
updatePlaylist(settings, set.Result);
|
||||
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||
}, cancellationToken);
|
||||
|
||||
private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet)
|
||||
private void updatePlaylist(MultiplayerRoomSettings settings, BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
if (Room == null || !Room.Settings.Equals(settings))
|
||||
return;
|
||||
|
||||
Debug.Assert(apiRoom != null);
|
||||
|
||||
var beatmapSet = onlineSet.ToBeatmapSet(rulesets);
|
||||
var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID);
|
||||
beatmap.MD5Hash = settings.BeatmapChecksum;
|
||||
|
||||
var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance();
|
||||
var ruleset = Rulesets.GetRuleset(settings.RulesetID).CreateInstance();
|
||||
var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset));
|
||||
var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset));
|
||||
|
||||
@ -568,6 +562,14 @@ namespace osu.Game.Online.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="BeatmapSetInfo"/> from an online source.
|
||||
/// </summary>
|
||||
/// <param name="beatmapId">The beatmap set ID.</param>
|
||||
/// <param name="cancellationToken">A token to cancel the request.</param>
|
||||
/// <returns>The <see cref="BeatmapSetInfo"/> retrieval task.</returns>
|
||||
protected abstract Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// For the provided user ID, update whether the user is included in <see cref="CurrentMatchPlayingUserIds"/>.
|
||||
/// </summary>
|
||||
|
@ -361,14 +361,6 @@ namespace osu.Game
|
||||
|
||||
PerformFromScreen(screen =>
|
||||
{
|
||||
// we might already be at song select, so a check is required before performing the load to solo.
|
||||
if (screen is MainMenu)
|
||||
menuScreen.LoadToSolo();
|
||||
|
||||
// we might even already be at the song
|
||||
if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true))
|
||||
return;
|
||||
|
||||
// Find beatmaps that match our predicate.
|
||||
var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList();
|
||||
|
||||
@ -381,9 +373,16 @@ namespace osu.Game
|
||||
?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value))
|
||||
?? beatmaps.First();
|
||||
|
||||
Ruleset.Value = selection.Ruleset;
|
||||
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
|
||||
}, validScreens: new[] { typeof(SongSelect) });
|
||||
if (screen is IHandlePresentBeatmap presentableScreen)
|
||||
{
|
||||
presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset);
|
||||
}
|
||||
else
|
||||
{
|
||||
Ruleset.Value = selection.Ruleset;
|
||||
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection);
|
||||
}
|
||||
}, validScreens: new[] { typeof(SongSelect), typeof(IHandlePresentBeatmap) });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -441,7 +440,7 @@ namespace osu.Game
|
||||
public override Task Import(params ImportTask[] imports)
|
||||
{
|
||||
// encapsulate task as we don't want to begin the import process until in a ready state.
|
||||
var importTask = new Task(async () => await base.Import(imports));
|
||||
var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false));
|
||||
|
||||
waitForReady(() => this, _ => importTask.Start());
|
||||
|
||||
@ -832,7 +831,7 @@ namespace osu.Game
|
||||
asyncLoadStream = Task.Run(async () =>
|
||||
{
|
||||
if (previousLoadStream != null)
|
||||
await previousLoadStream;
|
||||
await previousLoadStream.ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
@ -846,7 +845,7 @@ namespace osu.Game
|
||||
|
||||
// The delegate won't complete if OsuGame has been disposed in the meantime
|
||||
while (!IsDisposed && !del.Completed)
|
||||
await Task.Delay(10);
|
||||
await Task.Delay(10).ConfigureAwait(false);
|
||||
|
||||
// Either we're disposed or the load process has started successfully
|
||||
if (IsDisposed)
|
||||
@ -854,7 +853,7 @@ namespace osu.Game
|
||||
|
||||
Debug.Assert(task != null);
|
||||
|
||||
await task;
|
||||
await task.ConfigureAwait(false);
|
||||
|
||||
Logger.Log($"Loaded {component}!", level: LogLevel.Debug);
|
||||
}
|
||||
@ -881,13 +880,8 @@ namespace osu.Game
|
||||
switch (action)
|
||||
{
|
||||
case GlobalAction.ResetInputSettings:
|
||||
var sensitivity = frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity);
|
||||
|
||||
sensitivity.Disabled = false;
|
||||
sensitivity.Value = 1;
|
||||
sensitivity.Disabled = true;
|
||||
|
||||
frameworkConfig.Set(FrameworkSetting.IgnoredInputHandlers, string.Empty);
|
||||
frameworkConfig.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers).SetDefault();
|
||||
frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity).SetDefault();
|
||||
frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();
|
||||
return true;
|
||||
|
||||
|
@ -434,7 +434,7 @@ namespace osu.Game
|
||||
foreach (var importer in fileImporters)
|
||||
{
|
||||
if (importer.HandledExtensions.Contains(extension))
|
||||
await importer.Import(paths);
|
||||
await importer.Import(paths).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -445,7 +445,7 @@ namespace osu.Game
|
||||
{
|
||||
var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key));
|
||||
return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask;
|
||||
}));
|
||||
})).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public IEnumerable<string> HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions);
|
||||
|
@ -160,9 +160,9 @@ namespace osu.Game.Overlays
|
||||
tcs.SetException(e);
|
||||
};
|
||||
|
||||
await API.PerformAsync(req);
|
||||
await API.PerformAsync(req).ConfigureAwait(false);
|
||||
|
||||
await tcs.Task;
|
||||
return tcs.Task;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -244,7 +244,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
|
||||
return unsorted.OrderByDescending(u => u.LastVisit).ToList();
|
||||
|
||||
case UserSortCriteria.Rank:
|
||||
return unsorted.OrderByDescending(u => u.CurrentModeRank.HasValue).ThenBy(u => u.CurrentModeRank ?? 0).ToList();
|
||||
return unsorted.OrderByDescending(u => u.Statistics.GlobalRank.HasValue).ThenBy(u => u.Statistics.GlobalRank ?? 0).ToList();
|
||||
|
||||
case UserSortCriteria.Username:
|
||||
return unsorted.OrderBy(u => u.Username).ToList();
|
||||
|
42
osu.Game/Overlays/Dialog/ConfirmDialog.cs
Normal file
42
osu.Game/Overlays/Dialog/ConfirmDialog.cs
Normal file
@ -0,0 +1,42 @@
|
||||
// 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.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// A dialog which confirms a user action.
|
||||
/// </summary>
|
||||
public class ConfirmDialog : PopupDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct a new confirmation dialog.
|
||||
/// </summary>
|
||||
/// <param name="message">The description of the action to be displayed to the user.</param>
|
||||
/// <param name="onConfirm">An action to perform on confirmation.</param>
|
||||
/// <param name="onCancel">An optional action to perform on cancel.</param>
|
||||
public ConfirmDialog(string message, Action onConfirm, Action onCancel = null)
|
||||
{
|
||||
HeaderText = message;
|
||||
BodyText = "Last chance to turn back";
|
||||
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"Yes",
|
||||
Action = onConfirm
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"Cancel",
|
||||
Action = onCancel
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
protected override string Header => "Mouse";
|
||||
|
||||
private readonly BindableBool rawInputToggle = new BindableBool();
|
||||
private Bindable<double> sensitivityBindable = new BindableDouble();
|
||||
|
||||
private Bindable<double> configSensitivity;
|
||||
|
||||
private Bindable<double> localSensitivity;
|
||||
|
||||
private Bindable<string> ignoredInputHandlers;
|
||||
|
||||
private Bindable<WindowMode> windowMode;
|
||||
@ -26,12 +30,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager osuConfig, FrameworkConfigManager config)
|
||||
{
|
||||
var configSensitivity = config.GetBindable<double>(FrameworkSetting.CursorSensitivity);
|
||||
|
||||
// use local bindable to avoid changing enabled state of game host's bindable.
|
||||
sensitivityBindable = configSensitivity.GetUnboundCopy();
|
||||
configSensitivity.BindValueChanged(val => sensitivityBindable.Value = val.NewValue);
|
||||
sensitivityBindable.BindValueChanged(val => configSensitivity.Value = val.NewValue);
|
||||
configSensitivity = config.GetBindable<double>(FrameworkSetting.CursorSensitivity);
|
||||
localSensitivity = configSensitivity.GetUnboundCopy();
|
||||
|
||||
windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
|
||||
ignoredInputHandlers = config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers);
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
@ -43,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
new SensitivitySetting
|
||||
{
|
||||
LabelText = "Cursor sensitivity",
|
||||
Current = sensitivityBindable
|
||||
Current = localSensitivity
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
@ -66,14 +70,43 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
Current = osuConfig.GetBindable<bool>(OsuSetting.MouseDisableButtons)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
|
||||
windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1, true);
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
configSensitivity.BindValueChanged(val =>
|
||||
{
|
||||
var disabled = localSensitivity.Disabled;
|
||||
|
||||
localSensitivity.Disabled = false;
|
||||
localSensitivity.Value = val.NewValue;
|
||||
localSensitivity.Disabled = disabled;
|
||||
}, true);
|
||||
|
||||
localSensitivity.BindValueChanged(val => configSensitivity.Value = val.NewValue);
|
||||
|
||||
windowMode.BindValueChanged(mode =>
|
||||
{
|
||||
var isFullscreen = mode.NewValue == WindowMode.Fullscreen;
|
||||
|
||||
if (isFullscreen)
|
||||
{
|
||||
confineMouseModeSetting.Current.Disabled = true;
|
||||
confineMouseModeSetting.TooltipText = "Not applicable in full screen mode";
|
||||
}
|
||||
else
|
||||
{
|
||||
confineMouseModeSetting.Current.Disabled = false;
|
||||
confineMouseModeSetting.TooltipText = string.Empty;
|
||||
}
|
||||
}, true);
|
||||
|
||||
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
|
||||
{
|
||||
rawInputToggle.Disabled = true;
|
||||
sensitivityBindable.Disabled = true;
|
||||
localSensitivity.Disabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -86,12 +119,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
ignoredInputHandlers.Value = enabled.NewValue ? standard_mouse_handlers : raw_mouse_handler;
|
||||
};
|
||||
|
||||
ignoredInputHandlers = config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers);
|
||||
ignoredInputHandlers.ValueChanged += handler =>
|
||||
{
|
||||
bool raw = !handler.NewValue.Contains("Raw");
|
||||
rawInputToggle.Value = raw;
|
||||
sensitivityBindable.Disabled = !raw;
|
||||
localSensitivity.Disabled = !raw;
|
||||
};
|
||||
|
||||
ignoredInputHandlers.TriggerChange();
|
||||
|
@ -121,8 +121,10 @@ namespace osu.Game.Overlays.Settings
|
||||
labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1;
|
||||
}
|
||||
|
||||
private class RestoreDefaultValueButton : Container, IHasTooltip
|
||||
protected internal class RestoreDefaultValueButton : Container, IHasTooltip
|
||||
{
|
||||
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
|
||||
|
||||
private Bindable<T> bindable;
|
||||
|
||||
public Bindable<T> Bindable
|
||||
|
@ -5,11 +5,9 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -30,9 +28,6 @@ namespace osu.Game
|
||||
[Resolved]
|
||||
private DialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private OsuGame game { get; set; }
|
||||
|
||||
@ -90,7 +85,7 @@ namespace osu.Game
|
||||
var type = current.GetType();
|
||||
|
||||
// check if we are already at a valid target screen.
|
||||
if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled)
|
||||
if (validScreens.Any(t => t.IsAssignableFrom(type)))
|
||||
{
|
||||
finalAction(current);
|
||||
Cancel();
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
|
||||
private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
var skills = CreateSkills(beatmap);
|
||||
var skills = CreateSkills(beatmap, mods);
|
||||
|
||||
if (!beatmap.HitObjects.Any())
|
||||
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||
@ -202,7 +202,8 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// Creates the <see cref="Skill"/>s to calculate the difficulty of an <see cref="IBeatmap"/>.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param>
|
||||
/// <param name="mods">Mods to calculate difficulty with.</param>
|
||||
/// <returns>The <see cref="Skill"/>s.</returns>
|
||||
protected abstract Skill[] CreateSkills(IBeatmap beatmap);
|
||||
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods);
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Difficulty.Skills
|
||||
{
|
||||
@ -46,10 +47,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
||||
/// </summary>
|
||||
protected double CurrentStrain { get; private set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Mods for use in skill calculations.
|
||||
/// </summary>
|
||||
protected IReadOnlyList<Mod> Mods => mods;
|
||||
|
||||
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
|
||||
|
||||
private readonly List<double> strainPeaks = new List<double>();
|
||||
|
||||
private readonly Mod[] mods;
|
||||
|
||||
protected Skill(Mod[] mods)
|
||||
{
|
||||
this.mods = mods;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
|
||||
/// </summary>
|
||||
|
55
osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
Normal file
55
osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// 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;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Rulesets.Filter
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows for extending the beatmap filtering capabilities of song select (as implemented in <see cref="FilterCriteria"/>)
|
||||
/// with ruleset-specific criteria.
|
||||
/// </summary>
|
||||
public interface IRulesetFilterCriteria
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether the supplied <paramref name="beatmap"/> satisfies ruleset-specific custom criteria,
|
||||
/// in addition to the ones mandated by song select.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to test the criteria against.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the beatmap matches the ruleset-specific custom filtering criteria,
|
||||
/// <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
bool Matches(BeatmapInfo beatmap);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a single custom keyword criterion, given by the user via the song select search box.
|
||||
/// The format of the criterion is:
|
||||
/// <code>
|
||||
/// {key}{op}{value}
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For adding optional string criteria, <see cref="FilterCriteria.OptionalTextFilter"/> can be used for matching,
|
||||
/// along with <see cref="FilterQueryParser.TryUpdateCriteriaText"/> for parsing.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For adding numerical-type range criteria, <see cref="FilterCriteria.OptionalRange{T}"/> can be used for matching,
|
||||
/// along with <see cref="FilterQueryParser.TryUpdateCriteriaRange{T}(ref osu.Game.Screens.Select.FilterCriteria.OptionalRange{T},osu.Game.Screens.Select.Filter.Operator,string,FilterQueryParser.TryParseFunction{T})"/>
|
||||
/// and <see cref="float"/>- and <see cref="double"/>-typed overloads for parsing.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="key">The key (name) of the criterion.</param>
|
||||
/// <param name="op">The operator in the criterion.</param>
|
||||
/// <param name="value">The value of the criterion.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the keyword criterion is valid, <c>false</c> if it has been ignored.
|
||||
/// Valid criteria are stripped from <see cref="FilterCriteria.SearchText"/>,
|
||||
/// while ignored criteria are included in <see cref="FilterCriteria.SearchText"/>.
|
||||
/// </returns>
|
||||
bool TryParseCustomKeywordCriteria(string key, Operator op, string value);
|
||||
}
|
||||
}
|
@ -150,17 +150,13 @@ namespace osu.Game.Rulesets.Judgements
|
||||
}
|
||||
|
||||
if (JudgementBody.Drawable is IAnimatableJudgement animatable)
|
||||
{
|
||||
var drawableAnimation = (Drawable)animatable;
|
||||
|
||||
animatable.PlayAnimation();
|
||||
|
||||
// a derived version of DrawableJudgement may be proposing a lifetime.
|
||||
// if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
|
||||
double lastTransformTime = drawableAnimation.LatestTransformEndTime;
|
||||
if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
|
||||
LifetimeEnd = lastTransformTime;
|
||||
}
|
||||
// a derived version of DrawableJudgement may be proposing a lifetime.
|
||||
// if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime.
|
||||
double lastTransformTime = JudgementBody.Drawable.LatestTransformEndTime;
|
||||
if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd)
|
||||
LifetimeEnd = lastTransformTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,7 @@ using JetBrains.Annotations;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
@ -306,5 +307,11 @@ namespace osu.Game.Rulesets
|
||||
/// <param name="result">The result type to get the name for.</param>
|
||||
/// <returns>The display name.</returns>
|
||||
public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription();
|
||||
|
||||
/// <summary>
|
||||
/// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
|
||||
/// </summary>
|
||||
[CanBeNull]
|
||||
public virtual IRulesetFilterCriteria CreateRulesetFilterCriteria() => null;
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
~DrawableRulesetDependencies()
|
||||
{
|
||||
// required to potentially clean up sample store from audio hierarchy.
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Scoring
|
||||
{
|
||||
var score = lookup.ScoreInfo;
|
||||
|
||||
var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token);
|
||||
var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token).ConfigureAwait(false);
|
||||
|
||||
// Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
|
||||
if (attributes.Attributes == null)
|
||||
|
@ -13,6 +13,8 @@ namespace osu.Game.Screens
|
||||
{
|
||||
private readonly bool animateOnEnter;
|
||||
|
||||
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
|
||||
|
||||
protected BackgroundScreen(bool animateOnEnter = true)
|
||||
{
|
||||
this.animateOnEnter = animateOnEnter;
|
||||
|
23
osu.Game/Screens/IHandlePresentBeatmap.cs
Normal file
23
osu.Game/Screens/IHandlePresentBeatmap.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Screens
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes a screen which can handle beatmap / ruleset selection via local logic.
|
||||
/// This is used in the <see cref="OsuGame.PresentBeatmap"/> flow to handle cases which require custom logic,
|
||||
/// for instance, if a lease is held on the Beatmap.
|
||||
/// </summary>
|
||||
public interface IHandlePresentBeatmap
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked with a requested beatmap / ruleset for selection.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to be selected.</param>
|
||||
/// <param name="ruleset">The ruleset to be selected.</param>
|
||||
void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset);
|
||||
}
|
||||
}
|
@ -154,7 +154,7 @@ namespace osu.Game.Screens.Import
|
||||
|
||||
Task.Factory.StartNew(async () =>
|
||||
{
|
||||
await game.Import(path);
|
||||
await game.Import(path).ConfigureAwait(false);
|
||||
|
||||
// some files will be deleted after successful import, so we want to refresh the view.
|
||||
Schedule(() =>
|
||||
|
@ -172,18 +172,6 @@ namespace osu.Game.Screens.Menu
|
||||
return;
|
||||
}
|
||||
|
||||
// disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805.
|
||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS)
|
||||
{
|
||||
notifications?.Post(new SimpleNotification
|
||||
{
|
||||
Text = "Multiplayer is temporarily unavailable on iOS as we figure out some low level issues.",
|
||||
Icon = FontAwesome.Solid.AppleAlt,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
OnMultiplayer?.Invoke();
|
||||
}
|
||||
|
||||
|
@ -9,10 +9,15 @@ namespace osu.Game.Screens.Menu
|
||||
{
|
||||
public class ConfirmExitDialog : PopupDialog
|
||||
{
|
||||
public ConfirmExitDialog(Action confirm, Action cancel)
|
||||
/// <summary>
|
||||
/// Construct a new exit confirmation dialog.
|
||||
/// </summary>
|
||||
/// <param name="onConfirm">An action to perform on confirmation.</param>
|
||||
/// <param name="onCancel">An optional action to perform on cancel.</param>
|
||||
public ConfirmExitDialog(Action onConfirm, Action onCancel = null)
|
||||
{
|
||||
HeaderText = "Are you sure you want to exit?";
|
||||
BodyText = "Last chance to back out.";
|
||||
HeaderText = "Are you sure you want to exit osu!?";
|
||||
BodyText = "Last chance to turn back";
|
||||
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle;
|
||||
|
||||
@ -20,13 +25,13 @@ namespace osu.Game.Screens.Menu
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"Goodbye",
|
||||
Action = confirm
|
||||
Text = @"Let me out!",
|
||||
Action = onConfirm
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = @"Just a little more",
|
||||
Action = cancel
|
||||
Text = @"Just a little more...",
|
||||
Action = onCancel
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ namespace osu.Game.Screens.Menu
|
||||
|
||||
rulesets.Hide();
|
||||
lazerLogo.Hide();
|
||||
background.Hide();
|
||||
background.ApplyToBackground(b => b.Hide());
|
||||
|
||||
using (BeginAbsoluteSequence(0, true))
|
||||
{
|
||||
@ -231,7 +231,8 @@ namespace osu.Game.Screens.Menu
|
||||
lazerLogo.Dispose(); // explicit disposal as we are pushing a new screen and the expire may not get run.
|
||||
|
||||
logo.FadeIn();
|
||||
background.FadeIn();
|
||||
|
||||
background.ApplyToBackground(b => b.Show());
|
||||
|
||||
game.Add(new GameWideFlash());
|
||||
|
||||
|
@ -9,12 +9,14 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Backgrounds;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
@ -23,7 +25,7 @@ using osu.Game.Screens.Select;
|
||||
|
||||
namespace osu.Game.Screens.Menu
|
||||
{
|
||||
public class MainMenu : OsuScreen
|
||||
public class MainMenu : OsuScreen, IHandlePresentBeatmap
|
||||
{
|
||||
public const float FADE_IN_DURATION = 300;
|
||||
|
||||
@ -104,7 +106,7 @@ namespace osu.Game.Screens.Menu
|
||||
Beatmap.SetDefault();
|
||||
this.Push(new Editor());
|
||||
},
|
||||
OnSolo = onSolo,
|
||||
OnSolo = loadSoloSongSelect,
|
||||
OnMultiplayer = () => this.Push(new Multiplayer()),
|
||||
OnPlaylists = () => this.Push(new Playlists()),
|
||||
OnExit = confirmAndExit,
|
||||
@ -160,9 +162,7 @@ namespace osu.Game.Screens.Menu
|
||||
LoadComponentAsync(songSelect = new PlaySongSelect());
|
||||
}
|
||||
|
||||
public void LoadToSolo() => Schedule(onSolo);
|
||||
|
||||
private void onSolo() => this.Push(consumeSongSelect());
|
||||
private void loadSoloSongSelect() => this.Push(consumeSongSelect());
|
||||
|
||||
private Screen consumeSongSelect()
|
||||
{
|
||||
@ -289,5 +289,13 @@ namespace osu.Game.Screens.Menu
|
||||
this.FadeOut(3000);
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
||||
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
|
||||
{
|
||||
Beatmap.Value = beatmap;
|
||||
Ruleset.Value = ruleset;
|
||||
|
||||
Schedule(loadSoloSongSelect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,11 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Select;
|
||||
|
||||
@ -19,6 +21,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
private LoadingLayer loadingLayer;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new instance of multiplayer song select.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">An optional initial beatmap selection to perform.</param>
|
||||
/// <param name="ruleset">An optional initial ruleset selection to perform.</param>
|
||||
public MultiplayerMatchSongSelect(WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
|
||||
{
|
||||
if (beatmap != null || ruleset != null)
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
if (beatmap != null) Beatmap.Value = beatmap;
|
||||
if (ruleset != null) Ruleset.Value = ruleset;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
@ -12,9 +12,13 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
@ -29,7 +33,7 @@ using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.Pa
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
[Cached]
|
||||
public class MultiplayerMatchSubScreen : RoomSubScreen
|
||||
public class MultiplayerMatchSubScreen : RoomSubScreen, IHandlePresentBeatmap
|
||||
{
|
||||
public override string Title { get; }
|
||||
|
||||
@ -279,14 +283,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList();
|
||||
}
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private DialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
private bool exitConfirmed;
|
||||
|
||||
public override bool OnBackButton()
|
||||
{
|
||||
if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible)
|
||||
if (client.Room == null)
|
||||
{
|
||||
// room has not been created yet; exit immediately.
|
||||
return base.OnBackButton();
|
||||
}
|
||||
|
||||
if (settingsOverlay.State.Value == Visibility.Visible)
|
||||
{
|
||||
settingsOverlay.Hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!exitConfirmed && dialogOverlay != null)
|
||||
{
|
||||
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
|
||||
{
|
||||
exitConfirmed = true;
|
||||
this.Exit();
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnBackButton();
|
||||
}
|
||||
|
||||
@ -394,5 +420,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
modSettingChangeTracker?.Dispose();
|
||||
}
|
||||
|
||||
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
|
||||
{
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
if (!client.IsHost)
|
||||
{
|
||||
// todo: should handle this when the request queue is implemented.
|
||||
// if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap
|
||||
// flow may need to change to support an "unable to present" return value.
|
||||
return;
|
||||
}
|
||||
|
||||
this.Push(new MultiplayerMatchSongSelect(beatmap, ruleset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,13 +137,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
|
||||
protected override async Task SubmitScore(Score score)
|
||||
{
|
||||
await base.SubmitScore(score);
|
||||
await base.SubmitScore(score).ConfigureAwait(false);
|
||||
|
||||
await client.ChangeState(MultiplayerUserState.FinishedPlay);
|
||||
await client.ChangeState(MultiplayerUserState.FinishedPlay).ConfigureAwait(false);
|
||||
|
||||
// Await up to 60 seconds for results to become available (6 api request timeouts).
|
||||
// This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur.
|
||||
await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60)));
|
||||
await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score)
|
||||
|
@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
|
||||
protected override async Task SubmitScore(Score score)
|
||||
{
|
||||
await base.SubmitScore(score);
|
||||
await base.SubmitScore(score).ConfigureAwait(false);
|
||||
|
||||
Debug.Assert(Token != null);
|
||||
|
||||
@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
};
|
||||
|
||||
api.Queue(request);
|
||||
await tcs.Task;
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -84,14 +84,15 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
InternalChildren = new[]
|
||||
{
|
||||
displayedCountSpriteText = createSpriteText().With(s =>
|
||||
{
|
||||
s.Alpha = 0;
|
||||
}),
|
||||
popOutCount = createSpriteText().With(s =>
|
||||
{
|
||||
s.Alpha = 0;
|
||||
s.Margin = new MarginPadding(0.05f);
|
||||
s.Blending = BlendingParameters.Additive;
|
||||
}),
|
||||
displayedCountSpriteText = createSpriteText().With(s =>
|
||||
{
|
||||
s.Alpha = 0;
|
||||
})
|
||||
};
|
||||
|
||||
|
@ -592,7 +592,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
try
|
||||
{
|
||||
await SubmitScore(score);
|
||||
await SubmitScore(score).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -601,7 +601,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
try
|
||||
{
|
||||
await ImportScore(score);
|
||||
await ImportScore(score).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -73,6 +73,9 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (match)
|
||||
match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true;
|
||||
|
||||
if (match && criteria.RulesetCriteria != null)
|
||||
match &= criteria.RulesetCriteria.Matches(Beatmap);
|
||||
|
||||
Filtered.Value = !match;
|
||||
}
|
||||
|
||||
|
17
osu.Game/Screens/Select/Filter/Operator.cs
Normal file
17
osu.Game/Screens/Select/Filter/Operator.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Screens.Select.Filter
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines logical operators that can be used in the song select search box keyword filters.
|
||||
/// </summary>
|
||||
public enum Operator
|
||||
{
|
||||
Less,
|
||||
LessOrEqual,
|
||||
Equal,
|
||||
GreaterOrEqual,
|
||||
Greater
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -36,6 +37,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
public FilterCriteria CreateCriteria()
|
||||
{
|
||||
Debug.Assert(ruleset.Value.ID != null);
|
||||
|
||||
var query = searchTextBox.Text;
|
||||
|
||||
var criteria = new FilterCriteria
|
||||
@ -53,6 +56,8 @@ namespace osu.Game.Screens.Select
|
||||
if (!maximumStars.IsDefault)
|
||||
criteria.UserStarDifficulty.Max = maximumStars.Value;
|
||||
|
||||
criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria();
|
||||
|
||||
FilterQueryParser.ApplyQueries(criteria, query);
|
||||
return criteria;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using JetBrains.Annotations;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Collections;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
@ -69,6 +70,9 @@ namespace osu.Game.Screens.Select
|
||||
[CanBeNull]
|
||||
public BeatmapCollection Collection;
|
||||
|
||||
[CanBeNull]
|
||||
public IRulesetFilterCriteria RulesetCriteria { get; set; }
|
||||
|
||||
public struct OptionalRange<T> : IEquatable<OptionalRange<T>>
|
||||
where T : struct
|
||||
{
|
||||
|
@ -5,13 +5,17 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
internal static class FilterQueryParser
|
||||
/// <summary>
|
||||
/// Utility class used for parsing song select filter queries entered via the search box.
|
||||
/// </summary>
|
||||
public static class FilterQueryParser
|
||||
{
|
||||
private static readonly Regex query_syntax_regex = new Regex(
|
||||
@"\b(?<key>stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\S*))",
|
||||
@"\b(?<key>\w+)(?<op>(:|=|(>|<)(:|=)?))(?<value>("".*"")|(\S*))",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
internal static void ApplyQueries(FilterCriteria criteria, string query)
|
||||
@ -19,62 +23,81 @@ namespace osu.Game.Screens.Select
|
||||
foreach (Match match in query_syntax_regex.Matches(query))
|
||||
{
|
||||
var key = match.Groups["key"].Value.ToLower();
|
||||
var op = match.Groups["op"].Value;
|
||||
var op = parseOperator(match.Groups["op"].Value);
|
||||
var value = match.Groups["value"].Value;
|
||||
|
||||
parseKeywordCriteria(criteria, key, value, op);
|
||||
|
||||
query = query.Replace(match.ToString(), "");
|
||||
if (tryParseKeywordCriteria(criteria, key, value, op))
|
||||
query = query.Replace(match.ToString(), "");
|
||||
}
|
||||
|
||||
criteria.SearchText = query;
|
||||
}
|
||||
|
||||
private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
|
||||
private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, Operator op)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "stars" when parseFloatWithPoint(value, out var stars):
|
||||
updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
|
||||
break;
|
||||
case "stars":
|
||||
return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2);
|
||||
|
||||
case "ar" when parseFloatWithPoint(value, out var ar):
|
||||
updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
|
||||
break;
|
||||
case "ar":
|
||||
return TryUpdateCriteriaRange(ref criteria.ApproachRate, op, value);
|
||||
|
||||
case "dr" when parseFloatWithPoint(value, out var dr):
|
||||
case "hp" when parseFloatWithPoint(value, out dr):
|
||||
updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
|
||||
break;
|
||||
case "dr":
|
||||
case "hp":
|
||||
return TryUpdateCriteriaRange(ref criteria.DrainRate, op, value);
|
||||
|
||||
case "cs" when parseFloatWithPoint(value, out var cs):
|
||||
updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
|
||||
break;
|
||||
case "cs":
|
||||
return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value);
|
||||
|
||||
case "bpm" when parseDoubleWithPoint(value, out var bpm):
|
||||
updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
|
||||
break;
|
||||
case "bpm":
|
||||
return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2);
|
||||
|
||||
case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
|
||||
var scale = getLengthScale(value);
|
||||
updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
|
||||
break;
|
||||
case "length":
|
||||
return tryUpdateLengthRange(criteria, op, value);
|
||||
|
||||
case "divisor" when parseInt(value, out var divisor):
|
||||
updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
|
||||
break;
|
||||
case "divisor":
|
||||
return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt);
|
||||
|
||||
case "status" when Enum.TryParse<BeatmapSetOnlineStatus>(value, true, out var statusValue):
|
||||
updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
|
||||
break;
|
||||
case "status":
|
||||
return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value,
|
||||
(string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val));
|
||||
|
||||
case "creator":
|
||||
updateCriteriaText(ref criteria.Creator, op, value);
|
||||
break;
|
||||
return TryUpdateCriteriaText(ref criteria.Creator, op, value);
|
||||
|
||||
case "artist":
|
||||
updateCriteriaText(ref criteria.Artist, op, value);
|
||||
break;
|
||||
return TryUpdateCriteriaText(ref criteria.Artist, op, value);
|
||||
|
||||
default:
|
||||
return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Operator parseOperator(string value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case "=":
|
||||
case ":":
|
||||
return Operator.Equal;
|
||||
|
||||
case "<":
|
||||
return Operator.Less;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
return Operator.LessOrEqual;
|
||||
|
||||
case ">":
|
||||
return Operator.Greater;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
return Operator.GreaterOrEqual;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported operator {value}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,129 +107,203 @@ namespace osu.Game.Screens.Select
|
||||
value.EndsWith('m') ? 60000 :
|
||||
value.EndsWith('h') ? 3600000 : 1000;
|
||||
|
||||
private static bool parseFloatWithPoint(string value, out float result) =>
|
||||
private static bool tryParseFloatWithPoint(string value, out float result) =>
|
||||
float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool parseDoubleWithPoint(string value, out double result) =>
|
||||
private static bool tryParseDoubleWithPoint(string value, out double result) =>
|
||||
double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static bool parseInt(string value, out int result) =>
|
||||
private static bool tryParseInt(string value, out int result) =>
|
||||
int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
|
||||
|
||||
private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value)
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter with the specified <paramref name="op"/> and textual <paramref name="value"/>.
|
||||
/// If the value indicates a valid textual filter, the function returns <c>true</c> and the resulting data is stored into
|
||||
/// <paramref name="textFilter"/>.
|
||||
/// </summary>
|
||||
/// <param name="textFilter">The <see cref="FilterCriteria.OptionalTextFilter"/> to store the parsed data into, if successful.</param>
|
||||
/// <param name="op">
|
||||
/// The operator for the keyword filter.
|
||||
/// Only <see cref="Operator.Equal"/> is valid for textual filters.
|
||||
/// </param>
|
||||
/// <param name="value">The value of the keyword filter.</param>
|
||||
public static bool TryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
textFilter.SearchTerm = value.Trim('"');
|
||||
break;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, string op, float value, float tolerance = 0.05f)
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter of type <see cref="float"/>
|
||||
/// from the specified <paramref name="op"/> and <paramref name="val"/>.
|
||||
/// If <paramref name="val"/> can be parsed as a <see cref="float"/>, the function returns <c>true</c>
|
||||
/// and the resulting range constraint is stored into <paramref name="range"/>.
|
||||
/// </summary>
|
||||
/// <param name="range">
|
||||
/// The <see cref="float"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/>
|
||||
/// to store the parsed data into, if successful.
|
||||
/// </param>
|
||||
/// <param name="op">The operator for the keyword filter.</param>
|
||||
/// <param name="val">The value of the keyword filter.</param>
|
||||
/// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param>
|
||||
public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, string val, float tolerance = 0.05f)
|
||||
=> tryParseFloatWithPoint(val, out float value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
|
||||
|
||||
private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<float> range, Operator op, float value, float tolerance = 0.05f)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
case Operator.Greater:
|
||||
range.Min = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
case Operator.GreaterOrEqual:
|
||||
range.Min = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
case Operator.Less:
|
||||
range.Max = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
case Operator.LessOrEqual:
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, string op, double value, double tolerance = 0.05)
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter of type <see cref="double"/>
|
||||
/// from the specified <paramref name="op"/> and <paramref name="val"/>.
|
||||
/// If <paramref name="val"/> can be parsed as a <see cref="double"/>, the function returns <c>true</c>
|
||||
/// and the resulting range constraint is stored into <paramref name="range"/>.
|
||||
/// </summary>
|
||||
/// <param name="range">
|
||||
/// The <see cref="double"/>-typed <see cref="FilterCriteria.OptionalRange{T}"/>
|
||||
/// to store the parsed data into, if successful.
|
||||
/// </param>
|
||||
/// <param name="op">The operator for the keyword filter.</param>
|
||||
/// <param name="val">The value of the keyword filter.</param>
|
||||
/// <param name="tolerance">Allowed tolerance of the parsed range boundary value.</param>
|
||||
public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, string val, double tolerance = 0.05)
|
||||
=> tryParseDoubleWithPoint(val, out double value) && tryUpdateCriteriaRange(ref range, op, value, tolerance);
|
||||
|
||||
private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange<double> range, Operator op, double value, double tolerance = 0.05)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
range.Min = value - tolerance;
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
case Operator.Greater:
|
||||
range.Min = value + tolerance;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
case Operator.GreaterOrEqual:
|
||||
range.Min = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
case Operator.Less:
|
||||
range.Max = value - tolerance;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
case Operator.LessOrEqual:
|
||||
range.Max = value + tolerance;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void updateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, string op, T value)
|
||||
/// <summary>
|
||||
/// Used to determine whether the string value <paramref name="val"/> can be converted to type <typeparamref name="T"/>.
|
||||
/// If conversion can be performed, the delegate returns <c>true</c>
|
||||
/// and the conversion result is returned in the <c>out</c> parameter <paramref name="parsed"/>.
|
||||
/// </summary>
|
||||
/// <param name="val">The string value to attempt parsing for.</param>
|
||||
/// <param name="parsed">The parsed value, if conversion is possible.</param>
|
||||
public delegate bool TryParseFunction<T>(string val, out T parsed);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse a keyword filter of type <typeparamref name="T"/>,
|
||||
/// from the specified <paramref name="op"/> and <paramref name="val"/>.
|
||||
/// If <paramref name="val"/> can be parsed into <typeparamref name="T"/> using <paramref name="parseFunction"/>, the function returns <c>true</c>
|
||||
/// and the resulting range constraint is stored into <paramref name="range"/>.
|
||||
/// </summary>
|
||||
/// <param name="range">The <see cref="FilterCriteria.OptionalRange{T}"/> to store the parsed data into, if successful.</param>
|
||||
/// <param name="op">The operator for the keyword filter.</param>
|
||||
/// <param name="val">The value of the keyword filter.</param>
|
||||
/// <param name="parseFunction">Function used to determine if <paramref name="val"/> can be converted to type <typeparamref name="T"/>.</param>
|
||||
public static bool TryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, string val, TryParseFunction<T> parseFunction)
|
||||
where T : struct
|
||||
=> parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted);
|
||||
|
||||
private static bool tryUpdateCriteriaRange<T>(ref FilterCriteria.OptionalRange<T> range, Operator op, T value)
|
||||
where T : struct
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
default:
|
||||
return;
|
||||
return false;
|
||||
|
||||
case "=":
|
||||
case ":":
|
||||
case Operator.Equal:
|
||||
range.IsLowerInclusive = range.IsUpperInclusive = true;
|
||||
range.Min = value;
|
||||
range.Max = value;
|
||||
break;
|
||||
|
||||
case ">":
|
||||
case Operator.Greater:
|
||||
range.IsLowerInclusive = false;
|
||||
range.Min = value;
|
||||
break;
|
||||
|
||||
case ">=":
|
||||
case ">:":
|
||||
case Operator.GreaterOrEqual:
|
||||
range.IsLowerInclusive = true;
|
||||
range.Min = value;
|
||||
break;
|
||||
|
||||
case "<":
|
||||
case Operator.Less:
|
||||
range.IsUpperInclusive = false;
|
||||
range.Max = value;
|
||||
break;
|
||||
|
||||
case "<=":
|
||||
case "<:":
|
||||
case Operator.LessOrEqual:
|
||||
range.IsUpperInclusive = true;
|
||||
range.Max = value;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool tryUpdateLengthRange(FilterCriteria criteria, Operator op, string val)
|
||||
{
|
||||
if (!tryParseDoubleWithPoint(val.TrimEnd('m', 's', 'h'), out var length))
|
||||
return false;
|
||||
|
||||
var scale = getLengthScale(val);
|
||||
return tryUpdateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
~Skin()
|
||||
{
|
||||
// required to potentially clean up sample store from audio hierarchy.
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
|
@ -120,7 +120,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await base.Populate(model, archive, cancellationToken);
|
||||
await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (model.Name?.Contains(".osk") == true)
|
||||
populateMetadata(model);
|
||||
|
@ -45,11 +45,30 @@ namespace osu.Game.Storyboards
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the earliest visible time. Will be null unless this group's first <see cref="Alpha"/> command has a start value of zero.
|
||||
/// </summary>
|
||||
public double? EarliestDisplayedTime
|
||||
{
|
||||
get
|
||||
{
|
||||
var first = Alpha.Commands.FirstOrDefault();
|
||||
|
||||
return first?.StartValue == 0 ? first.StartTime : (double?)null;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public double CommandsStartTime
|
||||
{
|
||||
get
|
||||
{
|
||||
// if the first alpha command starts at zero it should be given priority over anything else.
|
||||
// this is due to it creating a state where the target is not present before that time, causing any other events to not be visible.
|
||||
var earliestDisplay = EarliestDisplayedTime;
|
||||
if (earliestDisplay != null)
|
||||
return earliestDisplay.Value;
|
||||
|
||||
double min = double.MaxValue;
|
||||
|
||||
for (int i = 0; i < timelines.Length; i++)
|
||||
|
@ -24,13 +24,46 @@ namespace osu.Game.Storyboards
|
||||
|
||||
public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup();
|
||||
|
||||
public double StartTime => Math.Min(
|
||||
TimelineGroup.HasCommands ? TimelineGroup.CommandsStartTime : double.MaxValue,
|
||||
loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Min(l => l.StartTime) : double.MaxValue);
|
||||
public double StartTime
|
||||
{
|
||||
get
|
||||
{
|
||||
// check for presence affecting commands as an initial pass.
|
||||
double earliestStartTime = TimelineGroup.EarliestDisplayedTime ?? double.MaxValue;
|
||||
|
||||
public double EndTime => Math.Max(
|
||||
TimelineGroup.HasCommands ? TimelineGroup.CommandsEndTime : double.MinValue,
|
||||
loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Max(l => l.EndTime) : double.MinValue);
|
||||
foreach (var l in loops)
|
||||
{
|
||||
if (!(l.EarliestDisplayedTime is double lEarliest))
|
||||
continue;
|
||||
|
||||
earliestStartTime = Math.Min(earliestStartTime, lEarliest);
|
||||
}
|
||||
|
||||
if (earliestStartTime < double.MaxValue)
|
||||
return earliestStartTime;
|
||||
|
||||
// if an alpha-affecting command was not found, use the earliest of any command.
|
||||
earliestStartTime = TimelineGroup.StartTime;
|
||||
|
||||
foreach (var l in loops)
|
||||
earliestStartTime = Math.Min(earliestStartTime, l.StartTime);
|
||||
|
||||
return earliestStartTime;
|
||||
}
|
||||
}
|
||||
|
||||
public double EndTime
|
||||
{
|
||||
get
|
||||
{
|
||||
double latestEndTime = TimelineGroup.EndTime;
|
||||
|
||||
foreach (var l in loops)
|
||||
latestEndTime = Math.Max(latestEndTime, l.EndTime);
|
||||
|
||||
return latestEndTime;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands);
|
||||
|
||||
|
@ -7,8 +7,10 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@ -48,7 +50,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
RoomManager.Schedule(() => RoomManager.PartRoom());
|
||||
|
||||
if (joinRoom)
|
||||
{
|
||||
Room.Name.Value = "test name";
|
||||
Room.Playlist.Add(new PlaylistItem
|
||||
{
|
||||
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
|
||||
Ruleset = { Value = Ruleset.Value }
|
||||
});
|
||||
|
||||
RoomManager.Schedule(() => RoomManager.CreateRoom(Room));
|
||||
}
|
||||
});
|
||||
|
||||
public override void SetUpSteps()
|
||||
|
@ -3,12 +3,15 @@
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
@ -25,6 +28,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
private readonly TestMultiplayerRoomManager roomManager;
|
||||
|
||||
public TestMultiplayerClient(TestMultiplayerRoomManager roomManager)
|
||||
{
|
||||
this.roomManager = roomManager;
|
||||
}
|
||||
|
||||
public void Connect() => isConnected.Value = true;
|
||||
|
||||
public void Disconnect() => isConnected.Value = false;
|
||||
@ -89,13 +102,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
protected override Task<MultiplayerRoom> JoinRoom(long roomId)
|
||||
{
|
||||
var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value };
|
||||
var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId);
|
||||
|
||||
var room = new MultiplayerRoom(roomId);
|
||||
room.Users.Add(user);
|
||||
var user = new MultiplayerRoomUser(api.LocalUser.Value.Id)
|
||||
{
|
||||
User = api.LocalUser.Value
|
||||
};
|
||||
|
||||
if (room.Users.Count == 1)
|
||||
room.Host = user;
|
||||
var room = new MultiplayerRoom(roomId)
|
||||
{
|
||||
Settings =
|
||||
{
|
||||
Name = apiRoom.Name.Value,
|
||||
BeatmapID = apiRoom.Playlist.Last().BeatmapID,
|
||||
RulesetID = apiRoom.Playlist.Last().RulesetID,
|
||||
BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash,
|
||||
RequiredMods = apiRoom.Playlist.Last().RequiredMods.Select(m => new APIMod(m)).ToArray(),
|
||||
AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(),
|
||||
PlaylistItemId = apiRoom.Playlist.Last().ID
|
||||
},
|
||||
Users = { user },
|
||||
Host = user
|
||||
};
|
||||
|
||||
return Task.FromResult(room);
|
||||
}
|
||||
@ -108,7 +136,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
Debug.Assert(Room != null);
|
||||
|
||||
await ((IMultiplayerClient)this).SettingsChanged(settings);
|
||||
await ((IMultiplayerClient)this).SettingsChanged(settings).ConfigureAwait(false);
|
||||
|
||||
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
||||
ChangeUserState(user.UserID, MultiplayerUserState.Idle);
|
||||
@ -150,5 +178,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
return ((IMultiplayerClient)this).LoadRequested();
|
||||
}
|
||||
|
||||
protected override Task<BeatmapSetInfo> GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
Debug.Assert(Room != null);
|
||||
|
||||
var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == Room.RoomID);
|
||||
var set = apiRoom.Playlist.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet
|
||||
?? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId)?.BeatmapSet;
|
||||
|
||||
if (set == null)
|
||||
throw new InvalidOperationException("Beatmap not found.");
|
||||
|
||||
return Task.FromResult(set);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user