1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 05:42:56 +08:00

Merge branch 'speedpp' of https://github.com/mrowswares/osu into speedpp

This commit is contained in:
apollo-dw 2021-09-13 14:51:19 +01:00
commit 68050a4073
164 changed files with 2258 additions and 1011 deletions

163
.github/workflows/test-diffcalc.yml vendored Normal file
View File

@ -0,0 +1,163 @@
# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
# Usage:
# !pp check 0 | Runs only the osu! ruleset.
# !pp check 0 2 | Runs only the osu! and catch rulesets.
#
name: Diffcalc Consistency Checks
on:
issue_comment:
types: [ created ]
env:
DB_USER: root
DB_HOST: 127.0.0.1
CONCURRENCY: 4
ALLOW_DOWNLOAD: 1
SAVE_DOWNLOADED: 1
jobs:
diffcalc:
name: Diffcalc
runs-on: ubuntu-latest
continue-on-error: true
if: |
${{ github.event.issue.pull_request }} &&
contains(github.event.comment.body, '!pp check') &&
${{ github.event.comment.author_association == 'MEMBER' }}
strategy:
fail-fast: false
matrix:
ruleset:
- { name: osu, id: 0 }
- { name: taiko, id: 1 }
- { name: catch, id: 2 }
- { name: mania, id: 3 }
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Verify ruleset
if: contains(github.event.comment.body, matrix.ruleset.id) == false
run: |
echo "${{ github.event.comment.body }} doesn't contain ${{ matrix.ruleset.id }}"
exit 1
- name: Verify MySQL connection from host
run: |
sudo apt-get install -y mysql-client
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "SHOW DATABASES"
- name: Create directory structure
run: |
mkdir -p $GITHUB_WORKSPACE/master/
mkdir -p $GITHUB_WORKSPACE/pr/
# Checkout osu
- name: Checkout osu (master)
uses: actions/checkout@v2
with:
repository: ppy/osu
path: 'master/osu'
- name: Checkout osu (pr)
uses: actions/checkout@v2
with:
path: 'pr/osu'
# Checkout osu-difficulty-calculator
- name: Checkout osu-difficulty-calculator (master)
uses: actions/checkout@v2
with:
repository: ppy/osu-difficulty-calculator
path: 'master/osu-difficulty-calculator'
- name: Checkout osu-difficulty-calculator (pr)
uses: actions/checkout@v2
with:
repository: ppy/osu-difficulty-calculator
path: 'pr/osu-difficulty-calculator'
- name: Install .NET 5.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "5.0.x"
# Sanity checks to make sure diffcalc is not run when incompatible.
- name: Build diffcalc (master)
run: |
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator
./UseLocalOsu.sh
dotnet build
- name: Build diffcalc (pr)
run: |
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator
./UseLocalOsu.sh
dotnet build
# Initial data imports
- name: Download + import data
run: |
PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
# Set env variable for further steps.
echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV
cd $GITHUB_WORKSPACE
wget https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2
wget https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2
tar -xf $PERFORMANCE_DATA_NAME.tar.bz2
tar -xf $BEATMAPS_DATA_NAME.tar.bz2
cd $GITHUB_WORKSPACE/$PERFORMANCE_DATA_NAME
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_master"
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_pr"
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_master
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_pr
# Run diffcalc
- name: Run diffcalc (master)
env:
DB_NAME: osu_master
run: |
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
- name: Run diffcalc (pr)
env:
DB_NAME: osu_pr
run: |
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
# Print diffs
- name: Print diffs
run: |
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "
SELECT
m.beatmap_id,
m.mods,
m.diff_unified as 'sr_master',
p.diff_unified as 'sr_pr',
(p.diff_unified - m.diff_unified) as 'diff'
FROM osu_master.osu_beatmap_difficulty m
JOIN osu_pr.osu_beatmap_difficulty p
ON m.beatmap_id = p.beatmap_id
AND m.mode = p.mode
AND m.mods = p.mods
WHERE abs(m.diff_unified - p.diff_unified) > 0.1
ORDER BY abs(m.diff_unified - p.diff_unified)
DESC
LIMIT 10000;"
# Todo: Run ppcalc

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.827.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.828.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.830.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
</ItemGroup>

View File

@ -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.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -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.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Mania.Edit.Setup
{
public class ManiaSetupSection : RulesetSetupSection
{
private LabelledSwitchButton specialStyle;
public ManiaSetupSection()
: base(new ManiaRuleset().RulesetInfo)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
specialStyle = new LabelledSwitchButton
{
Label = "Use special (N+1) style",
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 5k (4+1) or 8key (7+1) configurations.",
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
specialStyle.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateBeatmap()
{
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
}
}
}

View File

@ -27,11 +27,13 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.Edit.Setup;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mania.Skinning.Legacy;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania
@ -390,6 +392,8 @@ namespace osu.Game.Rulesets.Mania
{
return new ManiaFilterCriteria();
}
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
}
public enum PlayfieldType

View File

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

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -19,6 +18,7 @@ using osuTK;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.UI
{
@ -28,12 +28,6 @@ namespace osu.Game.Rulesets.Mania.UI
public const float COLUMN_WIDTH = 80;
public const float SPECIAL_COLUMN_WIDTH = 70;
/// <summary>
/// For hitsounds played by this <see cref="Column"/> (i.e. not as a result of hitting a hitobject),
/// a certain number of samples are allowed to be played concurrently so that it feels better when spam-pressing the key.
/// </summary>
private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY;
/// <summary>
/// The index of this column as part of the whole playfield.
/// </summary>
@ -45,10 +39,10 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
private readonly OrderedHitPolicy hitPolicy;
private readonly Container<SkinnableSound> hitSounds;
public Container UnderlayElements => HitObjectArea.UnderlayElements;
private readonly GameplaySampleTriggerSource sampleTriggerSource;
public Column(int index)
{
Index = index;
@ -64,6 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new[]
{
hitExplosionPool = new DrawablePool<PoolableHitExplosion>(5),
sampleTriggerSource = new GameplaySampleTriggerSource(HitObjectContainer),
// For input purposes, the background is added at the highest depth, but is then proxied back below all other elements
background.CreateProxy(),
HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both },
@ -72,12 +67,6 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both
},
background,
hitSounds = new Container<SkinnableSound>
{
Name = "Column samples pool",
RelativeSizeAxes = Axes.Both,
Children = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray()
},
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
};
@ -133,29 +122,12 @@ namespace osu.Game.Rulesets.Mania.UI
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
}
private int nextHitSoundIndex;
public bool OnPressed(ManiaAction action)
{
if (action != Action.Value)
return false;
var nextObject =
HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
// fallback to non-alive objects to find next off-screen object
HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
HitObjectContainer.Objects.LastOrDefault();
if (nextObject is DrawableManiaHitObject maniaObject)
{
var hitSound = hitSounds[nextHitSoundIndex];
hitSound.Samples = maniaObject.GetGameplaySamples();
hitSound.Play();
nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds;
}
sampleTriggerSource.Play();
return true;
}

View File

@ -37,12 +37,11 @@ namespace osu.Game.Rulesets.Mania.UI
public override void PlayAnimation()
{
base.PlayAnimation();
switch (Result)
{
case HitResult.None:
case HitResult.Miss:
base.PlayAnimation();
break;
default:
@ -52,6 +51,8 @@ namespace osu.Game.Rulesets.Mania.UI
this.Delay(50)
.ScaleTo(0.75f, 250)
.FadeOut(200);
// osu!mania uses a custom fade length, so the base call is intentionally omitted.
break;
}
}

View File

@ -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.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Rulesets.Osu.Edit.Setup
{
public class OsuSetupSection : RulesetSetupSection
{
private LabelledSliderBar<float> stackLeniency;
public OsuSetupSection()
: base(new OsuRuleset().RulesetInfo)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new[]
{
stackLeniency = new LabelledSliderBar<float>
{
Label = "Stack Leniency",
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
{
Default = 0.7f,
MinValue = 0,
MaxValue = 1,
Precision = 0.1f
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
stackLeniency.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateBeatmap()
{
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
}
}
}

View File

@ -74,10 +74,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override void PlayAnimation()
{
base.PlayAnimation();
if (Result != HitResult.Miss)
JudgementText.ScaleTo(new Vector2(0.8f, 1)).Then().ScaleTo(new Vector2(1.2f, 1), 1800, Easing.OutQuint);
{
JudgementText
.ScaleTo(new Vector2(0.8f, 1))
.ScaleTo(new Vector2(1.2f, 1), 1800, Easing.OutQuint);
}
base.PlayAnimation();
}
}
}

View File

@ -30,9 +30,11 @@ using osu.Game.Skinning;
using System;
using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Osu.Edit.Setup;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu
@ -305,5 +307,7 @@ namespace osu.Game.Rulesets.Osu
}
};
}
public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();
}
}

View File

@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
pathVersion.BindValueChanged(_ => Refresh());
accentColour = drawableObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true);
accentColour.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
}
private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour)
=> AccentColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderTrackOverride)?.Value ?? defaultAccentColour;
protected virtual Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour) =>
skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderTrackOverride)?.Value ?? hitObjectAccentColour;
}
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
/// <summary>
/// A <see cref="SliderBody"/> which changes its curve depending on the snaking progress.
/// </summary>
public class SnakingSliderBody : SliderBody, ISliderProgress
public abstract class SnakingSliderBody : SliderBody, ISliderProgress
{
public readonly List<Vector2> CurrentCurve = new List<Vector2>();

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osu.Game.Utils;
using osuTK.Graphics;
@ -14,6 +15,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
protected override DrawableSliderPath CreateSliderPath() => new LegacyDrawableSliderPath();
protected override Color4 GetBodyAccentColour(ISkinSource skin, Color4 hitObjectAccentColour)
{
// legacy skins use a constant value for slider track alpha, regardless of the source colour.
return base.GetBodyAccentColour(skin, hitObjectAccentColour).Opacity(0.7f);
}
private class LegacyDrawableSliderPath : DrawableSliderPath
{
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
@ -22,8 +29,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// Roughly matches osu!stable's slider border portions.
=> base.CalculatedBorderPortion * 0.77f;
public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, 0.7f);
protected override Color4 ColourAt(float position)
{
float realBorderPortion = shadow_portion + CalculatedBorderPortion;

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Origin = Anchor.Centre,
Children = new Drawable[]
{
new TaikoPlayfield(new ControlPointInfo()),
new TaikoPlayfield(),
hoc = new ScrollingHitObjectContainer()
}
};
@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Origin = Anchor.Centre,
Children = new Drawable[]
{
new TaikoPlayfield(new ControlPointInfo()),
new TaikoPlayfield(),
hoc = new ScrollingHitObjectContainer()
}
};

View File

@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.UI;
using osuTK;
@ -17,6 +16,13 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[BackgroundDependencyLoader]
private void load()
{
var playfield = new TaikoPlayfield();
var beatmap = CreateWorkingBeatmap(new TaikoRuleset().RulesetInfo).GetPlayableBeatmap(new TaikoRuleset().RulesetInfo);
foreach (var h in beatmap.HitObjects)
playfield.Add(h);
SetContents(_ => new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{
RelativeSizeAxes = Axes.Both,
@ -25,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200),
Child = new InputDrum(new ControlPointInfo())
Child = new InputDrum(playfield.HitObjectContainer)
}
});
}

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Beatmap.Value.Track.Start();
});
AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield(new ControlPointInfo())
AddStep("Load playfield", () => SetContents(_ => new TaikoPlayfield
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,

View File

@ -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.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -1,104 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Audio
{
/// <summary>
/// Stores samples for the input drum.
/// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point.
/// </summary>
public class DrumSampleContainer : LifetimeManagementContainer
{
private readonly ControlPointInfo controlPoints;
private readonly Dictionary<double, DrumSample> mappings = new Dictionary<double, DrumSample>();
private readonly IBindableList<SampleControlPoint> samplePoints = new BindableList<SampleControlPoint>();
public DrumSampleContainer(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
}
[BackgroundDependencyLoader]
private void load()
{
samplePoints.BindTo(controlPoints.SamplePoints);
samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true);
}
private void recreateMappings()
{
mappings.Clear();
ClearInternal();
SampleControlPoint[] points = samplePoints.Count == 0
? new[] { controlPoints.SamplePointAt(double.MinValue) }
: samplePoints.ToArray();
for (int i = 0; i < points.Length; i++)
{
var samplePoint = points[i];
var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue;
var lifetimeEnd = i + 1 < points.Length ? points[i + 1].Time : double.MaxValue;
AddInternal(mappings[samplePoint.Time] = new DrumSample(samplePoint)
{
LifetimeStart = lifetimeStart,
LifetimeEnd = lifetimeEnd
});
}
}
public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
public class DrumSample : CompositeDrawable
{
public override bool RemoveWhenNotAlive => false;
public PausableSkinnableSound Centre { get; private set; }
public PausableSkinnableSound Rim { get; private set; }
private readonly SampleControlPoint samplePoint;
private Bindable<string> sampleBank;
private BindableNumber<int> sampleVolume;
public DrumSample(SampleControlPoint samplePoint)
{
this.samplePoint = samplePoint;
}
[BackgroundDependencyLoader]
private void load()
{
sampleBank = samplePoint.SampleBankBindable.GetBoundCopy();
sampleBank.BindValueChanged(_ => recreate());
sampleVolume = samplePoint.SampleVolumeBindable.GetBoundCopy();
sampleVolume.BindValueChanged(_ => recreate());
recreate();
}
private void recreate()
{
InternalChildren = new Drawable[]
{
Centre = new PausableSkinnableSound(samplePoint.GetSampleInfo()),
Rim = new PausableSkinnableSound(samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP))
};
}
}
}
}

View File

@ -18,12 +18,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
internal class TaikoBeatmapConverter : BeatmapConverter<TaikoHitObject>
{
/// <summary>
/// osu! is generally slower than taiko, so a factor is added to increase
/// speed. This must be used everywhere slider length or beat length is used.
/// </summary>
public const float LEGACY_VELOCITY_MULTIPLIER = 1.4f;
/// <summary>
/// Because swells are easier in taiko than spinners are in osu!,
/// legacy taiko multiplies a factor when converting the number of required hits.
@ -52,10 +46,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken)
{
// Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= LEGACY_VELOCITY_MULTIPLIER;
if (!(original.BeatmapInfo.BaseDifficulty is TaikoMutliplierAppliedDifficulty))
{
// Rewrite the beatmap info to add the slider velocity multiplier
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = new TaikoMutliplierAppliedDifficulty(original.BeatmapInfo.BaseDifficulty);
}
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original, cancellationToken);
@ -155,7 +151,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
int spans = (obj as IHasRepeats)?.SpanCount() ?? 1;
double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER;
double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
@ -194,5 +190,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}
protected override Beatmap<TaikoHitObject> CreateBeatmap() => new TaikoBeatmap();
private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty
{
public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty)
{
difficulty.CopyTo(this);
SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER;
}
}
}
}

View File

@ -6,10 +6,10 @@ using System;
using System.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Judgements;
using osuTK;
@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
double IHasDistance.Distance => Duration * Velocity;
SliderPath IHasPath.Path
=> new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER);
=> new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(1) }, ((IHasDistance)this).Distance / LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER);
#endregion
}

View File

@ -7,7 +7,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Taiko.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
using osuTK;
@ -111,7 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public readonly Sprite Centre;
[Resolved]
private DrumSampleContainer sampleContainer { get; set; }
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public LegacyHalfDrum(bool flipped)
{
@ -143,17 +144,16 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public bool OnPressed(TaikoAction action)
{
Drawable target = null;
var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction)
{
target = Centre;
drumSample.Centre?.Play();
sampleTriggerSource.Play(HitType.Centre);
}
else if (action == RimAction)
{
target = Rim;
drumSample.Rim?.Play();
sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)

View File

@ -3,6 +3,7 @@
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.UI
{
@ -16,5 +17,26 @@ namespace osu.Game.Rulesets.Taiko.UI
this.MoveToY(-100, 500);
base.ApplyHitAnimations();
}
protected override Drawable CreateDefaultJudgement(HitResult result) => new TaikoJudgementPiece(result);
private class TaikoJudgementPiece : DefaultJudgementPiece
{
public TaikoJudgementPiece(HitResult result)
: base(result)
{
}
public override void PlayAnimation()
{
if (Result != HitResult.Miss)
{
JudgementText.ScaleTo(0.9f);
JudgementText.ScaleTo(1, 500, Easing.OutElastic);
}
base.PlayAnimation();
}
}
}
}

View File

@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo);
protected override Playfield CreatePlayfield() => new TaikoPlayfield();
public override DrawableHitObject<TaikoHitObject> CreateDrawableRepresentation(TaikoHitObject h) => null;

View File

@ -0,0 +1,30 @@
// 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.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.UI
{
public class DrumSampleTriggerSource : GameplaySampleTriggerSource
{
public DrumSampleTriggerSource(HitObjectContainer hitObjectContainer)
: base(hitObjectContainer)
{
}
public void Play(HitType hitType)
{
var hitObject = GetMostValidObject();
if (hitObject == null)
return;
PlaySamples(new ISampleInfo[] { hitObject.SampleControlPoint.GetSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL) });
}
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");
}
}

View File

@ -2,18 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float middle_split = 0.025f;
[Cached]
private DrumSampleContainer sampleContainer;
private DrumSampleTriggerSource sampleTriggerSource;
public InputDrum(ControlPointInfo controlPoints)
public InputDrum(HitObjectContainer hitObjectContainer)
{
sampleContainer = new DrumSampleContainer(controlPoints);
sampleTriggerSource = new DrumSampleTriggerSource(hitObjectContainer);
RelativeSizeAxes = Axes.Both;
}
@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
}),
sampleContainer
sampleTriggerSource
};
}
@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centreHit;
[Resolved]
private DrumSampleContainer sampleContainer { get; set; }
private DrumSampleTriggerSource sampleTriggerSource { get; set; }
public TaikoHalfDrum(bool flipped)
{
@ -156,21 +156,19 @@ namespace osu.Game.Rulesets.Taiko.UI
Drawable target = null;
Drawable back = null;
var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction)
{
target = centreHit;
back = centre;
drumSample.Centre?.Play();
sampleTriggerSource.Play(HitType.Centre);
}
else if (action == RimAction)
{
target = rimHit;
back = rim;
drumSample.Rim?.Play();
sampleTriggerSource.Play(HitType.Rim);
}
if (target != null)

View File

@ -8,7 +8,6 @@ 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;
using osu.Game.Rulesets.Judgements;
@ -27,8 +26,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public class TaikoPlayfield : ScrollingPlayfield
{
private readonly ControlPointInfo controlPoints;
/// <summary>
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
/// </summary>
@ -56,11 +53,6 @@ namespace osu.Game.Rulesets.Taiko.UI
private Container hitTargetOffsetContent;
public TaikoPlayfield(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
@ -131,7 +123,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Children = new Drawable[]
{
new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.PlayfieldBackgroundLeft), _ => new PlayfieldBackgroundLeft()),
new InputDrum(controlPoints)
new InputDrum(HitObjectContainer)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,

View File

@ -3,15 +3,12 @@
using System;
using System.IO;
using NUnit.Framework;
using osuTK;
using osuTK.Graphics;
using osu.Game.Tests.Resources;
using System.Linq;
using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO;
using osu.Game.Rulesets.Catch;
@ -19,9 +16,13 @@ using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Beatmaps.Formats
{
@ -166,7 +167,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var stream = new LineBufferedReader(resStream))
{
var beatmap = decoder.Decode(stream);
var controlPoints = beatmap.ControlPointInfo;
var controlPoints = (LegacyControlPointInfo)beatmap.ControlPointInfo;
Assert.AreEqual(4, controlPoints.TimingPoints.Count);
Assert.AreEqual(5, controlPoints.DifficultyPoints.Count);
@ -240,7 +241,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("overlapping-control-points.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var controlPoints = decoder.Decode(stream).ControlPointInfo;
var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(4));
Assert.That(controlPoints.DifficultyPoints.Count, Is.EqualTo(3));

View File

@ -14,6 +14,7 @@ using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Catch;
@ -49,6 +50,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
}
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStabilityDoubleConvert(string name)
{
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
// run an extra convert. this is expected to be stable.
decodedAfterEncode.beatmap = convert(decodedAfterEncode.beatmap);
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
}
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStabilityWithNonLegacyControlPoints(string name)
{
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
// we are testing that the transfer of relevant data to hitobjects (from legacy control points) sticks through encode/decode.
// before the encode step, the legacy information is removed here.
decoded.beatmap.ControlPointInfo = removeLegacyControlPointTypes(decoded.beatmap.ControlPointInfo);
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
// in this process, we may lose some detail in the control points section.
// let's focus on only the hitobjects.
var originalHitObjects = decoded.beatmap.HitObjects.Serialize();
var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize();
Assert.That(newHitObjects, Is.EqualTo(originalHitObjects));
ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo)
{
// emulate non-legacy control points by cloning the non-legacy portion.
// the assertion is that the encoder can recreate this losslessly from hitobject data.
Assert.IsInstanceOf<LegacyControlPointInfo>(controlPointInfo);
var newControlPoints = new ControlPointInfo();
foreach (var point in controlPointInfo.AllControlPoints)
{
// completely ignore "legacy" types, which have been moved to HitObjects.
// even though these would mostly be ignored by the Add call, they will still be available in groups,
// which isn't what we want to be testing here.
if (point is SampleControlPoint)
continue;
newControlPoints.Add(point.Time, point.DeepClone());
}
return newControlPoints;
}
}
[Test]
public void TestEncodeMultiSegmentSliderWithFloatingPointError()
{
@ -116,7 +174,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
private Stream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream();

View File

@ -11,6 +11,7 @@ using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using Realms;
namespace osu.Game.Tests.Database
@ -42,7 +43,7 @@ namespace osu.Game.Tests.Database
KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer);
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
Assert.That(queryCount(), Is.EqualTo(3));
@ -66,7 +67,7 @@ namespace osu.Game.Tests.Database
{
KeyBindingContainer testContainer = new TestKeyBindingContainer();
keyBindingStore.Register(testContainer);
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
using (var primaryUsage = realmContextFactory.GetForRead())
{

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
@ -30,7 +31,7 @@ namespace osu.Game.Tests.Editing.Checks
{
check = new CheckMutedObjects();
cpi = new ControlPointInfo();
cpi = new LegacyControlPointInfo();
cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });

View File

@ -4,6 +4,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
namespace osu.Game.Tests.NonVisual
{
@ -64,7 +65,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestAddRedundantSample()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
cpi.Add(0, new SampleControlPoint()); // is *not* redundant, special exception for first sample point
cpi.Add(1000, new SampleControlPoint()); // is redundant
@ -142,7 +143,7 @@ namespace osu.Game.Tests.NonVisual
[Test]
public void TestRemoveGroupAlsoRemovedControlPoints()
{
var cpi = new ControlPointInfo();
var cpi = new LegacyControlPointInfo();
var group = cpi.GroupAt(1000, true);

View File

@ -36,9 +36,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)]
[TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)]
[TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)]
[TestCase(ScoringMode.Classic, HitResult.Meh, 50)]
[TestCase(ScoringMode.Classic, HitResult.Ok, 100)]
[TestCase(ScoringMode.Classic, HitResult.Great, 300)]
[TestCase(ScoringMode.Classic, HitResult.Meh, 41)]
[TestCase(ScoringMode.Classic, HitResult.Ok, 46)]
[TestCase(ScoringMode.Classic, HitResult.Great, 72)]
public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
scoreProcessor.Mode.Value = scoringMode;
@ -85,19 +85,18 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 1_000_030)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 1_000_150)] // 1 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 594)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] // (((3 * 300) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] // (((3 * 350) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] // (0 * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] // (((3 * 10) / (4 * 10)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] // (((3 * 50) / (4 * 50)) * 4 * 300) * (1 + 1 / 25)
// TODO: The following two cases don't match expectations currently (a single hit is registered in acc portion when it shouldn't be). See https://github.com/ppy/osu/issues/12604.
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 330)] // (1 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 450)] // (1 * 1 * 300) * (1 + 0 / 25) + 3 * 50 (bonus points)
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)]
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 68)]
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 81)]
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 109)]
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 149)]
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 149)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 9)]
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 15)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)]
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 149)]
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 18)]
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 18)]
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
{
var minResult = new TestJudgement(hitResult).MinResult;
@ -129,8 +128,8 @@ namespace osu.Game.Tests.Rulesets.Scoring
/// </remarks>
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 69)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 60)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
{
IEnumerable<HitObject> hitObjects = Enumerable

View File

@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneDesignSection : OsuManualInputManagerTestScene
{
private TestDesignSection designSection;
private EditorBeatmap editorBeatmap { get; set; }
[SetUpSteps]
public void SetUp()
{
AddStep("create blank beatmap", () => editorBeatmap = new EditorBeatmap(new Beatmap()));
AddStep("create section", () => Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(EditorBeatmap), editorBeatmap)
},
Child = designSection = new TestDesignSection()
});
}
[Test]
public void TestCountdownOff()
{
AddStep("turn countdown off", () => designSection.EnableCountdown.Current.Value = false);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.None);
AddUntilStep("other controls hidden", () => !designSection.CountdownSettings.IsPresent);
}
[Test]
public void TestCountdownOn()
{
AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal);
AddUntilStep("other controls shown", () => designSection.CountdownSettings.IsPresent);
AddStep("change countdown speed", () => designSection.CountdownSpeed.Current.Value = CountdownType.DoubleSpeed);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.DoubleSpeed);
AddUntilStep("other controls still shown", () => designSection.CountdownSettings.IsPresent);
}
[Test]
public void TestCountdownOffset()
{
AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal);
checkOffsetAfter("1", 1);
checkOffsetAfter(string.Empty, 0);
checkOffsetAfter("123", 123);
checkOffsetAfter("0", 0);
}
private void checkOffsetAfter(string userInput, int expectedFinalValue)
{
AddStep("click text box", () =>
{
var textBox = designSection.CountdownOffset.ChildrenOfType<TextBox>().Single();
InputManager.MoveMouseTo(textBox);
InputManager.Click(MouseButton.Left);
});
AddStep("set offset text", () => designSection.CountdownOffset.Current.Value = userInput);
AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert($"displayed value is {expectedFinalValue}", () => designSection.CountdownOffset.Current.Value == expectedFinalValue.ToString(CultureInfo.InvariantCulture));
AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.BeatmapInfo.CountdownOffset == expectedFinalValue);
}
private class TestDesignSection : DesignSection
{
public new LabelledSwitchButton EnableCountdown => base.EnableCountdown;
public new FillFlowContainer CountdownSettings => base.CountdownSettings;
public new LabelledEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
public new LabelledNumberBox CountdownOffset => base.CountdownOffset;
}
}
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Editing
beatmap.BeatmapInfo.BeatDivisor = 1;
beatmap.ControlPointInfo = new ControlPointInfo();
beatmap.ControlPointInfo.Clear();
beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 });
beatmap.ControlPointInfo.Add(2000, new TimingControlPoint { BeatLength = 500 });

View File

@ -4,8 +4,13 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
@ -23,15 +28,31 @@ namespace osu.Game.Tests.Visual.Editing
editorBeatmap = new EditorBeatmap(new OsuBeatmap());
}
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
[Test]
public void TestOsu() => runForRuleset(new OsuRuleset().RulesetInfo);
Child = new SetupScreen
[Test]
public void TestTaiko() => runForRuleset(new TaikoRuleset().RulesetInfo);
[Test]
public void TestCatch() => runForRuleset(new CatchRuleset().RulesetInfo);
[Test]
public void TestMania() => runForRuleset(new ManiaRuleset().RulesetInfo);
private void runForRuleset(RulesetInfo rulesetInfo)
{
AddStep("create screen", () =>
{
State = { Value = Visibility.Visible },
};
editorBeatmap.BeatmapInfo.Ruleset = rulesetInfo;
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Child = new SetupScreen
{
State = { Value = Visibility.Visible },
};
});
}
}
}

View File

@ -0,0 +1,135 @@
// 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.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneGameplaySampleTriggerSource : PlayerTestScene
{
private TestGameplaySampleTriggerSource sampleTriggerSource;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
private Beatmap beatmap;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
beatmap = new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
Ruleset = ruleset
}
};
const double start_offset = 8000;
const double spacing = 2000;
double t = start_offset;
beatmap.HitObjects.AddRange(new[]
{
new HitCircle
{
// intentionally start objects a bit late so we can test the case of no alive objects.
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
},
new HitCircle
{
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) }
},
new HitCircle
{
StartTime = t += spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) },
SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
},
new HitCircle
{
StartTime = t + spacing,
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) },
SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
},
});
return beatmap;
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("Add trigger source", () => Player.HUDOverlay.Add(sampleTriggerSource = new TestGameplaySampleTriggerSource(Player.DrawableRuleset.Playfield.HitObjectContainer)));
}
[Test]
public void TestCorrectHitObject()
{
HitObjectLifetimeEntry nextObjectEntry = null;
AddAssert("no alive objects", () => getNextAliveObject() == null);
AddAssert("check initially correct object", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[0]);
AddUntilStep("get next object", () =>
{
var nextDrawableObject = getNextAliveObject();
if (nextDrawableObject != null)
{
nextObjectEntry = nextDrawableObject.Entry;
InputManager.MoveMouseTo(nextDrawableObject.ScreenSpaceDrawQuad.Centre);
return true;
}
return false;
});
AddUntilStep("hit first hitobject", () =>
{
InputManager.Click(MouseButton.Left);
return nextObjectEntry.Result.HasResult;
});
AddAssert("check correct object after hit", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[1]);
AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[2]);
AddUntilStep("check correct object after miss", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
AddUntilStep("no alive objects", () => getNextAliveObject() == null);
AddAssert("check correct object after none alive", () => sampleTriggerSource.GetMostValidObject() == beatmap.HitObjects[3]);
}
private DrawableHitObject getNextAliveObject() =>
Player.DrawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault();
[Test]
public void TestSampleTriggering()
{
AddRepeatStep("trigger sample", () => sampleTriggerSource.Play(), 10);
}
public class TestGameplaySampleTriggerSource : GameplaySampleTriggerSource
{
public TestGameplaySampleTriggerSource(HitObjectContainer hitObjectContainer)
: base(hitObjectContainer)
{
}
public new HitObject GetMostValidObject() => base.GetMostValidObject();
}
}
}

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
@ -19,6 +20,7 @@ using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Users;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -78,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestGeneral()
{
int[] userIds = Enumerable.Range(0, 4).Select(i => PLAYER_1_ID + i).ToArray();
int[] userIds = getPlayerIds(4);
start(userIds);
loadSpectateScreen();
@ -314,6 +316,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("player 2 playing from correct point in time", () => getPlayer(PLAYER_2_ID).ChildrenOfType<DrawableRuleset>().Single().FrameStableClock.CurrentTime > 30000);
}
[Test]
public void TestPlayersLeaveWhileSpectating()
{
start(getPlayerIds(4));
sendFrames(getPlayerIds(4), 300);
loadSpectateScreen();
for (int count = 3; count >= 0; count--)
{
var id = PLAYER_1_ID + count;
end(id);
AddUntilStep($"{id} area grayed", () => getInstance(id).Colour != Color4.White);
AddUntilStep($"{id} score quit set", () => getLeaderboardScore(id).HasQuit.Value);
sendFrames(getPlayerIds(count), 300);
}
Player player = null;
AddStep($"get {PLAYER_1_ID} player instance", () => player = getInstance(PLAYER_1_ID).ChildrenOfType<Player>().Single());
start(new[] { PLAYER_1_ID });
sendFrames(PLAYER_1_ID, 300);
AddAssert($"{PLAYER_1_ID} player instance still same", () => getInstance(PLAYER_1_ID).ChildrenOfType<Player>().Single() == player);
AddAssert($"{PLAYER_1_ID} area still grayed", () => getInstance(PLAYER_1_ID).Colour != Color4.White);
AddAssert($"{PLAYER_1_ID} score quit still set", () => getLeaderboardScore(PLAYER_1_ID).HasQuit.Value);
}
private void loadSpectateScreen(bool waitForPlayerLoad = true)
{
AddStep("load screen", () =>
@ -333,14 +365,32 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
foreach (int id in userIds)
{
OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true);
var user = new MultiplayerRoomUser(id)
{
User = new User { Id = id },
};
OnlinePlayDependencies.Client.AddUser(user.User, true);
SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUsers.Add(new MultiplayerRoomUser(id));
playingUsers.Add(user);
}
});
}
private void end(int userId)
{
AddStep($"end play for {userId}", () =>
{
var user = playingUsers.Single(u => u.UserID == userId);
OnlinePlayDependencies.Client.RemoveUser(user.User.AsNonNull());
SpectatorClient.EndPlay(userId);
playingUsers.Remove(user);
});
}
private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count);
private void sendFrames(int[] userIds, int count = 10)
@ -374,5 +424,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType<Player>().Single();
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId);
private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray();
}
}

View File

@ -529,7 +529,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("invoke on back button", () => multiplayerScreen.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<LocalPlayerModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<UserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);

View File

@ -323,6 +323,71 @@ namespace osu.Game.Tests.Visual.Navigation
AddWaitStep("wait two frames", 2);
}
[Test]
public void TestOverlayClosing()
{
// use now playing overlay for "overlay -> background" drag case
// since most overlays use a scroll container that absorbs on mouse down
NowPlayingOverlay nowPlayingOverlay = null;
AddStep("enter menu", () => InputManager.Key(Key.Enter));
AddStep("get and press now playing hotkey", () =>
{
nowPlayingOverlay = Game.ChildrenOfType<NowPlayingOverlay>().Single();
InputManager.Key(Key.F6);
});
// drag tests
// background -> toolbar
AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight));
AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left));
AddStep("move cursor to toolbar", () => InputManager.MoveMouseTo(Game.Toolbar.ScreenSpaceDrawQuad.Centre));
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden);
AddStep("press now playing hotkey", () => InputManager.Key(Key.F6));
// toolbar -> background
AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left));
AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight));
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("now playing is still visible", () => nowPlayingOverlay.State.Value == Visibility.Visible);
// background -> overlay
AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left));
AddStep("move cursor to now playing overlay", () => InputManager.MoveMouseTo(nowPlayingOverlay.ScreenSpaceDrawQuad.Centre));
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("now playing is still visible", () => nowPlayingOverlay.State.Value == Visibility.Visible);
// overlay -> background
AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left));
AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight));
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("now playing is still visible", () => nowPlayingOverlay.State.Value == Visibility.Visible);
// background -> background
AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left));
AddStep("move cursor to left", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomLeft));
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden);
AddStep("press now playing hotkey", () => InputManager.Key(Key.F6));
// click tests
// toolbar
AddStep("move cursor to toolbar", () => InputManager.MoveMouseTo(Game.Toolbar.ScreenSpaceDrawQuad.Centre));
AddStep("click left mouse button", () => InputManager.Click(MouseButton.Left));
AddAssert("now playing is still visible", () => nowPlayingOverlay.State.Value == Visibility.Visible);
// background
AddStep("move cursor to background", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.BottomRight));
AddStep("click left mouse button", () => InputManager.Click(MouseButton.Left));
AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden);
}
private void pushEscape() =>
AddStep("Press escape", () => InputManager.Key(Key.Escape));

View File

@ -104,6 +104,9 @@ namespace osu.Game.Tests.Visual.Online
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg"
}, api.IsLoggedIn));
AddStep("Show ppy from username", () => profile.ShowUser(@"peppy"));
AddStep("Show flyte from username", () => profile.ShowUser(@"flyte"));
AddStep("Hide", profile.Hide);
AddStep("Show without reload", profile.Show);
}

View File

@ -1,14 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osuTK;
@ -17,22 +18,34 @@ namespace osu.Game.Tests.Visual.Settings
[TestFixture]
public class TestSceneTabletSettings : OsuTestScene
{
[BackgroundDependencyLoader]
private void load(GameHost host)
{
var tabletHandler = new TestTabletHandler();
private TestTabletHandler tabletHandler;
private TabletSettings settings;
AddRange(new Drawable[]
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create settings", () =>
{
new TabletSettings(tabletHandler)
tabletHandler = new TestTabletHandler();
Children = new Drawable[]
{
RelativeSizeAxes = Axes.None,
Width = SettingsPanel.PANEL_WIDTH,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}
settings = new TabletSettings(tabletHandler)
{
RelativeSizeAxes = Axes.None,
Width = SettingsPanel.PANEL_WIDTH,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}
};
});
AddStep("set square size", () => tabletHandler.SetTabletSize(new Vector2(100, 100)));
}
[Test]
public void TestVariousTabletSizes()
{
AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300)));
AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300)));
@ -40,6 +53,71 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero));
}
[Test]
public void TestWideAspectRatioValidity()
{
AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
AddStep("Reset to full area", () => settings.ChildrenOfType<DangerousSettingsButton>().First().TriggerClick());
ensureValid();
AddStep("rotate 10", () => tabletHandler.Rotation.Value = 10);
ensureInvalid();
AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
ensureInvalid();
AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
ensureInvalid();
AddStep("scale down", () => tabletHandler.AreaSize.Value *= 0.9f);
ensureValid();
}
[Test]
public void TestRotationValidity()
{
AddAssert("area valid", () => settings.AreaSelection.IsWithinBounds);
AddStep("rotate 90", () => tabletHandler.Rotation.Value = 90);
ensureValid();
AddStep("rotate 180", () => tabletHandler.Rotation.Value = 180);
ensureValid();
AddStep("rotate 270", () => tabletHandler.Rotation.Value = 270);
ensureValid();
AddStep("rotate 360", () => tabletHandler.Rotation.Value = 360);
ensureValid();
AddStep("rotate 0", () => tabletHandler.Rotation.Value = 0);
ensureValid();
AddStep("rotate 45", () => tabletHandler.Rotation.Value = 45);
ensureInvalid();
AddStep("rotate 0", () => tabletHandler.Rotation.Value = 0);
ensureValid();
}
[Test]
public void TestOffsetValidity()
{
ensureValid();
AddStep("move right", () => tabletHandler.AreaOffset.Value = Vector2.Zero);
ensureInvalid();
AddStep("move back", () => tabletHandler.AreaOffset.Value = tabletHandler.AreaSize.Value / 2);
ensureValid();
}
private void ensureValid() => AddAssert("area valid", () => settings.AreaSelection.IsWithinBounds);
private void ensureInvalid() => AddAssert("area invalid", () => !settings.AreaSelection.IsWithinBounds);
public class TestTabletHandler : ITabletHandler
{
public Bindable<Vector2> AreaOffset { get; } = new Bindable<Vector2>();

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Cursor;
@ -55,20 +56,24 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
Child = new OsuContextMenuContainer
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new Container
Child = new OsuContextMenuContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
Child = component = new LabelledColourPalette
RelativeSizeAxes = Axes.Both,
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ColourNamePrefix = "My colour #"
Width = 500,
AutoSizeAxes = Axes.Y,
Child = component = new LabelledColourPalette
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ColourNamePrefix = "My colour #"
}
}
}
};

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneLabelledDropdown : OsuTestScene
{
[Test]
public void TestLabelledDropdown()
=> AddStep(@"create dropdown", () => Child = new LabelledDropdown<string>
{
Label = @"Countdown speed",
Items = new[]
{
@"Half",
@"Normal",
@"Double"
},
Description = @"This is a description"
});
[Test]
public void TestLabelledEnumDropdown()
=> AddStep(@"create dropdown", () => Child = new LabelledEnumDropdown<BeatmapSetOnlineStatus>
{
Label = @"Beatmap status",
Description = @"This is a description"
});
}
}

View File

@ -422,7 +422,7 @@ namespace osu.Game.Tests.Visual.UserInterface
};
}
private class TestModSelectOverlay : LocalPlayerModSelectOverlay
private class TestModSelectOverlay : UserModSelectOverlay
{
public new Bindable<IReadOnlyList<Mod>> SelectedMods => base.SelectedMods;

View File

@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for ready", () => modSelect.State.Value == Visibility.Visible && modSelect.ButtonsLoaded);
}
private class TestModSelectOverlay : LocalPlayerModSelectOverlay
private class TestModSelectOverlay : UserModSelectOverlay
{
public new VisibilityContainer ModSettingsContainer => base.ModSettingsContainer;
public new TriangleButton CustomiseButton => base.CustomiseButton;

View File

@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneOsuLogo : OsuTestScene
{
[Test]
public void TestBasic()
{
AddStep("Add logo", () =>
{
Child = new OsuLogo
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
}
}
}

View File

@ -3,7 +3,7 @@
<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.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -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.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
</ItemGroup>

View File

@ -1,14 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Drawing;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Overlays;
@ -131,25 +129,5 @@ namespace osu.Game.Tournament.Screens.Setup
resolution.Value = $"{ScreenSpaceDrawQuad.Width:N0}x{ScreenSpaceDrawQuad.Height:N0}";
}
public class LabelledDropdown<T> : LabelledComponent<OsuDropdown<T>, T>
{
public LabelledDropdown()
: base(true)
{
}
public IEnumerable<T> Items
{
get => Component.Items;
set => Component.Items = value;
}
protected override OsuDropdown<T> CreateComponent() => new OsuDropdown<T>
{
RelativeSizeAxes = Axes.X,
Width = 0.5f,
};
}
}
}

View File

@ -32,7 +32,23 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Returns a shallow-clone of this <see cref="BeatmapDifficulty"/>.
/// </summary>
public BeatmapDifficulty Clone() => (BeatmapDifficulty)MemberwiseClone();
public BeatmapDifficulty Clone()
{
var diff = new BeatmapDifficulty();
CopyTo(diff);
return diff;
}
public void CopyTo(BeatmapDifficulty difficulty)
{
difficulty.ApproachRate = ApproachRate;
difficulty.DrainRate = DrainRate;
difficulty.CircleSize = CircleSize;
difficulty.OverallDifficulty = OverallDifficulty;
difficulty.SliderMultiplier = SliderMultiplier;
difficulty.SliderTickRate = SliderTickRate;
}
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.

View File

@ -10,7 +10,6 @@ using Newtonsoft.Json;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets;
using osu.Game.Scoring;
@ -18,7 +17,7 @@ namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
[Serializable]
public class BeatmapInfo : IEquatable<BeatmapInfo>, IJsonSerializable, IHasPrimaryKey
public class BeatmapInfo : IEquatable<BeatmapInfo>, IHasPrimaryKey
{
public int ID { get; set; }

View File

@ -153,6 +153,11 @@ namespace osu.Game.Beatmaps
if (!storage.Exists(cache_database_name))
return false;
if (string.IsNullOrEmpty(beatmap.MD5Hash)
&& string.IsNullOrEmpty(beatmap.Path)
&& beatmap.OnlineBeatmapID == null)
return false;
try
{
using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online")))

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using Newtonsoft.Json;
using osu.Game.Graphics;
using osu.Game.Utils;
using osuTK.Graphics;
@ -13,6 +14,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary>
/// The time at which the control point takes effect.
/// </summary>
[JsonIgnore]
public double Time => controlPointGroup?.Time ?? 0;
private ControlPointGroup controlPointGroup;

View File

@ -41,14 +41,6 @@ namespace osu.Game.Beatmaps.ControlPoints
private readonly SortedList<DifficultyControlPoint> difficultyPoints = new SortedList<DifficultyControlPoint>(Comparer<DifficultyControlPoint>.Default);
/// <summary>
/// All sound points.
/// </summary>
[JsonProperty]
public IBindableList<SampleControlPoint> SamplePoints => samplePoints;
private readonly BindableList<SampleControlPoint> samplePoints = new BindableList<SampleControlPoint>();
/// <summary>
/// All effect points.
/// </summary>
@ -69,7 +61,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the difficulty control point at.</param>
/// <returns>The difficulty control point.</returns>
[NotNull]
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
/// <summary>
/// Finds the effect control point that is active at <paramref name="time"/>.
@ -77,15 +69,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the effect control point at.</param>
/// <returns>The effect control point.</returns>
[NotNull]
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
/// <summary>
/// Finds the sound control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the sound control point at.</param>
/// <returns>The sound control point.</returns>
[NotNull]
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
public EffectControlPoint EffectPointAt(double time) => BinarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
/// <summary>
/// Finds the timing control point that is active at <paramref name="time"/>.
@ -93,7 +77,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the timing control point at.</param>
/// <returns>The timing control point.</returns>
[NotNull]
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
public TimingControlPoint TimingPointAt(double time) => BinarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
/// <summary>
/// Finds the maximum BPM represented by any timing control point.
@ -112,12 +96,11 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary>
/// Remove all <see cref="ControlPointGroup"/>s and return to a pristine state.
/// </summary>
public void Clear()
public virtual void Clear()
{
groups.Clear();
timingPoints.Clear();
difficultyPoints.Clear();
samplePoints.Clear();
effectPoints.Clear();
}
@ -129,7 +112,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <returns>Whether the control point was added.</returns>
public bool Add(double time, ControlPoint controlPoint)
{
if (checkAlreadyExisting(time, controlPoint))
if (CheckAlreadyExisting(time, controlPoint))
return false;
GroupAt(time, true).Add(controlPoint);
@ -147,8 +130,8 @@ namespace osu.Game.Beatmaps.ControlPoints
if (addIfNotExisting)
{
newGroup.ItemAdded += groupItemAdded;
newGroup.ItemRemoved += groupItemRemoved;
newGroup.ItemAdded += GroupItemAdded;
newGroup.ItemRemoved += GroupItemRemoved;
groups.Insert(~i, newGroup);
return newGroup;
@ -162,8 +145,8 @@ namespace osu.Game.Beatmaps.ControlPoints
foreach (var item in group.ControlPoints.ToArray())
group.Remove(item);
group.ItemAdded -= groupItemAdded;
group.ItemRemoved -= groupItemRemoved;
group.ItemAdded -= GroupItemAdded;
group.ItemRemoved -= GroupItemRemoved;
groups.Remove(group);
}
@ -228,10 +211,10 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the control point at.</param>
/// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param>
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
private T binarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
protected T BinarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
where T : ControlPoint
{
return binarySearch(list, time) ?? fallback;
return BinarySearch(list, time) ?? fallback;
}
/// <summary>
@ -240,7 +223,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="list">The list to search.</param>
/// <param name="time">The time to find the control point at.</param>
/// <returns>The active control point at <paramref name="time"/>.</returns>
private T binarySearch<T>(IReadOnlyList<T> list, double time)
protected virtual T BinarySearch<T>(IReadOnlyList<T> list, double time)
where T : ControlPoint
{
if (list == null)
@ -280,7 +263,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="time">The time to find the timing control point at.</param>
/// <param name="newPoint">A point to be added.</param>
/// <returns>Whether the new point should be added.</returns>
private bool checkAlreadyExisting(double time, ControlPoint newPoint)
protected virtual bool CheckAlreadyExisting(double time, ControlPoint newPoint)
{
ControlPoint existing = null;
@ -288,17 +271,13 @@ namespace osu.Game.Beatmaps.ControlPoints
{
case TimingControlPoint _:
// Timing points are a special case and need to be added regardless of fallback availability.
existing = binarySearch(TimingPoints, time);
existing = BinarySearch(TimingPoints, time);
break;
case EffectControlPoint _:
existing = EffectPointAt(time);
break;
case SampleControlPoint _:
existing = binarySearch(SamplePoints, time);
break;
case DifficultyControlPoint _:
existing = DifficultyPointAt(time);
break;
@ -307,7 +286,7 @@ namespace osu.Game.Beatmaps.ControlPoints
return newPoint?.IsRedundant(existing) == true;
}
private void groupItemAdded(ControlPoint controlPoint)
protected virtual void GroupItemAdded(ControlPoint controlPoint)
{
switch (controlPoint)
{
@ -319,17 +298,13 @@ namespace osu.Game.Beatmaps.ControlPoints
effectPoints.Add(typed);
break;
case SampleControlPoint typed:
samplePoints.Add(typed);
break;
case DifficultyControlPoint typed:
difficultyPoints.Add(typed);
break;
}
}
private void groupItemRemoved(ControlPoint controlPoint)
protected virtual void GroupItemRemoved(ControlPoint controlPoint)
{
switch (controlPoint)
{
@ -341,10 +316,6 @@ namespace osu.Game.Beatmaps.ControlPoints
effectPoints.Remove(typed);
break;
case SampleControlPoint typed:
samplePoints.Remove(typed);
break;
case DifficultyControlPoint typed:
difficultyPoints.Remove(typed);
break;
@ -353,7 +324,7 @@ namespace osu.Game.Beatmaps.ControlPoints
public ControlPointInfo DeepClone()
{
var controlPointInfo = new ControlPointInfo();
var controlPointInfo = (ControlPointInfo)Activator.CreateInstance(GetType());
foreach (var point in AllControlPoints)
controlPointInfo.Add(point.Time, point.DeepClone());

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
namespace osu.Game.Beatmaps
{
/// <summary>
@ -9,8 +11,14 @@ namespace osu.Game.Beatmaps
public enum CountdownType
{
None = 0,
[Description("Normal")]
Normal = 1,
[Description("Half speed")]
HalfSpeed = 2,
[Description("Double speed")]
DoubleSpeed = 3
}
}

View File

@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osuTK;
@ -24,7 +23,7 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables
{
public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip
public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip<DifficultyIconTooltipContent>
{
private readonly Container iconContainer;
@ -127,9 +126,9 @@ namespace osu.Game.Beatmaps.Drawables
difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars));
}
public ITooltip GetCustomTooltip() => new DifficultyIconTooltip();
ITooltip<DifficultyIconTooltipContent> IHasCustomTooltip<DifficultyIconTooltipContent>.GetCustomTooltip() => new DifficultyIconTooltip();
public object TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null;
DifficultyIconTooltipContent IHasCustomTooltip<DifficultyIconTooltipContent>.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null;
private class DifficultyRetriever : Component
{
@ -173,115 +172,5 @@ namespace osu.Game.Beatmaps.Drawables
difficultyCancellation?.Cancel();
}
}
private class DifficultyIconTooltipContent
{
public readonly BeatmapInfo Beatmap;
public readonly IBindable<StarDifficulty> Difficulty;
public DifficultyIconTooltipContent(BeatmapInfo beatmap, IBindable<StarDifficulty> difficulty)
{
Beatmap = beatmap;
Difficulty = difficulty;
}
}
private class DifficultyIconTooltip : VisibilityContainer, ITooltip
{
private readonly OsuSpriteText difficultyName, starRating;
private readonly Box background;
private readonly FillFlowContainer difficultyFlow;
public DifficultyIconTooltip()
{
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
AutoSizeDuration = 200,
AutoSizeEasing = Easing.OutQuint,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(10),
Children = new Drawable[]
{
difficultyName = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold),
},
difficultyFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
starRating = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Left = 4 },
Icon = FontAwesome.Solid.Star,
Size = new Vector2(12),
},
}
}
}
}
};
}
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load()
{
background.Colour = colours.Gray3;
}
private readonly IBindable<StarDifficulty> starDifficulty = new Bindable<StarDifficulty>();
public bool SetContent(object content)
{
if (!(content is DifficultyIconTooltipContent iconContent))
return false;
difficultyName.Text = iconContent.Beatmap.Version;
starDifficulty.UnbindAll();
starDifficulty.BindTo(iconContent.Difficulty);
starDifficulty.BindValueChanged(difficulty =>
{
starRating.Text = $"{difficulty.NewValue.Stars:0.##}";
difficultyFlow.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars);
}, true);
return true;
}
public void Move(Vector2 pos) => Position = pos;
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,121 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Beatmaps.Drawables
{
internal class DifficultyIconTooltip : VisibilityContainer, ITooltip<DifficultyIconTooltipContent>
{
private readonly OsuSpriteText difficultyName, starRating;
private readonly Box background;
private readonly FillFlowContainer difficultyFlow;
public DifficultyIconTooltip()
{
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
AutoSizeDuration = 200,
AutoSizeEasing = Easing.OutQuint,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(10),
Children = new Drawable[]
{
difficultyName = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold),
},
difficultyFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
starRating = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
},
new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Left = 4 },
Icon = FontAwesome.Solid.Star,
Size = new Vector2(12),
},
}
}
}
}
};
}
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader]
private void load()
{
background.Colour = colours.Gray3;
}
private readonly IBindable<StarDifficulty> starDifficulty = new Bindable<StarDifficulty>();
public void SetContent(DifficultyIconTooltipContent content)
{
difficultyName.Text = content.Beatmap.Version;
starDifficulty.UnbindAll();
starDifficulty.BindTo(content.Difficulty);
starDifficulty.BindValueChanged(difficulty =>
{
starRating.Text = $"{difficulty.NewValue.Stars:0.##}";
difficultyFlow.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars);
}, true);
}
public void Move(Vector2 pos) => Position = pos;
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
}
internal class DifficultyIconTooltipContent
{
public readonly BeatmapInfo Beatmap;
public readonly IBindable<StarDifficulty> Difficulty;
public DifficultyIconTooltipContent(BeatmapInfo beatmap, IBindable<StarDifficulty> difficulty)
{
Beatmap = beatmap;
Difficulty = difficulty;
}
}
}

View File

@ -4,12 +4,12 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;

View File

@ -44,6 +44,13 @@ namespace osu.Game.Beatmaps.Formats
offset = FormatVersion < 5 ? 24 : 0;
}
protected override Beatmap CreateTemplateObject()
{
var templateBeatmap = base.CreateTemplateObject();
templateBeatmap.ControlPointInfo = new LegacyControlPointInfo();
return templateBeatmap;
}
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatmap)
{
this.beatmap = beatmap;
@ -430,8 +437,13 @@ namespace osu.Game.Beatmaps.Formats
parser ??= new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
var obj = parser.Parse(line);
if (obj != null)
{
obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
beatmap.HitObjects.Add(obj);
}
}
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);

View File

@ -24,6 +24,12 @@ namespace osu.Game.Beatmaps.Formats
{
public const int LATEST_VERSION = 128;
/// <summary>
/// osu! is generally slower than taiko, so a factor is added to increase
/// speed. This must be used everywhere slider length or beat length is used.
/// </summary>
public const float LEGACY_TAIKO_VELOCITY_MULTIPLIER = 1.4f;
private readonly IBeatmap beatmap;
[CanBeNull]
@ -80,7 +86,7 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}"));
writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}"));
writer.WriteLine(FormattableString.Invariant($"Countdown: {(int)beatmap.BeatmapInfo.Countdown}"));
writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank(beatmap.ControlPointInfo.SamplePointAt(double.MinValue).SampleBank)}"));
writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank((beatmap.HitObjects.FirstOrDefault()?.SampleControlPoint ?? SampleControlPoint.DEFAULT).SampleBank)}"));
writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}"));
writer.WriteLine(FormattableString.Invariant($"Mode: {beatmap.BeatmapInfo.RulesetID}"));
writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}"));
@ -140,9 +146,9 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}"));
writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}"));
// Taiko adjusts the slider multiplier (see: TaikoBeatmapConverter.LEGACY_VELOCITY_MULTIPLIER)
// Taiko adjusts the slider multiplier (see: LEGACY_TAIKO_VELOCITY_MULTIPLIER)
writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1
? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / 1.4f}")
? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}")
: FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}"));
writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}"));
@ -166,6 +172,30 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[TimingPoints]");
if (!(beatmap.ControlPointInfo is LegacyControlPointInfo))
{
var legacyControlPoints = new LegacyControlPointInfo();
foreach (var point in beatmap.ControlPointInfo.AllControlPoints)
legacyControlPoints.Add(point.Time, point.DeepClone());
beatmap.ControlPointInfo = legacyControlPoints;
SampleControlPoint lastRelevantSamplePoint = null;
// iterate over hitobjects and pull out all required sample changes
foreach (var h in beatmap.HitObjects)
{
var hSamplePoint = h.SampleControlPoint;
if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint))
{
legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint);
lastRelevantSamplePoint = hSamplePoint;
}
}
}
foreach (var group in beatmap.ControlPointInfo.Groups)
{
var groupTimingPoint = group.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
@ -175,19 +205,19 @@ namespace osu.Game.Beatmaps.Formats
{
writer.Write(FormattableString.Invariant($"{groupTimingPoint.Time},"));
writer.Write(FormattableString.Invariant($"{groupTimingPoint.BeatLength},"));
outputControlPointEffectsAt(groupTimingPoint.Time, true);
outputControlPointAt(groupTimingPoint.Time, true);
}
// Output any remaining effects as secondary non-timing control point.
var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time);
writer.Write(FormattableString.Invariant($"{group.Time},"));
writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},"));
outputControlPointEffectsAt(group.Time, false);
outputControlPointAt(group.Time, false);
}
void outputControlPointEffectsAt(double time, bool isTimingPoint)
void outputControlPointAt(double time, bool isTimingPoint)
{
var samplePoint = beatmap.ControlPointInfo.SamplePointAt(time);
var samplePoint = ((LegacyControlPointInfo)beatmap.ControlPointInfo).SamplePointAt(time);
var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time);
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)

View File

@ -4,12 +4,11 @@
using System.Collections.Generic;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Beatmaps
{
public interface IBeatmap : IJsonSerializable
public interface IBeatmap
{
/// <summary>
/// This beatmap's info.

View File

@ -0,0 +1,62 @@
// 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 JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Beatmaps.Legacy
{
public class LegacyControlPointInfo : ControlPointInfo
{
/// <summary>
/// All sound points.
/// </summary>
[JsonProperty]
public IBindableList<SampleControlPoint> SamplePoints => samplePoints;
private readonly BindableList<SampleControlPoint> samplePoints = new BindableList<SampleControlPoint>();
/// <summary>
/// Finds the sound control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the sound control point at.</param>
/// <returns>The sound control point.</returns>
[NotNull]
public SampleControlPoint SamplePointAt(double time) => BinarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
public override void Clear()
{
base.Clear();
samplePoints.Clear();
}
protected override bool CheckAlreadyExisting(double time, ControlPoint newPoint)
{
if (newPoint is SampleControlPoint)
{
var existing = BinarySearch(SamplePoints, time);
return newPoint.IsRedundant(existing);
}
return base.CheckAlreadyExisting(time, newPoint);
}
protected override void GroupItemAdded(ControlPoint controlPoint)
{
if (controlPoint is SampleControlPoint typed)
samplePoints.Add(typed);
base.GroupItemAdded(controlPoint);
}
protected override void GroupItemRemoved(ControlPoint controlPoint)
{
if (controlPoint is SampleControlPoint typed)
samplePoints.Remove(typed);
base.GroupItemRemoved(controlPoint);
}
}
}

View File

@ -153,7 +153,7 @@ namespace osu.Game.Graphics.Backgrounds
TriangleParticle newParticle = parts[i];
// Scale moved distance by the size of the triangle. Smaller triangles should move more slowly.
newParticle.Position.Y += parts[i].Scale * movedDistance;
newParticle.Position.Y += Math.Max(0.5f, parts[i].Scale) * movedDistance;
newParticle.Colour.A = adjustedAlpha;
parts[i] = newParticle;

View File

@ -31,12 +31,9 @@ namespace osu.Game.Graphics.Cursor
private readonly OsuSpriteText text;
private bool instantMovement = true;
public override bool SetContent(object content)
public override void SetContent(LocalisableString contentString)
{
if (!(content is LocalisableString contentString))
return false;
if (contentString == text.Text) return true;
if (contentString == text.Text) return;
text.Text = contentString;
@ -47,8 +44,6 @@ namespace osu.Game.Graphics.Cursor
}
else
AutoSizeDuration = 0;
return true;
}
public OsuTooltip()

View File

@ -12,7 +12,7 @@ using osuTK;
namespace osu.Game.Graphics
{
public class DateTooltip : VisibilityContainer, ITooltip
public class DateTooltip : VisibilityContainer, ITooltip<DateTimeOffset>
{
private readonly OsuSpriteText dateText, timeText;
private readonly Box background;
@ -63,14 +63,10 @@ namespace osu.Game.Graphics
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
public bool SetContent(object content)
public void SetContent(DateTimeOffset date)
{
if (!(content is DateTimeOffset date))
return false;
dateText.Text = $"{date:d MMMM yyyy} ";
timeText.Text = $"{date:HH:mm:ss \"UTC\"z}";
return true;
}
public void Move(Vector2 pos) => Position = pos;

View File

@ -10,7 +10,7 @@ using osu.Game.Utils;
namespace osu.Game.Graphics
{
public class DrawableDate : OsuSpriteText, IHasCustomTooltip
public class DrawableDate : OsuSpriteText, IHasCustomTooltip<DateTimeOffset>
{
private DateTimeOffset date;
@ -75,8 +75,8 @@ namespace osu.Game.Graphics
private void updateTime() => Text = Format();
public ITooltip GetCustomTooltip() => new DateTooltip();
public ITooltip<DateTimeOffset> GetCustomTooltip() => new DateTooltip();
public object TooltipContent => Date;
public DateTimeOffset TooltipContent => Date;
}
}

View File

@ -15,11 +15,6 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public abstract class HoverSampleDebounceComponent : CompositeDrawable
{
/// <summary>
/// Length of debounce for hover sound playback, in milliseconds.
/// </summary>
public double HoverDebounceTime { get; } = 20;
private Bindable<double?> lastPlaybackTime;
[BackgroundDependencyLoader]
@ -34,7 +29,7 @@ namespace osu.Game.Graphics.UserInterface
if (e.HasAnyButtonPressed)
return false;
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime;
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
if (enoughTimePassedSinceLastPlayback)
{

View File

@ -6,7 +6,6 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Framework.Utils;
namespace osu.Game.Graphics.UserInterface
@ -28,7 +27,7 @@ namespace osu.Game.Graphics.UserInterface
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, SessionStatics statics)
private void load(AudioManager audio)
{
sampleHover = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-hover")
?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-hover");

View File

@ -0,0 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class LabelledDropdown<TItem> : LabelledComponent<OsuDropdown<TItem>, TItem>
{
public LabelledDropdown()
: base(true)
{
}
public IEnumerable<TItem> Items
{
get => Component.Items;
set => Component.Items = value;
}
protected sealed override OsuDropdown<TItem> CreateComponent() => CreateDropdown().With(d =>
{
d.RelativeSizeAxes = Axes.X;
d.Width = 0.5f;
});
protected virtual OsuDropdown<TItem> CreateDropdown() => new OsuDropdown<TItem>();
}
}

View File

@ -0,0 +1,14 @@
// 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.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class LabelledEnumDropdown<TEnum> : LabelledDropdown<TEnum>
where TEnum : struct, Enum
{
protected override OsuDropdown<TEnum> CreateDropdown() => new OsuEnumDropdown<TEnum>();
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class LabelledNumberBox : LabelledTextBox
{
protected override OsuTextBox CreateTextBox() => new OsuNumberBox();
}
}

View File

@ -7,21 +7,14 @@ using osu.Framework.IO.Serialization;
namespace osu.Game.IO.Serialization
{
public interface IJsonSerializable
{
}
public static class JsonSerializableExtensions
{
public static string Serialize(this IJsonSerializable obj) => JsonConvert.SerializeObject(obj, CreateGlobalSettings());
public static string Serialize(this object obj) => JsonConvert.SerializeObject(obj, CreateGlobalSettings());
public static T Deserialize<T>(this string objString) => JsonConvert.DeserializeObject<T>(objString, CreateGlobalSettings());
public static void DeserializeInto<T>(this string objString, T target) => JsonConvert.PopulateObject(objString, target, CreateGlobalSettings());
/// <summary>
/// Creates the default <see cref="JsonSerializerSettings"/> that should be used for all <see cref="IJsonSerializable"/>s.
/// </summary>
public static JsonSerializerSettings CreateGlobalSettings() => new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,

View File

@ -46,52 +46,53 @@ namespace osu.Game.Input
}
/// <summary>
/// Register a new type of <see cref="KeyBindingContainer{T}"/>, adding default bindings from <see cref="KeyBindingContainer.DefaultKeyBindings"/>.
/// Register all defaults for this store.
/// </summary>
/// <param name="container">The container to populate defaults from.</param>
public void Register(KeyBindingContainer container) => insertDefaults(container.DefaultKeyBindings);
/// <summary>
/// Register a ruleset, adding default bindings for each of its variants.
/// </summary>
/// <param name="ruleset">The ruleset to populate defaults from.</param>
public void Register(RulesetInfo ruleset)
{
var instance = ruleset.CreateInstance();
foreach (var variant in instance.AvailableVariants)
insertDefaults(instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
}
private void insertDefaults(IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
/// <param name="rulesets">The rulesets to populate defaults from.</param>
public void Register(KeyBindingContainer container, IEnumerable<RulesetInfo> rulesets)
{
using (var usage = realmFactory.GetForWrite())
{
// compare counts in database vs defaults
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
// intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed.
// this is much faster as a result.
var existingBindings = usage.Realm.All<RealmKeyBinding>().ToList();
insertDefaults(usage, existingBindings, container.DefaultKeyBindings);
foreach (var ruleset in rulesets)
{
int existingCount = usage.Realm.All<RealmKeyBinding>().Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
if (defaultsForAction.Count() <= existingCount)
continue;
foreach (var k in defaultsForAction.Skip(existingCount))
{
// insert any defaults which are missing.
usage.Realm.Add(new RealmKeyBinding
{
KeyCombinationString = k.KeyCombination.ToString(),
ActionInt = (int)k.Action,
RulesetID = rulesetId,
Variant = variant
});
}
var instance = ruleset.CreateInstance();
foreach (var variant in instance.AvailableVariants)
insertDefaults(usage, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant);
}
usage.Commit();
}
}
private void insertDefaults(RealmContextFactory.RealmUsage usage, List<RealmKeyBinding> existingBindings, IEnumerable<IKeyBinding> defaults, int? rulesetId = null, int? variant = null)
{
// compare counts in database vs defaults for each action type.
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
{
// avoid performing redundant queries when the database is empty and needs to be re-filled.
int existingCount = existingBindings.Count(k => k.RulesetID == rulesetId && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
if (defaultsForAction.Count() <= existingCount)
continue;
// insert any defaults which are missing.
usage.Realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
{
KeyCombinationString = k.KeyCombination.ToString(),
ActionInt = (int)k.Action,
RulesetID = rulesetId,
Variant = variant
}));
}
}
/// <summary>
/// Keys which should not be allowed for gameplay input purposes.
/// </summary>

View File

@ -8,15 +8,47 @@ namespace osu.Game.Online.API.Requests
{
public class GetUserRequest : APIRequest<User>
{
private readonly long? userId;
private readonly string lookup;
public readonly RulesetInfo Ruleset;
private readonly LookupType lookupType;
/// <summary>
/// Gets the currently logged-in user.
/// </summary>
public GetUserRequest()
{
}
/// <summary>
/// Gets a user from their ID.
/// </summary>
/// <param name="userId">The user to get.</param>
/// <param name="ruleset">The ruleset to get the user's info for.</param>
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
{
this.userId = userId;
lookup = userId.ToString();
lookupType = LookupType.Id;
Ruleset = ruleset;
}
protected override string Target => userId.HasValue ? $@"users/{userId}/{Ruleset?.ShortName}" : $@"me/{Ruleset?.ShortName}";
/// <summary>
/// Gets a user from their username.
/// </summary>
/// <param name="username">The user to get.</param>
/// <param name="ruleset">The ruleset to get the user's info for.</param>
public GetUserRequest(string username = null, RulesetInfo ruleset = null)
{
lookup = username;
lookupType = LookupType.Username;
Ruleset = ruleset;
}
protected override string Target => lookup != null ? $@"users/{lookup}/{Ruleset?.ShortName}?k={lookupType.ToString().ToLower()}" : $@"me/{Ruleset?.ShortName}";
private enum LookupType
{
Id,
Username
}
}
}

View File

@ -104,6 +104,8 @@ namespace osu.Game
protected Container ScreenOffsetContainer { get; private set; }
private Container overlayOffsetContainer;
[Resolved]
private FrameworkConfigManager frameworkConfig { get; set; }
@ -120,7 +122,7 @@ namespace osu.Game
public virtual StableStorage GetStorageForStableInstall() => null;
public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0);
private float toolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0);
private IdleTracker idleTracker;
@ -158,7 +160,7 @@ namespace osu.Game
private readonly string[] args;
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
private readonly List<OsuFocusedOverlayContainer> focusedOverlays = new List<OsuFocusedOverlayContainer>();
private readonly List<OverlayContainer> visibleBlockingOverlays = new List<OverlayContainer>();
@ -193,7 +195,7 @@ namespace osu.Game
/// <param name="hideToolbar">Whether the toolbar should also be hidden.</param>
public void CloseAllOverlays(bool hideToolbar = true)
{
foreach (var overlay in overlays)
foreach (var overlay in focusedOverlays)
overlay.Hide();
if (hideToolbar) Toolbar.Hide();
@ -331,6 +333,9 @@ namespace osu.Game
case LinkAction.OpenUserProfile:
if (int.TryParse(link.Argument, out int userId))
ShowUser(userId);
else
ShowUser(link.Argument);
break;
case LinkAction.OpenWiki:
@ -378,6 +383,12 @@ namespace osu.Game
/// <param name="userId">The user to display.</param>
public void ShowUser(int userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId));
/// <summary>
/// Show a user's profile as an overlay.
/// </summary>
/// <param name="username">The user to display.</param>
public void ShowUser(string username) => waitForReady(() => userProfile, _ => userProfile.ShowUser(username));
/// <summary>
/// Show a beatmap's set as an overlay, displaying the given beatmap.
/// </summary>
@ -692,9 +703,16 @@ namespace osu.Game
},
}
},
overlayContent = new Container { RelativeSizeAxes = Axes.Both },
rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
overlayOffsetContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
overlayContent = new Container { RelativeSizeAxes = Axes.Both },
rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
}
},
topMostOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
idleTracker,
new ConfineMouseTracker()
@ -731,7 +749,6 @@ namespace osu.Game
loadComponentSingleFile(Notifications.With(d =>
{
d.GetToolbarHeight = () => ToolbarOffset;
d.Anchor = Anchor.TopRight;
d.Origin = Anchor.TopRight;
}), rightFloatingOverlayContent.Add, true);
@ -757,7 +774,7 @@ namespace osu.Game
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
loadComponentSingleFile(Settings = new SettingsOverlay { GetToolbarHeight = () => ToolbarOffset }, leftFloatingOverlayContent.Add, true);
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
@ -766,14 +783,12 @@ namespace osu.Game
loadComponentSingleFile(new LoginOverlay
{
GetToolbarHeight = () => ToolbarOffset,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}, rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(new NowPlayingOverlay
{
GetToolbarHeight = () => ToolbarOffset,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}, rightFloatingOverlayContent.Add, true);
@ -904,8 +919,8 @@ namespace osu.Game
if (cache)
dependencies.CacheAs(component);
if (component is OverlayContainer overlay)
overlays.Add(overlay);
if (component is OsuFocusedOverlayContainer overlay)
focusedOverlays.Add(overlay);
// schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached).
// with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile,
@ -1013,8 +1028,8 @@ namespace osu.Game
{
base.UpdateAfterChildren();
ScreenOffsetContainer.Padding = new MarginPadding { Top = ToolbarOffset };
overlayContent.Padding = new MarginPadding { Top = ToolbarOffset };
ScreenOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset };
overlayOffsetContainer.Padding = new MarginPadding { Top = toolbarOffset };
var horizontalOffset = 0f;

View File

@ -13,7 +13,6 @@ using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
@ -57,6 +56,11 @@ namespace osu.Game
public const int SAMPLE_CONCURRENCY = 6;
/// <summary>
/// Length of debounce (in milliseconds) for commonly occuring sample playbacks that could stack.
/// </summary>
public const int SAMPLE_DEBOUNCE_TIME = 20;
/// <summary>
/// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects.
/// </summary>
@ -201,31 +205,7 @@ namespace osu.Game
dependencies.CacheAs(this);
dependencies.CacheAs(LocalConfig);
AddFont(Resources, @"Fonts/osuFont");
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
AddFont(Resources, @"Fonts/Torus/Torus-Light");
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Light");
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
AddFont(Resources, @"Fonts/Venera/Venera-Light");
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
AddFont(Resources, @"Fonts/Venera/Venera-Black");
InitialiseFonts();
Audio.Samples.PlaybackConcurrency = SAMPLE_CONCURRENCY;
@ -342,19 +322,12 @@ namespace osu.Game
globalBindings = new GlobalActionContainer(this)
};
MenuCursorContainer.Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }
};
MenuCursorContainer.Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both };
base.Content.Add(CreateScalingContainer().WithChildren(mainContent));
KeyBindingStore = new RealmKeyBindingStore(realmFactory);
KeyBindingStore.Register(globalBindings);
foreach (var r in RulesetStore.AvailableRulesets)
KeyBindingStore.Register(r);
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
dependencies.Cache(globalBindings);
@ -368,6 +341,35 @@ namespace osu.Game
Ruleset.BindValueChanged(onRulesetChanged);
}
protected virtual void InitialiseFonts()
{
AddFont(Resources, @"Fonts/osuFont");
AddFont(Resources, @"Fonts/Torus/Torus-Regular");
AddFont(Resources, @"Fonts/Torus/Torus-Light");
AddFont(Resources, @"Fonts/Torus/Torus-SemiBold");
AddFont(Resources, @"Fonts/Torus/Torus-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-Regular");
AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Light");
AddFont(Resources, @"Fonts/Inter/Inter-LightItalic");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBold");
AddFont(Resources, @"Fonts/Inter/Inter-SemiBoldItalic");
AddFont(Resources, @"Fonts/Inter/Inter-Bold");
AddFont(Resources, @"Fonts/Inter/Inter-BoldItalic");
AddFont(Resources, @"Fonts/Noto/Noto-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-Hangul");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Basic");
AddFont(Resources, @"Fonts/Noto/Noto-CJK-Compatibility");
AddFont(Resources, @"Fonts/Noto/Noto-Thai");
AddFont(Resources, @"Fonts/Venera/Venera-Light");
AddFont(Resources, @"Fonts/Venera/Venera-Bold");
AddFont(Resources, @"Fonts/Venera/Venera-Black");
}
private IDisposable blocking;
private void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state)

View File

@ -4,6 +4,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;

View File

@ -6,12 +6,12 @@ using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;

View File

@ -4,6 +4,7 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
@ -192,6 +193,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
if (showPerformancePoints)
{
Debug.Assert(score.PP != null);
content.Add(new OsuSpriteText
{
Text = score.PP.ToLocalisableString(@"N0"),

View File

@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x");
ppColumn.Alpha = value.Beatmap?.Status.GrantsPerformancePoints() == true ? 1 : 0;
ppColumn.Text = value.PP.ToLocalisableString(@"N0");
ppColumn.Text = value.PP?.ToLocalisableString(@"N0");
statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn);
modsColumn.Mods = value.Mods;

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@ -127,7 +128,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
public int? ScorePosition
{
set => rankText.Text = value == null ? (LocalisableString)"-" : value.ToLocalisableString(@"\##");
set => rankText.Text = value?.ToLocalisableString(@"\##") ?? (LocalisableString)"-";
}
/// <summary>

View File

@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;

View File

@ -140,12 +140,8 @@ namespace osu.Game.Overlays.Dashboard.Home.News
}
}
private class Date : CompositeDrawable, IHasCustomTooltip
private class Date : CompositeDrawable, IHasCustomTooltip<DateTimeOffset>
{
public ITooltip GetCustomTooltip() => new DateTooltip();
public object TooltipContent => date;
private readonly DateTimeOffset date;
public Date(DateTimeOffset date)
@ -190,6 +186,10 @@ namespace osu.Game.Overlays.Dashboard.Home.News
}
};
}
ITooltip<DateTimeOffset> IHasCustomTooltip<DateTimeOffset>.GetCustomTooltip() => new DateTooltip();
DateTimeOffset IHasCustomTooltip<DateTimeOffset>.TooltipContent => date;
}
}
}

View File

@ -67,12 +67,8 @@ namespace osu.Game.Overlays.Dashboard.Home.News
};
}
private class Date : CompositeDrawable, IHasCustomTooltip
private class Date : CompositeDrawable, IHasCustomTooltip<DateTimeOffset>
{
public ITooltip GetCustomTooltip() => new DateTooltip();
public object TooltipContent => date;
private readonly DateTimeOffset date;
public Date(DateTimeOffset date)
@ -110,6 +106,10 @@ namespace osu.Game.Overlays.Dashboard.Home.News
t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular);
});
}
ITooltip<DateTimeOffset> IHasCustomTooltip<DateTimeOffset>.GetCustomTooltip() => new DateTooltip();
DateTimeOffset IHasCustomTooltip<DateTimeOffset>.TooltipContent => date;
}
}
}

View File

@ -10,7 +10,6 @@ using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using System;
namespace osu.Game.Overlays
{
@ -20,11 +19,6 @@ namespace osu.Game.Overlays
private const float transition_time = 400;
/// <summary>
/// Provide a source for the toolbar height.
/// </summary>
public Func<float> GetToolbarHeight;
public LoginOverlay()
{
AutoSizeAxes = Axes.Both;
@ -94,12 +88,5 @@ namespace osu.Game.Overlays
settingsSection.Bounding = false;
this.FadeOut(transition_time);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 };
}
}
}

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public class IncompatibilityDisplayingModButton : ModButton
{
private readonly CompositeDrawable incompatibleIcon;
[Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; }
public IncompatibilityDisplayingModButton(Mod mod)
: base(mod)
{
ButtonContent.Add(incompatibleIcon = new IncompatibleIcon
{
Anchor = Anchor.BottomRight,
Origin = Anchor.Centre,
Position = new Vector2(-13),
});
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateCompatibility), true);
}
protected override void DisplayMod(Mod mod)
{
base.DisplayMod(mod);
Scheduler.AddOnce(updateCompatibility);
}
private void updateCompatibility()
{
var m = SelectedMod ?? Mods.First();
bool isIncompatible = false;
if (selectedMods.Value.Count > 0 && !selectedMods.Value.Contains(m))
isIncompatible = !ModUtils.CheckCompatibleSet(selectedMods.Value.Append(m));
if (isIncompatible)
incompatibleIcon.Show();
else
incompatibleIcon.Hide();
}
public override ITooltip<Mod> GetCustomTooltip() => new IncompatibilityDisplayingTooltip();
private class IncompatibilityDisplayingTooltip : ModButtonTooltip
{
private readonly OsuSpriteText incompatibleText;
private readonly Bindable<IReadOnlyList<Mod>> incompatibleMods = new Bindable<IReadOnlyList<Mod>>();
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; }
public IncompatibilityDisplayingTooltip()
{
AddRange(new Drawable[]
{
incompatibleText = new OsuSpriteText
{
Margin = new MarginPadding { Top = 5 },
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = "Incompatible with:"
},
new ModDisplay
{
Current = incompatibleMods,
ExpansionMode = ExpansionMode.AlwaysExpanded,
Scale = new Vector2(0.7f)
}
});
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
incompatibleText.Colour = colours.BlueLight;
}
protected override void UpdateDisplay(Mod mod)
{
base.UpdateDisplay(mod);
var incompatibleTypes = mod.IncompatibleMods;
var allMods = ruleset.Value.CreateInstance().GetAllMods();
incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).ToList();
incompatibleText.Text = incompatibleMods.Value.Any() ? "Incompatible with:" : "Compatible with all mods";
}
}
}
}

View File

@ -11,29 +11,24 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Utils;
namespace osu.Game.Overlays.Mods
{
/// <summary>
/// Represents a clickable button which can cycle through one of more mods.
/// </summary>
public class ModButton : ModButtonEmpty, IHasCustomTooltip
public class ModButton : ModButtonEmpty, IHasCustomTooltip<Mod>
{
private ModIcon foregroundIcon;
private ModIcon backgroundIcon;
private readonly SpriteText text;
private readonly Container<ModIcon> iconsContainer;
private readonly CompositeDrawable incompatibleIcon;
/// <summary>
/// Fired when the selection changes.
@ -48,9 +43,6 @@ namespace osu.Game.Overlays.Mods
// A selected index of -1 means not selected.
private int selectedIndex = -1;
[Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; }
/// <summary>
/// Change the selected mod index of this button.
/// </summary>
@ -109,7 +101,7 @@ namespace osu.Game.Overlays.Mods
.RotateTo(rotate_angle * direction)
.RotateTo(0f, mod_switch_duration, mod_switch_easing);
Schedule(() => displayMod(newSelection));
Schedule(() => DisplayMod(newSelection));
}
}
@ -138,7 +130,8 @@ namespace osu.Game.Overlays.Mods
}
private Mod mod;
private readonly Container scaleContainer;
protected readonly Container ButtonContent;
public Mod Mod
{
@ -162,7 +155,7 @@ namespace osu.Game.Overlays.Mods
if (Mods.Length > 0)
{
displayMod(Mods[0]);
DisplayMod(Mods[0]);
}
}
}
@ -173,13 +166,13 @@ namespace osu.Game.Overlays.Mods
protected override bool OnMouseDown(MouseDownEvent e)
{
scaleContainer.ScaleTo(0.9f, 800, Easing.Out);
ButtonContent.ScaleTo(0.9f, 800, Easing.Out);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
scaleContainer.ScaleTo(1, 500, Easing.OutElastic);
ButtonContent.ScaleTo(1, 500, Easing.OutElastic);
// only trigger the event if we are inside the area of the button
if (Contains(e.ScreenSpaceMousePosition))
@ -238,30 +231,13 @@ namespace osu.Game.Overlays.Mods
public void Deselect() => changeSelectedIndex(-1);
private void displayMod(Mod mod)
protected virtual void DisplayMod(Mod mod)
{
if (backgroundIcon != null)
backgroundIcon.Mod = foregroundIcon.Mod;
foregroundIcon.Mod = mod;
text.Text = mod.Name;
Colour = mod.HasImplementation ? Color4.White : Color4.Gray;
Scheduler.AddOnce(updateCompatibility);
}
private void updateCompatibility()
{
var m = SelectedMod ?? Mods.First();
bool isIncompatible = false;
if (selectedMods.Value.Count > 0 && !selectedMods.Value.Contains(m))
isIncompatible = !ModUtils.CheckCompatibleSet(selectedMods.Value.Append(m));
if (isIncompatible)
incompatibleIcon.Show();
else
incompatibleIcon.Hide();
}
private void createIcons()
@ -307,7 +283,7 @@ namespace osu.Game.Overlays.Mods
Anchor = Anchor.TopCentre,
Children = new Drawable[]
{
scaleContainer = new Container
ButtonContent = new Container
{
Children = new Drawable[]
{
@ -317,12 +293,6 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
},
incompatibleIcon = new IncompatibleIcon
{
Origin = Anchor.Centre,
Anchor = Anchor.BottomRight,
Position = new Vector2(-13),
}
},
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
@ -342,15 +312,8 @@ namespace osu.Game.Overlays.Mods
Mod = mod;
}
protected override void LoadComplete()
{
base.LoadComplete();
public virtual ITooltip<Mod> GetCustomTooltip() => new ModButtonTooltip();
selectedMods.BindValueChanged(_ => Scheduler.AddOnce(updateCompatibility), true);
}
public ITooltip GetCustomTooltip() => new ModButtonTooltip();
public object TooltipContent => SelectedMod ?? Mods.FirstOrDefault();
public Mod TooltipContent => SelectedMod ?? Mods.FirstOrDefault();
}
}

View File

@ -1,33 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Overlays.Mods
{
public class ModButtonTooltip : VisibilityContainer, ITooltip
public class ModButtonTooltip : VisibilityContainer, ITooltip<Mod>
{
private readonly OsuSpriteText descriptionText;
private readonly Box background;
private readonly OsuSpriteText incompatibleText;
private readonly Bindable<IReadOnlyList<Mod>> incompatibleMods = new Bindable<IReadOnlyList<Mod>>();
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; }
protected override Container<Drawable> Content { get; }
public ModButtonTooltip()
{
@ -35,13 +26,13 @@ namespace osu.Game.Overlays.Mods
Masking = true;
CornerRadius = 5;
Children = new Drawable[]
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
Content = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
@ -51,19 +42,7 @@ namespace osu.Game.Overlays.Mods
descriptionText = new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Margin = new MarginPadding { Bottom = 5 }
},
incompatibleText = new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = "Incompatible with:"
},
new ModDisplay
{
Current = incompatibleMods,
ExpansionMode = ExpansionMode.AlwaysExpanded,
Scale = new Vector2(0.7f)
}
}
},
};
@ -74,7 +53,6 @@ namespace osu.Game.Overlays.Mods
{
background.Colour = colours.Gray3;
descriptionText.Colour = colours.BlueLighter;
incompatibleText.Colour = colours.BlueLight;
}
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
@ -82,32 +60,19 @@ namespace osu.Game.Overlays.Mods
private Mod lastMod;
public bool SetContent(object content)
public void SetContent(Mod mod)
{
if (!(content is Mod mod))
return false;
if (mod.Equals(lastMod)) return true;
if (mod.Equals(lastMod))
return;
lastMod = mod;
UpdateDisplay(mod);
}
protected virtual void UpdateDisplay(Mod mod)
{
descriptionText.Text = mod.Description;
var incompatibleTypes = mod.IncompatibleMods;
var allMods = ruleset.Value.CreateInstance().GetAllMods();
incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).ToList();
if (!incompatibleMods.Value.Any())
{
incompatibleText.Text = "Compatible with all mods";
return true;
}
incompatibleText.Text = "Incompatible with:";
return true;
}
public void Move(Vector2 pos) => Position = pos;

View File

@ -51,14 +51,14 @@ namespace osu.Game.Overlays.Mods
if (m == null)
return new ModButtonEmpty();
return new ModButton(m)
return CreateModButton(m).With(b =>
{
SelectionChanged = mod =>
b.SelectionChanged = mod =>
{
ModButtonStateChanged(mod);
Action?.Invoke(mod);
},
};
};
});
}).ToArray();
modsLoadCts?.Cancel();
@ -247,6 +247,8 @@ namespace osu.Game.Overlays.Mods
Text = text
};
protected virtual ModButton CreateModButton(Mod mod) => new ModButton(mod);
/// <summary>
/// Play out all remaining animations immediately to leave mods in a good (final) state.
/// </summary>

View File

@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods
{
public class LocalPlayerModSelectOverlay : ModSelectOverlay
public class UserModSelectOverlay : ModSelectOverlay
{
protected override void OnModSelected(Mod mod)
{
@ -14,5 +14,17 @@ namespace osu.Game.Overlays.Mods
foreach (var section in ModSectionsContainer.Children)
section.DeselectTypes(mod.IncompatibleMods, true, mod);
}
protected override ModSection CreateModSection(ModType type) => new UserModSection(type);
private class UserModSection : ModSection
{
public UserModSection(ModType type)
: base(type)
{
}
protected override ModButton CreateModButton(Mod mod) => new IncompatibilityDisplayingModButton(mod);
}
}
}

View File

@ -123,12 +123,8 @@ namespace osu.Game.Overlays.News
main.AddText(post.Author, t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold));
}
private class DateContainer : CircularContainer, IHasCustomTooltip
private class DateContainer : CircularContainer, IHasCustomTooltip<DateTimeOffset>
{
public ITooltip GetCustomTooltip() => new DateTooltip();
public object TooltipContent => date;
private readonly DateTimeOffset date;
public DateContainer(DateTimeOffset date)
@ -162,6 +158,10 @@ namespace osu.Game.Overlays.News
}
protected override bool OnClick(ClickEvent e) => true; // Protects the NewsCard from clicks while hovering DateContainer
ITooltip<DateTimeOffset> IHasCustomTooltip<DateTimeOffset>.GetCustomTooltip() => new DateTooltip();
DateTimeOffset IHasCustomTooltip<DateTimeOffset>.TooltipContent => date;
}
}
}

View File

@ -8,8 +8,8 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Overlays.Notifications;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Threading;
@ -30,10 +30,8 @@ namespace osu.Game.Overlays
private FlowContainer<NotificationSection> sections;
/// <summary>
/// Provide a source for the toolbar height.
/// </summary>
public Func<float> GetToolbarHeight;
[Resolved]
private AudioManager audio { get; set; }
[BackgroundDependencyLoader]
private void load()
@ -104,14 +102,18 @@ namespace osu.Game.Overlays
private int runningDepth;
private void notificationClosed() => updateCounts();
private readonly Scheduler postScheduler = new Scheduler();
public override bool IsPresent => base.IsPresent || postScheduler.HasPendingTasks;
private bool processingPosts = true;
private double? lastSamplePlayback;
/// <summary>
/// Post a new notification for display.
/// </summary>
/// <param name="notification">The notification to display.</param>
public void Post(Notification notification) => postScheduler.Add(() =>
{
++runningDepth;
@ -130,11 +132,13 @@ namespace osu.Game.Overlays
Show();
updateCounts();
playDebouncedSample(notification.PopInSampleName);
});
protected override void Update()
{
base.Update();
if (processingPosts)
postScheduler.Update();
}
@ -157,6 +161,24 @@ namespace osu.Game.Overlays
this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);
}
private void notificationClosed()
{
updateCounts();
// this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it.
// popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment.
playDebouncedSample("UI/overlay-pop-out");
}
private void playDebouncedSample(string sampleName)
{
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
{
audio.Samples.Get(sampleName)?.Play();
lastSamplePlayback = Time.Current;
}
}
private void updateCounts()
{
UnreadCount.Value = sections.Select(c => c.UnreadCount).Sum();
@ -168,12 +190,5 @@ namespace osu.Game.Overlays
updateCounts();
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 };
}
}
}

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