mirror of
https://github.com/ppy/osu.git
synced 2024-09-21 21:27:24 +08:00
Merge branch 'master' into bubble_mod_implementation_clean
This commit is contained in:
commit
7b34607676
@ -1,7 +1,7 @@
|
||||
<!-- Contains required properties for osu!framework projects. -->
|
||||
<Project>
|
||||
<PropertyGroup Label="C#">
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
@ -3,15 +3,53 @@
|
||||
#
|
||||
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
|
||||
|
||||
$CSPROJ="osu.Game/osu.Game.csproj"
|
||||
$GAME_CSPROJ="osu.Game/osu.Game.csproj"
|
||||
$ANDROID_PROPS="osu.Android.props"
|
||||
$IOS_PROPS="osu.iOS.props"
|
||||
$SLN="osu.sln"
|
||||
|
||||
dotnet remove $CSPROJ package ppy.osu.Framework;
|
||||
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj;
|
||||
dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
|
||||
dotnet remove $GAME_CSPROJ reference ppy.osu.Framework;
|
||||
dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android;
|
||||
dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS;
|
||||
|
||||
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj `
|
||||
../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj `
|
||||
../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj `
|
||||
../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj;
|
||||
|
||||
dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj;
|
||||
dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj;
|
||||
dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj;
|
||||
|
||||
# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files
|
||||
(Get-Content "osu.Android.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.Android.props"
|
||||
(Get-Content "osu.iOS.props") -replace "`"..\\osu-framework", "`"`$(MSBuildThisFileDirectory)..\osu-framework" | Set-Content "osu.iOS.props"
|
||||
|
||||
# needed because iOS framework nupkg includes a set of properties to work around certain issues during building,
|
||||
# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference.
|
||||
(Get-Content "osu.iOS.props") |
|
||||
Foreach-Object {
|
||||
if ($_ -match "</Project>")
|
||||
{
|
||||
" <Import Project=`"`$(MSBuildThisFileDirectory)../osu-framework/osu.Framework.iOS.props`"/>"
|
||||
}
|
||||
|
||||
$_
|
||||
} | Set-Content "osu.iOS.props"
|
||||
|
||||
$TMP=New-TemporaryFile
|
||||
|
||||
$SLNF=Get-Content "osu.Desktop.slnf" | ConvertFrom-Json
|
||||
$TMP=New-TemporaryFile
|
||||
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj")
|
||||
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
|
||||
Move-Item -Path $TMP -Destination "osu.Desktop.slnf" -Force
|
||||
|
||||
$SLNF=Get-Content "osu.Android.slnf" | ConvertFrom-Json
|
||||
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj")
|
||||
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
|
||||
Move-Item -Path $TMP -Destination "osu.Android.slnf" -Force
|
||||
|
||||
$SLNF=Get-Content "osu.iOS.slnf" | ConvertFrom-Json
|
||||
$SLNF.solution.projects += ("../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj")
|
||||
ConvertTo-Json $SLNF | Out-File $TMP -Encoding UTF8
|
||||
Move-Item -Path $TMP -Destination "osu.iOS.slnf" -Force
|
||||
|
@ -5,14 +5,41 @@
|
||||
#
|
||||
# https://github.com/ppy/osu-framework/wiki/Testing-local-framework-checkout-with-other-projects
|
||||
|
||||
CSPROJ="osu.Game/osu.Game.csproj"
|
||||
GAME_CSPROJ="osu.Game/osu.Game.csproj"
|
||||
ANDROID_PROPS="osu.Android.props"
|
||||
IOS_PROPS="osu.iOS.props"
|
||||
SLN="osu.sln"
|
||||
|
||||
dotnet remove $CSPROJ package ppy.osu.Framework
|
||||
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj ../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj
|
||||
dotnet add $CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
|
||||
dotnet remove $GAME_CSPROJ reference ppy.osu.Framework
|
||||
dotnet remove $ANDROID_PROPS reference ppy.osu.Framework.Android
|
||||
dotnet remove $IOS_PROPS reference ppy.osu.Framework.iOS
|
||||
|
||||
dotnet sln $SLN add ../osu-framework/osu.Framework/osu.Framework.csproj \
|
||||
../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj \
|
||||
../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj \
|
||||
../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj
|
||||
|
||||
dotnet add $GAME_CSPROJ reference ../osu-framework/osu.Framework/osu.Framework.csproj
|
||||
dotnet add $ANDROID_PROPS reference ../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj
|
||||
dotnet add $IOS_PROPS reference ../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj
|
||||
|
||||
# workaround for dotnet add not inserting $(MSBuildThisFileDirectory) on props files
|
||||
sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.Android.props && rm osu.Android.props.bak
|
||||
sed -i.bak 's:"..\\osu-framework:"$(MSBuildThisFileDirectory)..\\osu-framework:g' ./osu.iOS.props && rm osu.iOS.props.bak
|
||||
|
||||
# needed because iOS framework nupkg includes a set of properties to work around certain issues during building,
|
||||
# and those get ignored when referencing framework via project, threfore we have to manually include it via props reference.
|
||||
sed -i.bak '/<\/Project>/i\
|
||||
<Import Project=\"$(MSBuildThisFileDirectory)../osu-framework/osu.Framework.iOS.props\"/>\
|
||||
' ./osu.iOS.props && rm osu.iOS.props.bak
|
||||
|
||||
SLNF="osu.Desktop.slnf"
|
||||
tmp=$(mktemp)
|
||||
|
||||
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj"]' osu.Desktop.slnf > $tmp
|
||||
mv -f $tmp $SLNF
|
||||
mv -f $tmp osu.Desktop.slnf
|
||||
|
||||
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.Android/osu.Framework.Android.csproj"]' osu.Android.slnf > $tmp
|
||||
mv -f $tmp osu.Android.slnf
|
||||
|
||||
jq '.solution.projects += ["../osu-framework/osu.Framework/osu.Framework.csproj", "../osu-framework/osu.Framework.NativeLibs/osu.Framework.NativeLibs.csproj", "../osu-framework/osu.Framework.iOS/osu.Framework.iOS.csproj"]' osu.iOS.slnf > $tmp
|
||||
mv -f $tmp osu.iOS.slnf
|
||||
|
@ -8,9 +8,13 @@
|
||||
<!-- NullabilityInfoContextSupport is disabled by default for Android -->
|
||||
<NullabilityInfoContextSupport>true</NullabilityInfoContextSupport>
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.131.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.314.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
15
osu.Android/Properties/AndroidManifestOverlay.xml
Normal file
15
osu.Android/Properties/AndroidManifestOverlay.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="mailto" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
123
osu.Game.Benchmarks/BenchmarkCarouselFilter.cs
Normal file
123
osu.Game.Benchmarks/BenchmarkCarouselFilter.cs
Normal file
@ -0,0 +1,123 @@
|
||||
// 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 BenchmarkDotNet.Attributes;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
|
||||
namespace osu.Game.Benchmarks
|
||||
{
|
||||
public class BenchmarkCarouselFilter : BenchmarkTest
|
||||
{
|
||||
private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
|
||||
{
|
||||
Ruleset = new RulesetInfo
|
||||
{
|
||||
ShortName = "osu",
|
||||
OnlineID = 0
|
||||
},
|
||||
StarRating = 4.0d,
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
ApproachRate = 5.0f,
|
||||
DrainRate = 3.0f,
|
||||
CircleSize = 2.0f,
|
||||
},
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Artist = "The Artist",
|
||||
ArtistUnicode = "check unicode too",
|
||||
Title = "Title goes here",
|
||||
TitleUnicode = "Title goes here",
|
||||
Author = { Username = "The Author" },
|
||||
Source = "unit tests",
|
||||
Tags = "look for tags too",
|
||||
},
|
||||
DifficultyName = "version as well",
|
||||
Length = 2500,
|
||||
BPM = 160,
|
||||
BeatDivisor = 12,
|
||||
Status = BeatmapOnlineStatus.Loved
|
||||
};
|
||||
|
||||
private CarouselBeatmap carouselBeatmap = null!;
|
||||
private FilterCriteria criteria1 = null!;
|
||||
private FilterCriteria criteria2 = null!;
|
||||
private FilterCriteria criteria3 = null!;
|
||||
private FilterCriteria criteria4 = null!;
|
||||
private FilterCriteria criteria5 = null!;
|
||||
private FilterCriteria criteria6 = null!;
|
||||
|
||||
public override void SetUp()
|
||||
{
|
||||
var beatmap = getExampleBeatmap();
|
||||
beatmap.OnlineID = 20201010;
|
||||
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineID = 1535 };
|
||||
carouselBeatmap = new CarouselBeatmap(beatmap);
|
||||
criteria1 = new FilterCriteria();
|
||||
criteria2 = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { ShortName = "catch" }
|
||||
};
|
||||
criteria3 = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { OnlineID = 6 },
|
||||
AllowConvertedBeatmaps = true,
|
||||
BPM = new FilterCriteria.OptionalRange<double>
|
||||
{
|
||||
IsUpperInclusive = false,
|
||||
Max = 160d
|
||||
}
|
||||
};
|
||||
criteria4 = new FilterCriteria
|
||||
{
|
||||
Ruleset = new RulesetInfo { OnlineID = 6 },
|
||||
AllowConvertedBeatmaps = true,
|
||||
SearchText = "an artist"
|
||||
};
|
||||
criteria5 = new FilterCriteria
|
||||
{
|
||||
Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = "the author AND then something else" }
|
||||
};
|
||||
criteria6 = new FilterCriteria { SearchText = "20201010" };
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CarouselBeatmapFilter()
|
||||
{
|
||||
carouselBeatmap.Filter(criteria1);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CriteriaMatchingSpecificRuleset()
|
||||
{
|
||||
carouselBeatmap.Filter(criteria2);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CriteriaMatchingRangeMax()
|
||||
{
|
||||
carouselBeatmap.Filter(criteria3);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CriteriaMatchingTerms()
|
||||
{
|
||||
carouselBeatmap.Filter(criteria4);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CriteriaMatchingCreator()
|
||||
{
|
||||
carouselBeatmap.Filter(criteria5);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public void CriteriaMatchingBeatmapIDs()
|
||||
{
|
||||
carouselBeatmap.Filter(criteria6);
|
||||
}
|
||||
}
|
||||
}
|
@ -167,8 +167,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
if (bodySprite != null)
|
||||
{
|
||||
bodySprite.Origin = Anchor.BottomCentre;
|
||||
bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y) * -1);
|
||||
bodySprite.Origin = Anchor.TopCentre;
|
||||
bodySprite.Anchor = Anchor.BottomCentre; // needs to be flipped due to scale flip in Update.
|
||||
}
|
||||
|
||||
if (light != null)
|
||||
@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
if (bodySprite != null)
|
||||
{
|
||||
bodySprite.Origin = Anchor.TopCentre;
|
||||
bodySprite.Scale = new Vector2(bodySprite.Scale.X, Math.Abs(bodySprite.Scale.Y));
|
||||
bodySprite.Anchor = Anchor.TopCentre;
|
||||
}
|
||||
|
||||
if (light != null)
|
||||
@ -211,11 +211,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
base.Update();
|
||||
missFadeTime.Value ??= holdNote.HoldBrokenTime;
|
||||
|
||||
int scaleDirection = (direction.Value == ScrollingDirection.Down ? 1 : -1);
|
||||
|
||||
// here we go...
|
||||
switch (bodyStyle)
|
||||
{
|
||||
case LegacyNoteBodyStyle.Stretch:
|
||||
// this is how lazer works by default. nothing required.
|
||||
if (bodySprite != null)
|
||||
bodySprite.Scale = new Vector2(1, scaleDirection);
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -228,7 +232,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
bodySprite.FillMode = FillMode.Stretch;
|
||||
// i dunno this looks about right??
|
||||
bodySprite.Scale = new Vector2(1, 32800 / sprite.DrawHeight);
|
||||
bodySprite.Scale = new Vector2(1, scaleDirection * 32800 / sprite.DrawHeight);
|
||||
}
|
||||
|
||||
break;
|
||||
|
156
osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs
Normal file
156
osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs
Normal file
@ -0,0 +1,156 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
public partial class TestSceneHitCircleLateFade : OsuTestScene
|
||||
{
|
||||
private float? alphaAtMiss;
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleClassicMod()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = new Mod[] { new OsuModClassic() };
|
||||
createCircle();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleClassicAndFullHiddenMods()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModClassic() };
|
||||
createCircle();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleClassicAndApproachCircleOnlyHiddenMods()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = new Mod[] { new OsuModHidden { OnlyFadeApproachCircles = { Value = true } }, new OsuModClassic() };
|
||||
createCircle();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitCircleNoMod()
|
||||
{
|
||||
AddStep("Create hit circle", () =>
|
||||
{
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
createCircle();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Opaque when missed", () => alphaAtMiss == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderClassicMod()
|
||||
{
|
||||
AddStep("Create slider", () =>
|
||||
{
|
||||
SelectedMods.Value = new Mod[] { new OsuModClassic() };
|
||||
createSlider();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Head circle transparent when missed", () => alphaAtMiss == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderNoMod()
|
||||
{
|
||||
AddStep("Create slider", () =>
|
||||
{
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
createSlider();
|
||||
});
|
||||
|
||||
AddUntilStep("Wait until head circle is missed", () => alphaAtMiss.IsNotNull());
|
||||
AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1);
|
||||
}
|
||||
|
||||
private void createCircle()
|
||||
{
|
||||
alphaAtMiss = null;
|
||||
|
||||
DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle
|
||||
{
|
||||
StartTime = Time.Current + 500,
|
||||
Position = new Vector2(250)
|
||||
});
|
||||
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawableHitCircle);
|
||||
|
||||
drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
drawableHitCircle.OnNewResult += (_, _) =>
|
||||
{
|
||||
alphaAtMiss = drawableHitCircle.Alpha;
|
||||
};
|
||||
|
||||
Child = drawableHitCircle;
|
||||
}
|
||||
|
||||
private void createSlider()
|
||||
{
|
||||
alphaAtMiss = null;
|
||||
|
||||
DrawableSlider drawableSlider = new DrawableSlider(new Slider
|
||||
{
|
||||
StartTime = Time.Current + 500,
|
||||
Position = new Vector2(250),
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(0, 100),
|
||||
})
|
||||
});
|
||||
|
||||
drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
drawableSlider.OnLoadComplete += _ =>
|
||||
{
|
||||
foreach (var mod in SelectedMods.Value.OfType<IApplicableToDrawableHitObject>())
|
||||
mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle);
|
||||
|
||||
drawableSlider.HeadCircle.OnNewResult += (_, _) =>
|
||||
{
|
||||
alphaAtMiss = drawableSlider.HeadCircle.Alpha;
|
||||
};
|
||||
};
|
||||
Child = drawableSlider;
|
||||
}
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private const double flash_duration = 1000;
|
||||
|
||||
private DrawableRuleset<OsuHitObject> ruleset = null!;
|
||||
private DrawableOsuRuleset ruleset = null!;
|
||||
|
||||
protected OsuAction? LastAcceptedAction { get; private set; }
|
||||
|
||||
@ -42,8 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
ruleset = drawableRuleset;
|
||||
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
|
||||
ruleset = (DrawableOsuRuleset)drawableRuleset;
|
||||
ruleset.KeyBindingInputManager.Add(new InputInterceptor(this));
|
||||
|
||||
var periods = new List<Period>();
|
||||
|
||||
|
@ -11,6 +11,7 @@ using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
@ -55,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// Grab the input manager to disable the user's cursor, and for future use
|
||||
inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
|
||||
inputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager;
|
||||
inputManager.AllowUserCursorMovement = false;
|
||||
|
||||
// Generate the replay frames the cursor should follow
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -11,6 +12,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
@ -31,6 +33,11 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
[SettingSource("Always play a slider's tail sample", "Always plays a slider's tail sample regardless of whether it was hit or not.")]
|
||||
public Bindable<bool> AlwaysPlayTailSample { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("Fade out hit circles earlier", "Make hit circles fade out into a miss, rather than after it.")]
|
||||
public Bindable<bool> FadeHitCircleEarly { get; } = new Bindable<bool>(true);
|
||||
|
||||
private bool usingHiddenFading;
|
||||
|
||||
public void ApplyToHitObject(HitObject hitObject)
|
||||
{
|
||||
switch (hitObject)
|
||||
@ -51,6 +58,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
if (ClassicNoteLock.Value)
|
||||
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
|
||||
|
||||
usingHiddenFading = drawableRuleset.Mods.OfType<OsuModHidden>().SingleOrDefault()?.OnlyFadeApproachCircles.Value == false;
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject obj)
|
||||
@ -59,12 +68,32 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
case DrawableSliderHead head:
|
||||
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(head);
|
||||
break;
|
||||
|
||||
case DrawableSliderTail tail:
|
||||
tail.SamplePlaysOnlyOnHit = !AlwaysPlayTailSample.Value;
|
||||
break;
|
||||
}
|
||||
|
||||
case DrawableHitCircle circle:
|
||||
if (FadeHitCircleEarly.Value && !usingHiddenFading)
|
||||
applyEarlyFading(circle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void applyEarlyFading(DrawableHitCircle circle)
|
||||
{
|
||||
circle.ApplyCustomUpdateState += (o, _) =>
|
||||
{
|
||||
using (o.BeginAbsoluteSequence(o.StateUpdateTime))
|
||||
{
|
||||
double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok);
|
||||
double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow;
|
||||
o.Delay(okWindow).FadeOut(lateMissFadeTime);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// grab the input manager for future use.
|
||||
osuInputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager;
|
||||
osuInputManager = ((DrawableOsuRuleset)drawableRuleset).KeyBindingInputManager;
|
||||
}
|
||||
|
||||
public void ApplyToPlayer(Player player)
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableOsuJudgement : DrawableJudgement
|
||||
{
|
||||
protected SkinnableLighting Lighting { get; private set; }
|
||||
internal SkinnableLighting Lighting { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
@ -10,7 +10,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class SkinnableLighting : SkinnableSprite
|
||||
internal partial class SkinnableLighting : SkinnableSprite
|
||||
{
|
||||
private DrawableHitObject targetObject;
|
||||
private JudgementResult targetResult;
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
@ -255,15 +256,23 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
Source.parts.CopyTo(parts, 0);
|
||||
}
|
||||
|
||||
private IUniformBuffer<CursorTrailParameters> cursorTrailParameters;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
vertexBatch ??= renderer.CreateQuadBatch<TexturedTrailVertex>(max_sprites, 1);
|
||||
|
||||
cursorTrailParameters ??= renderer.CreateUniformBuffer<CursorTrailParameters>();
|
||||
cursorTrailParameters.Data = cursorTrailParameters.Data with
|
||||
{
|
||||
FadeClock = time,
|
||||
FadeExponent = fadeExponent
|
||||
};
|
||||
|
||||
shader.Bind();
|
||||
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
|
||||
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
|
||||
shader.BindUniformBlock("m_CursorTrailParameters", cursorTrailParameters);
|
||||
|
||||
texture.Bind();
|
||||
|
||||
@ -323,6 +332,15 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
vertexBatch?.Dispose();
|
||||
cursorTrailParameters?.Dispose();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private record struct CursorTrailParameters
|
||||
{
|
||||
public UniformFloat FadeClock;
|
||||
public UniformFloat FadeExponent;
|
||||
private readonly UniformPadding8 pad1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
{
|
||||
protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config;
|
||||
|
||||
public new OsuInputManager KeyBindingInputManager => (OsuInputManager)base.KeyBindingInputManager;
|
||||
|
||||
public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield;
|
||||
|
||||
public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||
|
@ -4,6 +4,7 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Description>click the circles. to the beat.</Description>
|
||||
<LangVersion>10</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget">
|
||||
|
212
osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs
Normal file
212
osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModSingleTap.cs
Normal file
@ -0,0 +1,212 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Mods
|
||||
{
|
||||
public partial class TestSceneTaikoModSingleTap : TaikoModTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestInputAlternate() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 300,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 500,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 700,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(120),
|
||||
new TaikoReplayFrame(300, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(320),
|
||||
new TaikoReplayFrame(500, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(520),
|
||||
new TaikoReplayFrame(700, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(720),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestInputSameKey() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 300,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 500,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 700,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(120),
|
||||
new TaikoReplayFrame(300, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(320),
|
||||
new TaikoReplayFrame(500, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(520),
|
||||
new TaikoReplayFrame(700, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(720),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 4
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestInputIntro() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(0, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(20),
|
||||
new TaikoReplayFrame(100, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(120),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestInputStrong() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 300,
|
||||
Type = HitType.Rim,
|
||||
IsStrong = true
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 500,
|
||||
Type = HitType.Rim,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(120),
|
||||
new TaikoReplayFrame(300, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(320),
|
||||
new TaikoReplayFrame(500, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(520),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestInputBreaks() => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new TaikoModSingleTap(),
|
||||
Autoplay = false,
|
||||
Beatmap = new Beatmap
|
||||
{
|
||||
Breaks = new List<BreakPeriod>
|
||||
{
|
||||
new BreakPeriod(100, 1600),
|
||||
},
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Hit
|
||||
{
|
||||
StartTime = 100,
|
||||
Type = HitType.Rim
|
||||
},
|
||||
new Hit
|
||||
{
|
||||
StartTime = 2000,
|
||||
Type = HitType.Rim,
|
||||
},
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new TaikoReplayFrame(100, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(120),
|
||||
// Press different key after break but before hit object.
|
||||
new TaikoReplayFrame(1900, TaikoAction.LeftRim),
|
||||
new TaikoReplayFrame(1820),
|
||||
// Press original key at second hitobject and ensure it has been hit.
|
||||
new TaikoReplayFrame(2000, TaikoAction.RightRim),
|
||||
new TaikoReplayFrame(2020),
|
||||
},
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2
|
||||
});
|
||||
}
|
||||
}
|
@ -73,11 +73,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
beatmap.ControlPointInfo.Add(start_time, new TimingControlPoint
|
||||
{
|
||||
BeatLength = beat_length,
|
||||
TimeSignature = new TimeSignature(time_signature_numerator)
|
||||
TimeSignature = new TimeSignature(time_signature_numerator),
|
||||
OmitFirstBarLine = true
|
||||
});
|
||||
|
||||
beatmap.ControlPointInfo.Add(start_time, new EffectControlPoint { OmitFirstBarLine = true });
|
||||
|
||||
var barlines = new BarLineGenerator<BarLine>(beatmap).BarLines;
|
||||
|
||||
AddAssert("first barline ommited", () => barlines.All(b => b.StartTime != start_time));
|
||||
|
@ -72,7 +72,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
||||
converted.ControlPointInfo.Add(hitObject.StartTime, new EffectControlPoint
|
||||
{
|
||||
KiaiMode = currentEffectPoint.KiaiMode,
|
||||
OmitFirstBarLine = currentEffectPoint.OmitFirstBarLine,
|
||||
ScrollSpeed = lastScrollSpeed = nextScrollSpeed,
|
||||
});
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
@ -12,5 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
@ -13,5 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
using System.Linq;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
@ -8,6 +10,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public class TaikoModRelax : ModRelax
|
||||
{
|
||||
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's.";
|
||||
public override LocalisableString Description => @"No ninja-like spinners, demanding drumrolls or unexpected katus.";
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(TaikoModSingleTap) }).ToArray();
|
||||
}
|
||||
}
|
||||
|
127
osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs
Normal file
127
osu.Game.Rulesets.Taiko/Mods/TaikoModSingleTap.cs
Normal file
@ -0,0 +1,127 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Utils;
|
||||
using osu.Game.Rulesets.Taiko.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
public partial class TaikoModSingleTap : Mod, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield
|
||||
{
|
||||
public override string Name => @"Single Tap";
|
||||
public override string Acronym => @"SG";
|
||||
public override LocalisableString Description => @"One key for dons, one key for kats.";
|
||||
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(TaikoModCinema) };
|
||||
public override ModType Type => ModType.Conversion;
|
||||
|
||||
private DrawableTaikoRuleset ruleset = null!;
|
||||
|
||||
private TaikoPlayfield playfield { get; set; } = null!;
|
||||
|
||||
private TaikoAction? lastAcceptedCentreAction { get; set; }
|
||||
private TaikoAction? lastAcceptedRimAction { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A tracker for periods where single tap should not be enforced (i.e. non-gameplay periods).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
|
||||
/// </remarks>
|
||||
private PeriodTracker nonGameplayPeriods = null!;
|
||||
|
||||
private IFrameStableClock gameplayClock = null!;
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||
{
|
||||
ruleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||
ruleset.KeyBindingInputManager.Add(new InputInterceptor(this));
|
||||
playfield = (TaikoPlayfield)ruleset.Playfield;
|
||||
|
||||
var periods = new List<Period>();
|
||||
|
||||
if (drawableRuleset.Objects.Any())
|
||||
{
|
||||
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
|
||||
|
||||
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
|
||||
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
|
||||
|
||||
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
|
||||
}
|
||||
|
||||
nonGameplayPeriods = new PeriodTracker(periods);
|
||||
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
if (!nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime)) return;
|
||||
|
||||
lastAcceptedCentreAction = null;
|
||||
lastAcceptedRimAction = null;
|
||||
}
|
||||
|
||||
private bool checkCorrectAction(TaikoAction action)
|
||||
{
|
||||
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
return true;
|
||||
|
||||
// If next hit object is strong, allow usage of all actions. Strong drumrolls are ignored in this check.
|
||||
if (playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject is TaikoStrongableHitObject hitObject
|
||||
&& hitObject.IsStrong
|
||||
&& hitObject is not DrumRoll)
|
||||
return true;
|
||||
|
||||
if ((action == TaikoAction.LeftCentre || action == TaikoAction.RightCentre)
|
||||
&& (lastAcceptedCentreAction == null || lastAcceptedCentreAction == action))
|
||||
{
|
||||
lastAcceptedCentreAction = action;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((action == TaikoAction.LeftRim || action == TaikoAction.RightRim)
|
||||
&& (lastAcceptedRimAction == null || lastAcceptedRimAction == action))
|
||||
{
|
||||
lastAcceptedRimAction = action;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private partial class InputInterceptor : Component, IKeyBindingHandler<TaikoAction>
|
||||
{
|
||||
private readonly TaikoModSingleTap mod;
|
||||
|
||||
public InputInterceptor(TaikoModSingleTap mod)
|
||||
{
|
||||
this.mod = mod;
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
|
||||
// if the pressed action is incorrect, block it from reaching gameplay.
|
||||
=> !mod.checkCorrectAction(e.Action);
|
||||
|
||||
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -158,6 +158,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
new TaikoModDifficultyAdjust(),
|
||||
new TaikoModClassic(),
|
||||
new TaikoModSwap(),
|
||||
new TaikoModSingleTap(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
|
@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
|
||||
|
||||
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
@ -181,16 +181,19 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.AreEqual(956, timingPoint.Time);
|
||||
Assert.AreEqual(329.67032967033, timingPoint.BeatLength);
|
||||
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
|
||||
Assert.IsFalse(timingPoint.OmitFirstBarLine);
|
||||
|
||||
timingPoint = controlPoints.TimingPointAt(48428);
|
||||
Assert.AreEqual(956, timingPoint.Time);
|
||||
Assert.AreEqual(329.67032967033d, timingPoint.BeatLength);
|
||||
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
|
||||
Assert.IsFalse(timingPoint.OmitFirstBarLine);
|
||||
|
||||
timingPoint = controlPoints.TimingPointAt(119637);
|
||||
Assert.AreEqual(119637, timingPoint.Time);
|
||||
Assert.AreEqual(659.340659340659, timingPoint.BeatLength);
|
||||
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
|
||||
Assert.IsFalse(timingPoint.OmitFirstBarLine);
|
||||
|
||||
var difficultyPoint = controlPoints.DifficultyPointAt(0);
|
||||
Assert.AreEqual(0, difficultyPoint.Time);
|
||||
@ -222,17 +225,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
var effectPoint = controlPoints.EffectPointAt(0);
|
||||
Assert.AreEqual(0, effectPoint.Time);
|
||||
Assert.IsFalse(effectPoint.KiaiMode);
|
||||
Assert.IsFalse(effectPoint.OmitFirstBarLine);
|
||||
|
||||
effectPoint = controlPoints.EffectPointAt(53703);
|
||||
Assert.AreEqual(53703, effectPoint.Time);
|
||||
Assert.IsTrue(effectPoint.KiaiMode);
|
||||
Assert.IsFalse(effectPoint.OmitFirstBarLine);
|
||||
|
||||
effectPoint = controlPoints.EffectPointAt(116637);
|
||||
Assert.AreEqual(95901, effectPoint.Time);
|
||||
Assert.IsFalse(effectPoint.KiaiMode);
|
||||
Assert.IsFalse(effectPoint.OmitFirstBarLine);
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,6 +273,28 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeOmitBarLineEffect()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("omit-barline-control-points.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo;
|
||||
|
||||
Assert.That(controlPoints.TimingPoints.Count, Is.EqualTo(6));
|
||||
Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(0));
|
||||
|
||||
Assert.That(controlPoints.TimingPointAt(500).OmitFirstBarLine, Is.False);
|
||||
Assert.That(controlPoints.TimingPointAt(1500).OmitFirstBarLine, Is.True);
|
||||
Assert.That(controlPoints.TimingPointAt(2500).OmitFirstBarLine, Is.False);
|
||||
Assert.That(controlPoints.TimingPointAt(3500).OmitFirstBarLine, Is.False);
|
||||
Assert.That(controlPoints.TimingPointAt(4500).OmitFirstBarLine, Is.False);
|
||||
Assert.That(controlPoints.TimingPointAt(5500).OmitFirstBarLine, Is.True);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTimingPointResetsSpeedMultiplier()
|
||||
{
|
||||
|
125
osu.Game.Tests/Database/LegacyExporterTest.cs
Normal file
125
osu.Game.Tests/Database/LegacyExporterTest.cs
Normal file
@ -0,0 +1,125 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class LegacyExporterTest
|
||||
{
|
||||
private TestLegacyExporter legacyExporter = null!;
|
||||
private TemporaryNativeStorage storage = null!;
|
||||
|
||||
private const string short_filename = "normal file name";
|
||||
|
||||
private const string long_filename =
|
||||
"some file with super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name super long name";
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
storage = new TemporaryNativeStorage("export-storage");
|
||||
legacyExporter = new TestLegacyExporter(storage);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithNormalNameTest()
|
||||
{
|
||||
var item = new TestPathInfo(short_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
exportItemAndAssert(item, short_filename);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithNormalNameMultipleTimesTest()
|
||||
{
|
||||
var item = new TestPathInfo(short_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.LessThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
//Export multiple times
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
string expectedFileName = i == 0 ? short_filename : $"{short_filename} ({i})";
|
||||
exportItemAndAssert(item, expectedFileName);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithSuperLongNameTest()
|
||||
{
|
||||
int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
string expectedName = long_filename.Remove(expectedLength);
|
||||
|
||||
var item = new TestPathInfo(long_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
exportItemAndAssert(item, expectedName);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ExportFileWithSuperLongNameMultipleTimesTest()
|
||||
{
|
||||
int expectedLength = TestLegacyExporter.MAX_FILENAME_LENGTH - (legacyExporter.GetExtension().Length);
|
||||
string expectedName = long_filename.Remove(expectedLength);
|
||||
|
||||
var item = new TestPathInfo(long_filename);
|
||||
|
||||
Assert.That(item.Filename.Length, Is.GreaterThan(TestLegacyExporter.MAX_FILENAME_LENGTH));
|
||||
|
||||
//Export multiple times
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
string expectedFilename = i == 0 ? expectedName : $"{expectedName} ({i})";
|
||||
exportItemAndAssert(item, expectedFilename);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportItemAndAssert(IHasNamedFiles item, string expectedName)
|
||||
{
|
||||
Assert.DoesNotThrow(() => legacyExporter.Export(item));
|
||||
Assert.That(storage.Exists($"exports/{expectedName}{legacyExporter.GetExtension()}"), Is.True);
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
if (storage.IsNotNull())
|
||||
storage.Dispose();
|
||||
}
|
||||
|
||||
private class TestPathInfo : IHasNamedFiles
|
||||
{
|
||||
public string Filename { get; }
|
||||
|
||||
public IEnumerable<INamedFileUsage> Files { get; } = new List<INamedFileUsage>();
|
||||
|
||||
public TestPathInfo(string filename)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
public override string ToString() => Filename;
|
||||
}
|
||||
|
||||
private class TestLegacyExporter : LegacyExporter<IHasNamedFiles>
|
||||
{
|
||||
public TestLegacyExporter(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
|
||||
public string GetExtension() => FileExtension;
|
||||
|
||||
protected override string FileExtension => ".test";
|
||||
}
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ using osu.Game.Overlays.Settings;
|
||||
namespace osu.Game.Tests.Mods
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class SettingsSourceAttributeTest
|
||||
public partial class SettingSourceAttributeTest
|
||||
{
|
||||
[Test]
|
||||
public void TestOrdering()
|
@ -43,6 +43,18 @@ namespace osu.Game.Tests.NonVisual
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
|
||||
|
||||
cpi.Add(1200, new TimingControlPoint { OmitFirstBarLine = true }); // is not redundant
|
||||
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(3));
|
||||
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(3));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(3));
|
||||
|
||||
cpi.Add(1500, new TimingControlPoint { OmitFirstBarLine = true }); // is not redundant
|
||||
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(4));
|
||||
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(4));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(4));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -95,12 +107,12 @@ namespace osu.Game.Tests.NonVisual
|
||||
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
|
||||
|
||||
cpi.Add(1000, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // is not redundant
|
||||
cpi.Add(1400, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // same settings, but is not redundant
|
||||
cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
|
||||
cpi.Add(1400, new EffectControlPoint { KiaiMode = true }); // is redundant
|
||||
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(2));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
|
||||
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
|
||||
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -93,6 +94,7 @@ namespace osu.Game.Tests.NonVisual
|
||||
remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
|
||||
}
|
||||
|
||||
public override IAdjustableAudioComponent Audio { get; }
|
||||
public override Playfield Playfield { get; }
|
||||
public override Container Overlays { get; }
|
||||
public override Container FrameStableComponents { get; }
|
||||
|
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20230305.osk
Normal file
Binary file not shown.
27
osu.Game.Tests/Resources/omit-barline-control-points.osu
Normal file
27
osu.Game.Tests/Resources/omit-barline-control-points.osu
Normal file
@ -0,0 +1,27 @@
|
||||
osu file format v14
|
||||
|
||||
[TimingPoints]
|
||||
|
||||
// Uninherited: none, inherited: none
|
||||
0,500,4,2,0,100,1,0
|
||||
0,-50,4,3,0,100,0,0
|
||||
|
||||
// Uninherited: omit, inherited: none
|
||||
1000,500,4,2,0,100,1,8
|
||||
1000,-50,4,3,0,100,0,0
|
||||
|
||||
// Uninherited: none, inherited: omit (should be ignored, inheriting cannot omit)
|
||||
2000,500,4,2,0,100,1,0
|
||||
2000,-50,4,3,0,100,0,8
|
||||
|
||||
// Inherited: none, uninherited: none
|
||||
3000,-50,4,3,0,100,0,0
|
||||
3000,500,4,2,0,100,1,0
|
||||
|
||||
// Inherited: omit, uninherited: none (should be ignored, inheriting cannot omit)
|
||||
4000,-50,4,3,0,100,0,8
|
||||
4000,500,4,2,0,100,1,0
|
||||
|
||||
// Inherited: none, uninherited: omit
|
||||
5000,-50,4,3,0,100,0,0
|
||||
5000,500,4,2,0,100,1,8
|
@ -80,7 +80,7 @@ namespace osu.Game.Tests.Rulesets
|
||||
|
||||
dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore(parent.Get<GameHost>().Renderer));
|
||||
dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore());
|
||||
dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager(parent.Get<GameHost>().Renderer));
|
||||
dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager(parent.Get<GameHost>().Renderer, parent.Get<ShaderManager>()));
|
||||
|
||||
return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
|
||||
}
|
||||
@ -156,12 +156,15 @@ namespace osu.Game.Tests.Rulesets
|
||||
|
||||
private class TestShaderManager : ShaderManager
|
||||
{
|
||||
public TestShaderManager(IRenderer renderer)
|
||||
private readonly ShaderManager parentManager;
|
||||
|
||||
public TestShaderManager(IRenderer renderer, ShaderManager parentManager)
|
||||
: base(renderer, new ResourceStore<byte[]>())
|
||||
{
|
||||
this.parentManager = parentManager;
|
||||
}
|
||||
|
||||
public override byte[] LoadRaw(string name) => null;
|
||||
public override byte[] LoadRaw(string name) => parentManager.LoadRaw(name);
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
|
@ -133,6 +133,25 @@ namespace osu.Game.Tests.Skins.IO
|
||||
assertImportedOnce(import1, import2);
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestImportExportedNonAsciiSkinFilename() => runSkinTest(async osu =>
|
||||
{
|
||||
MemoryStream exportStream = new MemoryStream();
|
||||
|
||||
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 『1』", "author 1"), "custom.osk"));
|
||||
assertCorrectMetadata(import1, "name 『1』 [custom]", "author 1", osu);
|
||||
|
||||
import1.PerformRead(s =>
|
||||
{
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
});
|
||||
|
||||
string exportFilename = import1.GetDisplayString().GetValidFilename();
|
||||
|
||||
var import2 = await loadSkinIntoOsu(osu, new ImportTask(exportStream, $"{exportFilename}.osk"));
|
||||
assertCorrectMetadata(import2, "name 『1』 [custom]", "author 1", osu);
|
||||
});
|
||||
|
||||
[Test]
|
||||
public Task TestSameMetadataNameSameFolderName([Values] bool batchImport) => runSkinTest(async osu =>
|
||||
{
|
||||
|
@ -48,7 +48,9 @@ namespace osu.Game.Tests.Skins
|
||||
// Covers BPM counter.
|
||||
"Archives/modified-default-20221205.osk",
|
||||
// Covers judgement counter.
|
||||
"Archives/modified-default-20230117.osk"
|
||||
"Archives/modified-default-20230117.osk",
|
||||
// Covers player avatar and flag.
|
||||
"Archives/modified-argon-20230305.osk",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@ -66,9 +68,9 @@ namespace osu.Game.Tests.Skins
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
foreach (var target in skin.DrawableComponentInfo)
|
||||
foreach (var target in skin.LayoutInfos)
|
||||
{
|
||||
foreach (var info in target.Value)
|
||||
foreach (var info in target.Value.AllDrawables)
|
||||
instantiatedTypes.Add(info.Type);
|
||||
}
|
||||
}
|
||||
@ -87,8 +89,8 @@ namespace osu.Game.Tests.Skins
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents], Has.Length.EqualTo(9));
|
||||
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(9));
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,11 +102,11 @@ namespace osu.Game.Tests.Skins
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
|
||||
Assert.That(skin.DrawableComponentInfo, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents], Has.Length.EqualTo(6));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.SongSelect], Has.Length.EqualTo(1));
|
||||
Assert.That(skin.LayoutInfos, Has.Count.EqualTo(2));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(6));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.ToArray(), Has.Length.EqualTo(1));
|
||||
|
||||
var skinnableInfo = skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.SongSelect].First();
|
||||
var skinnableInfo = skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.SongSelect].AllDrawables.First();
|
||||
|
||||
Assert.That(skinnableInfo.Type, Is.EqualTo(typeof(SkinnableSprite)));
|
||||
Assert.That(skinnableInfo.Settings.First().Key, Is.EqualTo("sprite_name"));
|
||||
@ -115,10 +117,10 @@ namespace osu.Game.Tests.Skins
|
||||
using (var storage = new ZipArchiveReader(stream))
|
||||
{
|
||||
var skin = new TestSkin(new SkinInfo(), null, storage);
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents], Has.Length.EqualTo(8));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
|
||||
Assert.That(skin.DrawableComponentInfo[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.ToArray(), Has.Length.EqualTo(8));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(UnstableRateCounter)));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(ColourHitErrorMeter)));
|
||||
Assert.That(skin.LayoutInfos[SkinComponentsContainerLookup.TargetArea.MainHUDComponents].AllDrawables.Select(i => i.Type), Contains.Item(typeof(LegacySongProgress)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Background
|
||||
{
|
||||
@ -97,15 +98,29 @@ namespace osu.Game.Tests.Visual.Background
|
||||
texelSize = Source.texelSize;
|
||||
}
|
||||
|
||||
private IUniformBuffer<TriangleBorderData>? borderDataBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
TextureShader.GetUniform<float>("thickness").UpdateValue(ref thickness);
|
||||
TextureShader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
|
||||
borderDataBuffer ??= renderer.CreateUniformBuffer<TriangleBorderData>();
|
||||
borderDataBuffer.Data = borderDataBuffer.Data with
|
||||
{
|
||||
Thickness = thickness,
|
||||
TexelSize = texelSize
|
||||
};
|
||||
|
||||
TextureShader.BindUniformBlock("m_BorderData", borderDataBuffer);
|
||||
|
||||
base.Draw(renderer);
|
||||
}
|
||||
|
||||
protected override bool CanDrawOpaqueInterior => false;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
borderDataBuffer?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,16 +7,20 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Storyboards;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@ -36,13 +40,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
ControlPointInfo controlPointInfo = new LegacyControlPointInfo();
|
||||
|
||||
beatmap = new Beatmap
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Difficulty = new BeatmapDifficulty { CircleSize = 6, SliderMultiplier = 3 },
|
||||
Ruleset = ruleset
|
||||
}
|
||||
},
|
||||
ControlPointInfo = controlPointInfo
|
||||
};
|
||||
|
||||
const double start_offset = 8000;
|
||||
@ -51,7 +58,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
// intentionally start objects a bit late so we can test the case of no alive objects.
|
||||
double t = start_offset;
|
||||
|
||||
beatmap.HitObjects.AddRange(new[]
|
||||
beatmap.HitObjects.AddRange(new HitObject[]
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
@ -71,12 +78,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
},
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = t + spacing,
|
||||
StartTime = t += spacing,
|
||||
},
|
||||
new Slider
|
||||
{
|
||||
StartTime = t += spacing,
|
||||
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }),
|
||||
Samples = new[] { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) },
|
||||
SampleControlPoint = new SampleControlPoint { SampleBank = "soft" },
|
||||
},
|
||||
});
|
||||
|
||||
// Add a change in volume halfway through final slider.
|
||||
controlPointInfo.Add(t, new SampleControlPoint
|
||||
{
|
||||
SampleBank = "normal",
|
||||
SampleVolume = 20,
|
||||
});
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
@ -129,14 +148,36 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
waitForAliveObjectIndex(3);
|
||||
checkValidObjectIndex(3);
|
||||
|
||||
AddStep("Seek into future", () => Beatmap.Value.Track.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000));
|
||||
seekBeforeIndex(4);
|
||||
waitForAliveObjectIndex(4);
|
||||
|
||||
// Even before the object, we should prefer the first nested object's sample.
|
||||
// This is because the (parent) object will only play its sample at the final EndTime.
|
||||
AddAssert("check valid object is slider's first nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.First()));
|
||||
|
||||
AddStep("seek to just before slider ends", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[4].GetEndTime() - 100));
|
||||
waitForCatchUp();
|
||||
AddUntilStep("wait until valid object is slider's last nested", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4].NestedHitObjects.Last()));
|
||||
|
||||
// After we get far enough away, the samples of the object itself should be used, not any nested object.
|
||||
AddStep("seek to further after slider", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[4].GetEndTime() + 1000));
|
||||
waitForCatchUp();
|
||||
AddUntilStep("wait until valid object is slider itself", () => sampleTriggerSource.GetMostValidObject(), () => Is.EqualTo(beatmap.HitObjects[4]));
|
||||
|
||||
AddStep("Seek into future", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects.Last().GetEndTime() + 10000));
|
||||
waitForCatchUp();
|
||||
waitForAliveObjectIndex(null);
|
||||
checkValidObjectIndex(3);
|
||||
checkValidObjectIndex(4);
|
||||
}
|
||||
|
||||
private void seekBeforeIndex(int index) =>
|
||||
AddStep($"seek to just before object {index}", () => Beatmap.Value.Track.Seek(beatmap.HitObjects[index].StartTime - 100));
|
||||
private void seekBeforeIndex(int index)
|
||||
{
|
||||
AddStep($"seek to just before object {index}", () => Player.GameplayClockContainer.Seek(beatmap.HitObjects[index].StartTime - 100));
|
||||
waitForCatchUp();
|
||||
}
|
||||
|
||||
private void waitForCatchUp() =>
|
||||
AddUntilStep("wait for frame stable clock to catch up", () => Precision.AlmostEquals(Player.GameplayClockContainer.CurrentTime, Player.DrawableRuleset.FrameStableClock.CurrentTime));
|
||||
|
||||
private void waitForAliveObjectIndex(int? index)
|
||||
{
|
||||
|
@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -281,6 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
remove => throw new InvalidOperationException($"{nameof(RevertResult)} operations not supported in test context");
|
||||
}
|
||||
|
||||
public override IAdjustableAudioComponent Audio { get; }
|
||||
public override Playfield Playfield { get; }
|
||||
public override Container Overlays { get; }
|
||||
public override Container FrameStableComponents { get; }
|
||||
|
@ -1,9 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.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.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
@ -12,6 +14,7 @@ using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Input;
|
||||
@ -20,33 +23,85 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public partial class TestSceneSkinEditor : PlayerTestScene
|
||||
{
|
||||
private SkinEditor? skinEditor;
|
||||
private SkinEditor skinEditor = null!;
|
||||
|
||||
protected override bool Autoplay => true;
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
[Cached]
|
||||
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
||||
|
||||
private SkinComponentsContainer targetContainer => Player.ChildrenOfType<SkinComponentsContainer>().First();
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinComponentsContainer>().All(c => c.ComponentsLoaded));
|
||||
AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded);
|
||||
|
||||
AddStep("reload skin editor", () =>
|
||||
{
|
||||
skinEditor?.Expire();
|
||||
if (skinEditor.IsNotNull())
|
||||
skinEditor.Expire();
|
||||
Player.ScaleTo(0.4f);
|
||||
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => skinEditor!.IsLoaded);
|
||||
AddUntilStep("wait for loaded", () => skinEditor.IsLoaded);
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestBringToFront(bool alterSelectionOrder)
|
||||
{
|
||||
AddAssert("Ensure over three components available", () => targetContainer.Components.Count, () => Is.GreaterThan(3));
|
||||
|
||||
IEnumerable<ISerialisableDrawable> originalOrder = null!;
|
||||
|
||||
AddStep("Save order of components before operation", () => originalOrder = targetContainer.Components.Take(3).ToArray());
|
||||
|
||||
if (alterSelectionOrder)
|
||||
AddStep("Select first three components in reverse order", () => skinEditor.SelectedComponents.AddRange(originalOrder.Reverse()));
|
||||
else
|
||||
AddStep("Select first three components", () => skinEditor.SelectedComponents.AddRange(originalOrder));
|
||||
|
||||
AddAssert("Components are not front-most", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.Not.EqualTo(skinEditor.SelectedComponents));
|
||||
|
||||
AddStep("Bring to front", () => skinEditor.BringSelectionToFront());
|
||||
AddAssert("Ensure components are now front-most in original order", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.EqualTo(originalOrder));
|
||||
AddStep("Bring to front again", () => skinEditor.BringSelectionToFront());
|
||||
AddAssert("Ensure components are still front-most in original order", () => targetContainer.Components.TakeLast(3).ToArray(), () => Is.EqualTo(originalOrder));
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestSendToBack(bool alterSelectionOrder)
|
||||
{
|
||||
AddAssert("Ensure over three components available", () => targetContainer.Components.Count, () => Is.GreaterThan(3));
|
||||
|
||||
IEnumerable<ISerialisableDrawable> originalOrder = null!;
|
||||
|
||||
AddStep("Save order of components before operation", () => originalOrder = targetContainer.Components.TakeLast(3).ToArray());
|
||||
|
||||
if (alterSelectionOrder)
|
||||
AddStep("Select last three components in reverse order", () => skinEditor.SelectedComponents.AddRange(originalOrder.Reverse()));
|
||||
else
|
||||
AddStep("Select last three components", () => skinEditor.SelectedComponents.AddRange(originalOrder));
|
||||
|
||||
AddAssert("Components are not back-most", () => targetContainer.Components.Take(3).ToArray(), () => Is.Not.EqualTo(skinEditor.SelectedComponents));
|
||||
|
||||
AddStep("Send to back", () => skinEditor.SendSelectionToBack());
|
||||
AddAssert("Ensure components are now back-most in original order", () => targetContainer.Components.Take(3).ToArray(), () => Is.EqualTo(originalOrder));
|
||||
AddStep("Send to back again", () => skinEditor.SendSelectionToBack());
|
||||
AddAssert("Ensure components are still back-most in original order", () => targetContainer.Components.Take(3).ToArray(), () => Is.EqualTo(originalOrder));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestToggleEditor()
|
||||
{
|
||||
AddToggleStep("toggle editor visibility", _ => skinEditor!.ToggleVisibility());
|
||||
AddToggleStep("toggle editor visibility", _ => skinEditor.ToggleVisibility());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -59,7 +114,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter);
|
||||
|
||||
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
|
||||
skinEditor!.SelectedComponents.Clear();
|
||||
skinEditor.SelectedComponents.Clear();
|
||||
skinEditor.SelectedComponents.Add(blueprint.Item);
|
||||
});
|
||||
|
||||
|
@ -12,6 +12,7 @@ using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Gameplay;
|
||||
using osuTK.Input;
|
||||
@ -32,6 +33,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached(typeof(IGameplayClock))]
|
||||
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
|
||||
|
||||
[Cached]
|
||||
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
|
@ -15,6 +15,7 @@ using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.SkinEditor;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
@ -188,6 +189,33 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeToNonSkinnableScreen()
|
||||
{
|
||||
advanceToSongSelect();
|
||||
openSkinEditor();
|
||||
AddAssert("blueprint container present", () => skinEditor.ChildrenOfType<SkinBlueprintContainer>().Count(), () => Is.EqualTo(1));
|
||||
AddAssert("placeholder not present", () => skinEditor.ChildrenOfType<NonSkinnableScreenPlaceholder>().Count(), () => Is.Zero);
|
||||
AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType<EditorSidebar>().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0));
|
||||
|
||||
AddStep("add skinnable component", () =>
|
||||
{
|
||||
skinEditor.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick();
|
||||
});
|
||||
AddUntilStep("newly added component selected", () => skinEditor.SelectedComponents, () => Has.Count.EqualTo(1));
|
||||
|
||||
AddStep("exit to main menu", () => Game.ScreenStack.CurrentScreen.Exit());
|
||||
AddAssert("selection cleared", () => skinEditor.SelectedComponents, () => Has.Count.Zero);
|
||||
AddAssert("blueprint container not present", () => skinEditor.ChildrenOfType<SkinBlueprintContainer>().Count(), () => Is.Zero);
|
||||
AddAssert("placeholder present", () => skinEditor.ChildrenOfType<NonSkinnableScreenPlaceholder>().Count(), () => Is.EqualTo(1));
|
||||
AddAssert("editor sidebars empty", () => skinEditor.ChildrenOfType<EditorSidebar>().SelectMany(sidebar => sidebar.Children).Count(), () => Is.Zero);
|
||||
|
||||
advanceToSongSelect();
|
||||
AddAssert("blueprint container present", () => skinEditor.ChildrenOfType<SkinBlueprintContainer>().Count(), () => Is.EqualTo(1));
|
||||
AddAssert("placeholder not present", () => skinEditor.ChildrenOfType<NonSkinnableScreenPlaceholder>().Count(), () => Is.Zero);
|
||||
AddAssert("editor sidebars not empty", () => skinEditor.ChildrenOfType<EditorSidebar>().SelectMany(sidebar => sidebar.Children).Count(), () => Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
private void advanceToSongSelect()
|
||||
{
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
|
@ -516,15 +516,20 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
sets.Clear();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var set = TestResources.CreateTestBeatmapSetInfo(5);
|
||||
|
||||
if (i >= 2 && i < 10)
|
||||
// A total of 6 sets have date submitted (4 don't)
|
||||
// A total of 5 sets have artist string (3 of which also have date submitted)
|
||||
|
||||
if (i >= 2 && i < 8) // i = 2, 3, 4, 5, 6, 7 have submitted date
|
||||
set.DateSubmitted = DateTimeOffset.Now.AddMinutes(i);
|
||||
if (i < 5)
|
||||
if (i < 5) // i = 0, 1, 2, 3, 4 have matching string
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
|
||||
|
||||
set.Beatmaps.ForEach(b => b.Metadata.Title = $"submitted: {set.DateSubmitted}");
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
});
|
||||
@ -532,15 +537,26 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
loadBeatmaps(sets);
|
||||
|
||||
AddStep("Sort by date submitted", () => carousel.Filter(new FilterCriteria { Sort = SortMode.DateSubmitted }, false));
|
||||
checkVisibleItemCount(diff: false, count: 8);
|
||||
checkVisibleItemCount(diff: false, count: 10);
|
||||
checkVisibleItemCount(diff: true, count: 5);
|
||||
|
||||
AddAssert("missing date are at end",
|
||||
() => carousel.Items.OfType<DrawableCarouselBeatmapSet>().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(4));
|
||||
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
|
||||
() => Is.EqualTo(6));
|
||||
|
||||
AddStep("Sort by date submitted and string", () => carousel.Filter(new FilterCriteria
|
||||
{
|
||||
Sort = SortMode.DateSubmitted,
|
||||
SearchText = zzz_string
|
||||
}, false));
|
||||
checkVisibleItemCount(diff: false, count: 3);
|
||||
checkVisibleItemCount(diff: false, count: 5);
|
||||
checkVisibleItemCount(diff: true, count: 5);
|
||||
|
||||
AddAssert("missing date are at end",
|
||||
() => carousel.Items.OfType<DrawableCarouselBeatmapSet>().Reverse().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted == null).Count(), () => Is.EqualTo(2));
|
||||
AddAssert("rest are at start", () => carousel.Items.OfType<DrawableCarouselBeatmapSet>().TakeWhile(i => i.Item is CarouselBeatmapSet s && s.BeatmapSet.DateSubmitted != null).Count(),
|
||||
() => Is.EqualTo(3));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -1129,7 +1145,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
// until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet.
|
||||
AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () =>
|
||||
carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count);
|
||||
carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible), () => Is.EqualTo(count));
|
||||
}
|
||||
|
||||
private void checkSelectionIsCentered()
|
||||
@ -1190,8 +1206,11 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var item in Scroll.Children)
|
||||
foreach (var item in Scroll.Children.OrderBy(c => c.Y))
|
||||
{
|
||||
if (item.Item?.Visible != true)
|
||||
continue;
|
||||
|
||||
yield return item;
|
||||
|
||||
if (item is DrawableCarouselBeatmapSet set)
|
||||
|
@ -61,6 +61,18 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("scroll to 500", () => scroll.ScrollTo(500));
|
||||
AddUntilStep("scrolled to 500", () => Precision.AlmostEquals(scroll.Current, 500, 0.1f));
|
||||
AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible);
|
||||
|
||||
AddStep("click button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(scroll.Button);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("button is visible", () => scroll.Button.State == Visibility.Visible);
|
||||
|
||||
AddStep("user scroll down by 1", () => InputManager.ScrollVerticalBy(-1));
|
||||
|
||||
AddAssert("button is hidden", () => scroll.Button.State == Visibility.Hidden);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -71,6 +83,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("invoke action", () => scroll.Button.Action.Invoke());
|
||||
|
||||
AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f));
|
||||
|
||||
AddStep("invoke action", () => scroll.Button.Action.Invoke());
|
||||
|
||||
AddAssert("scrolled to end", () => scroll.IsScrolledToEnd());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -85,6 +101,14 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
});
|
||||
|
||||
AddUntilStep("scrolled back to start", () => Precision.AlmostEquals(scroll.Current, 0, 0.1f));
|
||||
|
||||
AddStep("click button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(scroll.Button);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("scrolled to end", () => scroll.IsScrolledToEnd());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -97,12 +121,12 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddStep("hover button", () => InputManager.MoveMouseTo(scroll.Button));
|
||||
AddRepeatStep("click button", () => InputManager.Click(MouseButton.Left), 3);
|
||||
|
||||
AddAssert("invocation count is 1", () => invocationCount == 1);
|
||||
AddAssert("invocation count is 3", () => invocationCount == 3);
|
||||
}
|
||||
|
||||
private partial class TestScrollContainer : OverlayScrollContainer
|
||||
{
|
||||
public new ScrollToTopButton Button => base.Button;
|
||||
public new ScrollBackButton Button => base.Button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
@ -29,10 +29,21 @@ namespace osu.Game.Beatmaps
|
||||
return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim());
|
||||
}
|
||||
|
||||
public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[]
|
||||
public static List<string> GetSearchableTerms(this IBeatmapInfo beatmapInfo)
|
||||
{
|
||||
beatmapInfo.DifficultyName
|
||||
}.Concat(beatmapInfo.Metadata.GetSearchableTerms()).Where(s => !string.IsNullOrEmpty(s)).ToArray();
|
||||
var termsList = new List<string>(BeatmapMetadataInfoExtensions.MAX_SEARCHABLE_TERM_COUNT + 1);
|
||||
|
||||
addIfNotNull(beatmapInfo.DifficultyName);
|
||||
|
||||
BeatmapMetadataInfoExtensions.CollectSearchableTerms(beatmapInfo.Metadata, termsList);
|
||||
return termsList;
|
||||
|
||||
void addIfNotNull(string? s)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(s))
|
||||
termsList.Add(s);
|
||||
}
|
||||
}
|
||||
|
||||
private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]";
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
@ -13,16 +13,31 @@ namespace osu.Game.Beatmaps
|
||||
/// <summary>
|
||||
/// An array of all searchable terms provided in contained metadata.
|
||||
/// </summary>
|
||||
public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) => new[]
|
||||
public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo)
|
||||
{
|
||||
metadataInfo.Author.Username,
|
||||
metadataInfo.Artist,
|
||||
metadataInfo.ArtistUnicode,
|
||||
metadataInfo.Title,
|
||||
metadataInfo.TitleUnicode,
|
||||
metadataInfo.Source,
|
||||
metadataInfo.Tags
|
||||
}.Where(s => !string.IsNullOrEmpty(s)).ToArray();
|
||||
var termsList = new List<string>(MAX_SEARCHABLE_TERM_COUNT);
|
||||
CollectSearchableTerms(metadataInfo, termsList);
|
||||
return termsList.ToArray();
|
||||
}
|
||||
|
||||
internal const int MAX_SEARCHABLE_TERM_COUNT = 7;
|
||||
|
||||
internal static void CollectSearchableTerms(IBeatmapMetadataInfo metadataInfo, IList<string> termsList)
|
||||
{
|
||||
addIfNotNull(metadataInfo.Author.Username);
|
||||
addIfNotNull(metadataInfo.Artist);
|
||||
addIfNotNull(metadataInfo.ArtistUnicode);
|
||||
addIfNotNull(metadataInfo.Title);
|
||||
addIfNotNull(metadataInfo.TitleUnicode);
|
||||
addIfNotNull(metadataInfo.Source);
|
||||
addIfNotNull(metadataInfo.Tags);
|
||||
|
||||
void addIfNotNull(string s)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(s))
|
||||
termsList.Add(s);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A user-presentable display title representing this metadata.
|
||||
|
@ -13,15 +13,9 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
|
||||
{
|
||||
KiaiModeBindable = { Disabled = true },
|
||||
OmitFirstBarLineBindable = { Disabled = true },
|
||||
ScrollSpeedBindable = { Disabled = true }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether the first bar line of this control point is ignored.
|
||||
/// </summary>
|
||||
public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// The relative scroll speed at this control point.
|
||||
/// </summary>
|
||||
@ -43,15 +37,6 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
|
||||
public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the first bar line of this control point is ignored.
|
||||
/// </summary>
|
||||
public bool OmitFirstBarLine
|
||||
{
|
||||
get => OmitFirstBarLineBindable.Value;
|
||||
set => OmitFirstBarLineBindable.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this control point enables Kiai mode.
|
||||
/// </summary>
|
||||
@ -67,16 +52,13 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
}
|
||||
|
||||
public override bool IsRedundant(ControlPoint? existing)
|
||||
=> !OmitFirstBarLine
|
||||
&& existing is EffectControlPoint existingEffect
|
||||
=> existing is EffectControlPoint existingEffect
|
||||
&& KiaiMode == existingEffect.KiaiMode
|
||||
&& OmitFirstBarLine == existingEffect.OmitFirstBarLine
|
||||
&& ScrollSpeed == existingEffect.ScrollSpeed;
|
||||
|
||||
public override void CopyFrom(ControlPoint other)
|
||||
{
|
||||
KiaiMode = ((EffectControlPoint)other).KiaiMode;
|
||||
OmitFirstBarLine = ((EffectControlPoint)other).OmitFirstBarLine;
|
||||
ScrollSpeed = ((EffectControlPoint)other).ScrollSpeed;
|
||||
|
||||
base.CopyFrom(other);
|
||||
@ -88,10 +70,9 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
|
||||
public bool Equals(EffectControlPoint? other)
|
||||
=> base.Equals(other)
|
||||
&& OmitFirstBarLine == other.OmitFirstBarLine
|
||||
&& ScrollSpeed == other.ScrollSpeed
|
||||
&& KiaiMode == other.KiaiMode;
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), OmitFirstBarLine, ScrollSpeed, KiaiMode);
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), ScrollSpeed, KiaiMode);
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,11 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
/// </summary>
|
||||
public readonly Bindable<TimeSignature> TimeSignatureBindable = new Bindable<TimeSignature>(TimeSignature.SimpleQuadruple);
|
||||
|
||||
/// <summary>
|
||||
/// Whether the first bar line of this control point is ignored.
|
||||
/// </summary>
|
||||
public readonly BindableBool OmitFirstBarLineBindable = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
|
||||
/// </summary>
|
||||
@ -30,6 +35,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
Value = default_beat_length,
|
||||
Disabled = true
|
||||
},
|
||||
OmitFirstBarLineBindable = { Disabled = true },
|
||||
TimeSignatureBindable = { Disabled = true }
|
||||
};
|
||||
|
||||
@ -42,6 +48,15 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
set => TimeSignatureBindable.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the first bar line of this control point is ignored.
|
||||
/// </summary>
|
||||
public bool OmitFirstBarLine
|
||||
{
|
||||
get => OmitFirstBarLineBindable.Value;
|
||||
set => OmitFirstBarLineBindable.Value = value;
|
||||
}
|
||||
|
||||
public const double DEFAULT_BEAT_LENGTH = 1000;
|
||||
|
||||
/// <summary>
|
||||
@ -73,6 +88,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
public override void CopyFrom(ControlPoint other)
|
||||
{
|
||||
TimeSignature = ((TimingControlPoint)other).TimeSignature;
|
||||
OmitFirstBarLine = ((TimingControlPoint)other).OmitFirstBarLine;
|
||||
BeatLength = ((TimingControlPoint)other).BeatLength;
|
||||
|
||||
base.CopyFrom(other);
|
||||
@ -85,8 +101,9 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
public bool Equals(TimingControlPoint? other)
|
||||
=> base.Equals(other)
|
||||
&& TimeSignature.Equals(other.TimeSignature)
|
||||
&& OmitFirstBarLine == other.OmitFirstBarLine
|
||||
&& BeatLength.Equals(other.BeatLength);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), TimeSignature, BeatLength);
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), TimeSignature, BeatLength, OmitFirstBarLine);
|
||||
}
|
||||
}
|
||||
|
@ -431,6 +431,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
controlPoint.BeatLength = beatLength;
|
||||
controlPoint.TimeSignature = timeSignature;
|
||||
controlPoint.OmitFirstBarLine = omitFirstBarSignature;
|
||||
|
||||
addControlPoint(time, controlPoint, true);
|
||||
}
|
||||
@ -447,7 +448,6 @@ namespace osu.Game.Beatmaps.Formats
|
||||
var effectPoint = new EffectControlPoint
|
||||
{
|
||||
KiaiMode = kiaiMode,
|
||||
OmitFirstBarLine = omitFirstBarSignature,
|
||||
};
|
||||
|
||||
// osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments.
|
||||
|
@ -222,6 +222,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
{
|
||||
var samplePoint = legacyControlPoints.SamplePointAt(time);
|
||||
var effectPoint = legacyControlPoints.EffectPointAt(time);
|
||||
var timingPoint = legacyControlPoints.TimingPointAt(time);
|
||||
|
||||
// Apply the control point to a hit sample to uncover legacy properties (e.g. suffix)
|
||||
HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty));
|
||||
@ -230,10 +231,10 @@ namespace osu.Game.Beatmaps.Formats
|
||||
LegacyEffectFlags effectFlags = LegacyEffectFlags.None;
|
||||
if (effectPoint.KiaiMode)
|
||||
effectFlags |= LegacyEffectFlags.Kiai;
|
||||
if (effectPoint.OmitFirstBarLine)
|
||||
if (timingPoint.OmitFirstBarLine)
|
||||
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
|
||||
|
||||
writer.Write(FormattableString.Invariant($"{legacyControlPoints.TimingPointAt(time).TimeSignature.Numerator},"));
|
||||
writer.Write(FormattableString.Invariant($"{timingPoint.TimeSignature.Numerator},"));
|
||||
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
|
||||
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
|
||||
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
|
||||
|
@ -8,12 +8,12 @@ using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Collections
|
||||
{
|
||||
public partial class DeleteCollectionDialog : DeleteConfirmationDialog
|
||||
public partial class DeleteCollectionDialog : DangerousActionDialog
|
||||
{
|
||||
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
|
||||
{
|
||||
BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})");
|
||||
DeleteAction = deleteAction;
|
||||
DangerousAction = deleteAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -19,6 +20,19 @@ namespace osu.Game.Database
|
||||
public abstract class LegacyExporter<TModel>
|
||||
where TModel : class, IHasNamedFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// Max length of filename (including extension).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// The filename limit for most OSs is 255. This actual usable length is smaller because <see cref="Storage.CreateFileSafely(string)"/> adds an additional "_<see cref="Guid"/>" to the end of the path.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For more information see <see href="https://www.ibm.com/docs/en/spectrum-protect/8.1.9?topic=parameters-file-specification-syntax">file specification syntax</see>, <seealso href="https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits">file systems limitations</seealso>
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public const int MAX_FILENAME_LENGTH = 255 - (32 + 4 + 2 + 5); //max path - (Guid + Guid "D" format chars + Storage.CreateFileSafely chars + account for ' (99)' suffix)
|
||||
|
||||
/// <summary>
|
||||
/// The file extension for exports (including the leading '.').
|
||||
/// </summary>
|
||||
@ -44,12 +58,16 @@ namespace osu.Game.Database
|
||||
{
|
||||
string itemFilename = GetFilename(item).GetValidFilename();
|
||||
|
||||
if (itemFilename.Length > MAX_FILENAME_LENGTH - FileExtension.Length)
|
||||
itemFilename = itemFilename.Remove(MAX_FILENAME_LENGTH - FileExtension.Length);
|
||||
|
||||
IEnumerable<string> existingExports =
|
||||
exportStorage
|
||||
.GetFiles(string.Empty, $"{itemFilename}*{FileExtension}")
|
||||
.Concat(exportStorage.GetDirectories(string.Empty));
|
||||
|
||||
string filename = NamingUtils.GetNextBestFilename(existingExports, $"{itemFilename}{FileExtension}");
|
||||
|
||||
using (var stream = exportStorage.CreateFileSafely(filename))
|
||||
ExportModelTo(item, stream);
|
||||
|
||||
|
16
osu.Game/Graphics/Backgrounds/TriangleBorderData.cs
Normal file
16
osu.Game/Graphics/Backgrounds/TriangleBorderData.cs
Normal file
@ -0,0 +1,16 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
|
||||
namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public record struct TriangleBorderData
|
||||
{
|
||||
public UniformFloat Thickness;
|
||||
public UniformFloat TexelSize;
|
||||
private readonly UniformPadding8 pad1;
|
||||
}
|
||||
}
|
@ -252,7 +252,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private class TrianglesDrawNode : DrawNode
|
||||
{
|
||||
private float fill = 1f;
|
||||
private const float fill = 1f;
|
||||
|
||||
protected new Triangles Source => (Triangles)base.Source;
|
||||
|
||||
@ -284,6 +284,8 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
parts.AddRange(Source.parts);
|
||||
}
|
||||
|
||||
private IUniformBuffer<TriangleBorderData> borderDataBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
@ -294,14 +296,17 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
|
||||
}
|
||||
|
||||
// Due to triangles having various sizes we would need to set a different "texelSize" value for each of them, which is insanely expensive, thus we should use one single value.
|
||||
// texelSize computed for an average triangle (size 100) will result in big triangles becoming blurry, so we may just use 0 for all of them.
|
||||
// But we still need to specify at least something, because otherwise other shader usages will override this value.
|
||||
float texelSize = 0f;
|
||||
borderDataBuffer ??= renderer.CreateUniformBuffer<TriangleBorderData>();
|
||||
borderDataBuffer.Data = borderDataBuffer.Data with
|
||||
{
|
||||
Thickness = fill,
|
||||
// Due to triangles having various sizes we would need to set a different "TexelSize" value for each of them, which is insanely expensive, thus we should use one single value.
|
||||
// TexelSize computed for an average triangle (size 100) will result in big triangles becoming blurry, so we may just use 0 for all of them.
|
||||
TexelSize = 0
|
||||
};
|
||||
|
||||
shader.Bind();
|
||||
shader.GetUniform<float>("thickness").UpdateValue(ref fill);
|
||||
shader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
|
||||
shader.BindUniformBlock("m_BorderData", borderDataBuffer);
|
||||
|
||||
foreach (TriangleParticle particle in parts)
|
||||
{
|
||||
@ -352,6 +357,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
vertexBatch?.Dispose();
|
||||
borderDataBuffer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,6 @@ using osu.Framework.Allocation;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
@ -227,6 +226,8 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
parts.AddRange(Source.parts);
|
||||
}
|
||||
|
||||
private IUniformBuffer<TriangleBorderData>? borderDataBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
@ -240,9 +241,15 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
|
||||
}
|
||||
|
||||
borderDataBuffer ??= renderer.CreateUniformBuffer<TriangleBorderData>();
|
||||
borderDataBuffer.Data = borderDataBuffer.Data with
|
||||
{
|
||||
Thickness = thickness,
|
||||
TexelSize = texelSize
|
||||
};
|
||||
|
||||
shader.Bind();
|
||||
shader.GetUniform<float>("thickness").UpdateValue(ref thickness);
|
||||
shader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
|
||||
shader.BindUniformBlock("m_BorderData", borderDataBuffer);
|
||||
|
||||
Vector2 relativeSize = Vector2.Divide(triangleSize, size);
|
||||
|
||||
@ -259,8 +266,6 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
Vector2Extensions.Transform(triangleQuad.BottomRight * size, DrawInfo.Matrix)
|
||||
);
|
||||
|
||||
ColourInfo colourInfo = triangleColourInfo(DrawColourInfo.Colour, triangleQuad);
|
||||
|
||||
RectangleF textureCoords = new RectangleF(
|
||||
triangleQuad.TopLeft.X - topLeft.X,
|
||||
triangleQuad.TopLeft.Y - topLeft.Y,
|
||||
@ -268,23 +273,12 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
triangleQuad.Height
|
||||
) / relativeSize;
|
||||
|
||||
renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords);
|
||||
renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords);
|
||||
}
|
||||
|
||||
shader.Unbind();
|
||||
}
|
||||
|
||||
private static ColourInfo triangleColourInfo(ColourInfo source, Quad quad)
|
||||
{
|
||||
return new ColourInfo
|
||||
{
|
||||
TopLeft = source.Interpolate(quad.TopLeft),
|
||||
TopRight = source.Interpolate(quad.TopRight),
|
||||
BottomLeft = source.Interpolate(quad.BottomLeft),
|
||||
BottomRight = source.Interpolate(quad.BottomRight)
|
||||
};
|
||||
}
|
||||
|
||||
private static Quad clampToDrawable(Vector2 topLeft, Vector2 size)
|
||||
{
|
||||
float leftClamped = Math.Clamp(topLeft.X, 0f, 1f);
|
||||
@ -303,6 +297,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
vertexBatch?.Dispose();
|
||||
borderDataBuffer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Graphics.Containers
|
||||
while (beatLength < MinimumBeatLength)
|
||||
beatLength *= 2;
|
||||
|
||||
int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength) - (effectPoint.OmitFirstBarLine ? 1 : 0);
|
||||
int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength) - (timingPoint.OmitFirstBarLine ? 1 : 0);
|
||||
|
||||
// The beats before the start of the first control point are off by 1, this should do the trick
|
||||
if (currentTrackTime < timingPoint.Time)
|
||||
|
@ -3,10 +3,12 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
@ -55,14 +57,32 @@ namespace osu.Game.Graphics.Sprites
|
||||
progress = source.animationProgress;
|
||||
}
|
||||
|
||||
private IUniformBuffer<AnimationData> animationDataBuffer;
|
||||
|
||||
protected override void Blit(IRenderer renderer)
|
||||
{
|
||||
TextureShader.GetUniform<float>("progress").UpdateValue(ref progress);
|
||||
animationDataBuffer ??= renderer.CreateUniformBuffer<AnimationData>();
|
||||
animationDataBuffer.Data = animationDataBuffer.Data with { Progress = progress };
|
||||
|
||||
TextureShader.BindUniformBlock("m_AnimationData", animationDataBuffer);
|
||||
|
||||
base.Blit(renderer);
|
||||
}
|
||||
|
||||
protected override bool CanDrawOpaqueInterior => false;
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
animationDataBuffer?.Dispose();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private record struct AnimationData
|
||||
{
|
||||
public UniformFloat Progress;
|
||||
private readonly UniformPadding12 pad1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -249,13 +249,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private ColourInfo getSegmentColour(SegmentInfo segment)
|
||||
{
|
||||
var segmentColour = new ColourInfo
|
||||
{
|
||||
TopLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 0f)),
|
||||
TopRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 0f)),
|
||||
BottomLeft = DrawColourInfo.Colour.Interpolate(new Vector2(segment.Start, 1f)),
|
||||
BottomRight = DrawColourInfo.Colour.Interpolate(new Vector2(segment.End, 1f))
|
||||
};
|
||||
var segmentColour = DrawColourInfo.Colour.Interpolate(new Quad(segment.Start, 0f, segment.End - segment.Start, 1f));
|
||||
|
||||
var tierColour = segment.Tier >= 0 ? tierColours[segment.Tier] : new Colour4(0, 0, 0, 0);
|
||||
segmentColour.ApplyChild(tierColour);
|
||||
|
@ -39,6 +39,16 @@ namespace osu.Game.Localisation.SkinComponents
|
||||
/// </summary>
|
||||
public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), "The text to be displayed.");
|
||||
|
||||
/// <summary>
|
||||
/// "Corner radius"
|
||||
/// </summary>
|
||||
public static LocalisableString CornerRadius => new TranslatableString(getKey(@"corner_radius"), "Corner radius");
|
||||
|
||||
/// <summary>
|
||||
/// "How rounded the corners should be."
|
||||
/// </summary>
|
||||
public static LocalisableString CornerRadiusDescription => new TranslatableString(getKey(@"corner_radius_description"), "How rounded the corners should be.");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,12 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "Currently editing"
|
||||
/// </summary>
|
||||
public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), "Currently editing");
|
||||
public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), @"Currently editing");
|
||||
|
||||
/// <summary>
|
||||
/// "All layout elements for layers in the current screen will be reset to defaults."
|
||||
/// </summary>
|
||||
public static LocalisableString RevertToDefaultDescription => new TranslatableString(getKey(@"revert_to_default_description"), @"All layout elements for layers in the current screen will be reset to defaults.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
|
@ -39,6 +39,7 @@ namespace osu.Game.Overlays.Chat
|
||||
|
||||
private const float chatting_text_width = 220;
|
||||
private const float search_icon_width = 40;
|
||||
private const float padding = 5;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
@ -71,9 +72,10 @@ namespace osu.Game.Overlays.Chat
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = chatting_text_width,
|
||||
Masking = true,
|
||||
Padding = new MarginPadding { Right = 5 },
|
||||
Padding = new MarginPadding { Horizontal = padding },
|
||||
Child = chattingText = new OsuSpriteText
|
||||
{
|
||||
MaxWidth = chatting_text_width - padding * 2,
|
||||
Font = OsuFont.Torus.With(size: 20),
|
||||
Colour = colourProvider.Background1,
|
||||
Anchor = Anchor.CentreRight,
|
||||
@ -97,7 +99,7 @@ namespace osu.Game.Overlays.Chat
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Right = 5 },
|
||||
Padding = new MarginPadding { Right = padding },
|
||||
Child = chatTextBox = new ChatTextBox
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
|
@ -8,18 +8,22 @@ using osu.Game.Localisation;
|
||||
namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for various confirmation dialogs that concern deletion actions.
|
||||
/// A dialog which provides confirmation for actions which result in permanent consequences.
|
||||
/// Differs from <see cref="ConfirmDialog"/> in that the confirmation button is a "dangerous" one
|
||||
/// (requires the confirm button to be held).
|
||||
/// </summary>
|
||||
public abstract partial class DeleteConfirmationDialog : PopupDialog
|
||||
/// <remarks>
|
||||
/// The default implementation comes with text for a generic deletion operation.
|
||||
/// This can be further customised by specifying custom <see cref="PopupDialog.HeaderText"/>.
|
||||
/// </remarks>
|
||||
public abstract partial class DangerousActionDialog : PopupDialog
|
||||
{
|
||||
/// <summary>
|
||||
/// The action which performs the deletion.
|
||||
/// </summary>
|
||||
protected Action? DeleteAction { get; set; }
|
||||
protected Action? DangerousAction { get; set; }
|
||||
|
||||
protected DeleteConfirmationDialog()
|
||||
protected DangerousActionDialog()
|
||||
{
|
||||
HeaderText = DeleteConfirmationDialogStrings.HeaderText;
|
||||
|
||||
@ -30,7 +34,7 @@ namespace osu.Game.Overlays.Dialog
|
||||
new PopupDialogDangerousButton
|
||||
{
|
||||
Text = DeleteConfirmationDialogStrings.Confirm,
|
||||
Action = () => DeleteAction?.Invoke()
|
||||
Action = () => DangerousAction?.Invoke()
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
@ -7,12 +7,12 @@ using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public partial class DeleteModPresetDialog : DeleteConfirmationDialog
|
||||
public partial class DeleteModPresetDialog : DangerousActionDialog
|
||||
{
|
||||
public DeleteModPresetDialog(Live<ModPreset> modPreset)
|
||||
{
|
||||
BodyText = modPreset.PerformRead(preset => preset.Name);
|
||||
DeleteAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true);
|
||||
DangerousAction = () => modPreset.PerformWrite(preset => preset.DeletePending = true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -27,7 +28,14 @@ namespace osu.Game.Overlays.Mods
|
||||
public Color4 AccentColour
|
||||
{
|
||||
get => headerBackground.Colour;
|
||||
set => headerBackground.Colour = value;
|
||||
set
|
||||
{
|
||||
headerBackground.Colour = value;
|
||||
|
||||
var hsv = new Colour4(value.R, value.G, value.B, 1f).ToHSV();
|
||||
var trianglesColour = Colour4.FromHSV(hsv.X, hsv.Y + 0.2f, hsv.Z - 0.1f);
|
||||
triangles.Colour = ColourInfo.GradientVertical(trianglesColour, trianglesColour.MultiplyAlpha(0f));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -44,6 +52,7 @@ namespace osu.Game.Overlays.Mods
|
||||
private readonly Box headerBackground;
|
||||
private readonly Container contentContainer;
|
||||
private readonly Box contentBackground;
|
||||
private readonly TrianglesV2 triangles;
|
||||
|
||||
private const float header_height = 42;
|
||||
|
||||
@ -73,6 +82,13 @@ namespace osu.Game.Overlays.Mods
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = header_height + ModSelectPanel.CORNER_RADIUS
|
||||
},
|
||||
triangles = new TrianglesV2
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = header_height,
|
||||
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
|
||||
Velocity = 0.7f,
|
||||
},
|
||||
headerText = new OsuTextFlowContainer(t =>
|
||||
{
|
||||
t.Font = OsuFont.TorusAlternate.With(size: 17);
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -21,25 +22,29 @@ using osuTK.Graphics;
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="UserTrackingScrollContainer"/> which provides <see cref="ScrollToTopButton"/>. Mostly used in <see cref="FullscreenOverlay{T}"/>.
|
||||
/// <see cref="UserTrackingScrollContainer"/> which provides <see cref="ScrollBackButton"/>. Mostly used in <see cref="FullscreenOverlay{T}"/>.
|
||||
/// </summary>
|
||||
public partial class OverlayScrollContainer : UserTrackingScrollContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Scroll position at which the <see cref="ScrollToTopButton"/> will be shown.
|
||||
/// Scroll position at which the <see cref="ScrollBackButton"/> will be shown.
|
||||
/// </summary>
|
||||
private const int button_scroll_position = 200;
|
||||
|
||||
protected readonly ScrollToTopButton Button;
|
||||
protected ScrollBackButton Button;
|
||||
|
||||
public OverlayScrollContainer()
|
||||
private readonly Bindable<float?> lastScrollTarget = new Bindable<float?>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(Button = new ScrollToTopButton
|
||||
AddInternal(Button = new ScrollBackButton
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding(20),
|
||||
Action = scrollToTop
|
||||
Action = scrollBack,
|
||||
LastScrollTarget = { BindTarget = lastScrollTarget }
|
||||
});
|
||||
}
|
||||
|
||||
@ -53,16 +58,31 @@ namespace osu.Game.Overlays
|
||||
return;
|
||||
}
|
||||
|
||||
Button.State = Target > button_scroll_position ? Visibility.Visible : Visibility.Hidden;
|
||||
Button.State = Target > button_scroll_position || lastScrollTarget.Value != null ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
|
||||
private void scrollToTop()
|
||||
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
|
||||
{
|
||||
ScrollToStart();
|
||||
Button.State = Visibility.Hidden;
|
||||
base.OnUserScroll(value, animated, distanceDecay);
|
||||
|
||||
lastScrollTarget.Value = null;
|
||||
}
|
||||
|
||||
public partial class ScrollToTopButton : OsuHoverContainer
|
||||
private void scrollBack()
|
||||
{
|
||||
if (lastScrollTarget.Value == null)
|
||||
{
|
||||
lastScrollTarget.Value = Target;
|
||||
ScrollToStart();
|
||||
}
|
||||
else
|
||||
{
|
||||
ScrollTo(lastScrollTarget.Value.Value);
|
||||
lastScrollTarget.Value = null;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ScrollBackButton : OsuHoverContainer
|
||||
{
|
||||
private const int fade_duration = 500;
|
||||
|
||||
@ -88,8 +108,11 @@ namespace osu.Game.Overlays
|
||||
|
||||
private readonly Container content;
|
||||
private readonly Box background;
|
||||
private readonly SpriteIcon spriteIcon;
|
||||
|
||||
public ScrollToTopButton()
|
||||
public Bindable<float?> LastScrollTarget = new Bindable<float?>();
|
||||
|
||||
public ScrollBackButton()
|
||||
: base(HoverSampleSet.ScrollToTop)
|
||||
{
|
||||
Size = new Vector2(50);
|
||||
@ -113,7 +136,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new SpriteIcon
|
||||
spriteIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -134,6 +157,17 @@ namespace osu.Game.Overlays
|
||||
flashColour = colourProvider.Light1;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
LastScrollTarget.BindValueChanged(target =>
|
||||
{
|
||||
spriteIcon.RotateTo(target.NewValue != null ? 180 : 0, fade_duration, Easing.OutQuint);
|
||||
TooltipText = target.NewValue != null ? CommonStrings.ButtonsBackToPrevious : CommonStrings.ButtonsBackToTop;
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
background.FlashColour(flashColour, 800, Easing.OutQuint);
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
@ -39,7 +40,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Right = -3 }, // Compensate for scrollbar margin
|
||||
Child = new OsuScrollContainer
|
||||
Child = new SidebarScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new Container
|
||||
@ -74,5 +75,30 @@ namespace osu.Game.Overlays
|
||||
|
||||
[NotNull]
|
||||
protected virtual Drawable CreateContent() => Empty();
|
||||
|
||||
private partial class SidebarScrollContainer : OsuScrollContainer
|
||||
{
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
{
|
||||
if (e.ScrollDelta.Y > 0 && IsScrolledToStart())
|
||||
return false;
|
||||
|
||||
if (e.ScrollDelta.Y < 0 && IsScrolledToEnd())
|
||||
return false;
|
||||
|
||||
return base.OnScroll(e);
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
if (e.Delta.Y > 0 && IsScrolledToStart())
|
||||
return false;
|
||||
|
||||
if (e.Delta.Y < 0 && IsScrolledToEnd())
|
||||
return false;
|
||||
|
||||
return base.OnDragStart(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.Handlers.Tablet;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
@ -66,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colour.Gray1,
|
||||
},
|
||||
usableAreaContainer = new Container
|
||||
usableAreaContainer = new UsableAreaContainer(handler)
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
@ -225,4 +226,28 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
tabletContainer.Scale = new Vector2(1 / adjust);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class UsableAreaContainer : Container
|
||||
{
|
||||
private readonly Bindable<Vector2> areaOffset;
|
||||
|
||||
public UsableAreaContainer(ITabletHandler tabletHandler)
|
||||
{
|
||||
areaOffset = tabletHandler.AreaOffset.GetBoundCopy();
|
||||
}
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e) => true;
|
||||
|
||||
protected override void OnDrag(DragEvent e)
|
||||
{
|
||||
var newPos = Position + e.Delta;
|
||||
this.MoveTo(Vector2.Clamp(newPos, Vector2.Zero, Parent.Size));
|
||||
}
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e)
|
||||
{
|
||||
areaOffset.Value = Position;
|
||||
base.OnDragEnd(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,12 +6,12 @@ using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public partial class MassDeleteConfirmationDialog : DeleteConfirmationDialog
|
||||
public partial class MassDeleteConfirmationDialog : DangerousActionDialog
|
||||
{
|
||||
public MassDeleteConfirmationDialog(Action deleteAction)
|
||||
{
|
||||
BodyText = "Everything?";
|
||||
DeleteAction = deleteAction;
|
||||
DangerousAction = deleteAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
osu.Game/Overlays/Settings/SettingsPercentageSlider.cs
Normal file
20
osu.Game/Overlays/Settings/SettingsPercentageSlider.cs
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="SettingsSlider{TValue,TSlider}"/> that displays its value as a percentage by default.
|
||||
/// Mostly provided for convenience of use with <see cref="SettingSourceAttribute"/>.
|
||||
/// </summary>
|
||||
public partial class SettingsPercentageSlider<TValue> : SettingsSlider<TValue>
|
||||
where TValue : struct, IEquatable<TValue>, IComparable<TValue>, IConvertible
|
||||
{
|
||||
protected override Drawable CreateControl() => ((RoundedSliderBar<TValue>)base.CreateControl()).With(sliderBar => sliderBar.DisplayAsPercentage = true);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class NonSkinnableScreenPlaceholder : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private SkinEditorOverlay? skinEditorOverlay { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Dark6,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.95f,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0, 5),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Icon = FontAwesome.Solid.ExclamationCircle,
|
||||
Size = new Vector2(24),
|
||||
Y = -5,
|
||||
},
|
||||
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 18))
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
TextAnchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = "Please navigate to a skinnable screen using the scene library",
|
||||
},
|
||||
new RoundedButton
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 200,
|
||||
Margin = new MarginPadding { Top = 20 },
|
||||
Action = () => skinEditorOverlay?.Hide(),
|
||||
Text = "Return to game"
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -82,6 +82,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
Text = Item.GetType().Name,
|
||||
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.TopRight,
|
||||
},
|
||||
@ -99,7 +100,6 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
base.LoadComplete();
|
||||
|
||||
updateSelectedState();
|
||||
this.FadeInFromZero(200, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
|
@ -7,15 +7,7 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
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.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Skinning;
|
||||
@ -26,16 +18,16 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public partial class SkinBlueprintContainer : BlueprintContainer<ISerialisableDrawable>
|
||||
{
|
||||
private readonly Drawable target;
|
||||
private readonly ISerialisableDrawableContainer targetContainer;
|
||||
|
||||
private readonly List<BindableList<ISerialisableDrawable>> targetComponents = new List<BindableList<ISerialisableDrawable>>();
|
||||
|
||||
[Resolved]
|
||||
private SkinEditor editor { get; set; } = null!;
|
||||
|
||||
public SkinBlueprintContainer(Drawable target)
|
||||
public SkinBlueprintContainer(ISerialisableDrawableContainer targetContainer)
|
||||
{
|
||||
this.target = target;
|
||||
this.targetContainer = targetContainer;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -44,23 +36,11 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
SelectedItems.BindTo(editor.SelectedComponents);
|
||||
|
||||
// track each target container on the current screen.
|
||||
var targetContainers = target.ChildrenOfType<ISerialisableDrawableContainer>().ToArray();
|
||||
|
||||
if (targetContainers.Length == 0)
|
||||
{
|
||||
AddInternal(new NonSkinnableScreenPlaceholder());
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var targetContainer in targetContainers)
|
||||
{
|
||||
var bindableList = new BindableList<ISerialisableDrawable> { BindTarget = targetContainer.Components };
|
||||
bindableList.BindCollectionChanged(componentsChanged, true);
|
||||
|
||||
targetComponents.Add(bindableList);
|
||||
}
|
||||
}
|
||||
|
||||
private void componentsChanged(object? sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
|
||||
{
|
||||
@ -160,65 +140,5 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
foreach (var list in targetComponents)
|
||||
list.UnbindAll();
|
||||
}
|
||||
|
||||
public partial class NonSkinnableScreenPlaceholder : CompositeDrawable
|
||||
{
|
||||
[Resolved]
|
||||
private SkinEditorOverlay? skinEditorOverlay { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Dark6,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0.95f,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0, 5),
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Icon = FontAwesome.Solid.ExclamationCircle,
|
||||
Size = new Vector2(24),
|
||||
Y = -5,
|
||||
},
|
||||
new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 18))
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
TextAnchor = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = "Please navigate to a skinnable screen using the scene library",
|
||||
},
|
||||
new RoundedButton
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 200,
|
||||
Margin = new MarginPadding { Top = 20 },
|
||||
Action = () => skinEditorOverlay?.Hide(),
|
||||
Text = "Return to game"
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -22,12 +23,12 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
public Action<Type>? RequestPlacement;
|
||||
|
||||
private readonly CompositeDrawable? target;
|
||||
private readonly SkinComponentsContainer? target;
|
||||
|
||||
private FillFlowContainer fill = null!;
|
||||
|
||||
public SkinComponentToolbox(CompositeDrawable? target = null)
|
||||
: base(SkinEditorStrings.Components)
|
||||
public SkinComponentToolbox(SkinComponentsContainer? target = null)
|
||||
: base(target?.Lookup.Ruleset == null ? SkinEditorStrings.Components : LocalisableString.Interpolate($"{SkinEditorStrings.Components} ({target.Lookup.Ruleset.Name})"))
|
||||
{
|
||||
this.target = target;
|
||||
}
|
||||
@ -50,7 +51,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
fill.Clear();
|
||||
|
||||
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables();
|
||||
var skinnableTypes = SerialisedDrawableInfo.GetAllAvailableDrawables(target?.Lookup.Ruleset);
|
||||
foreach (var type in skinnableTypes)
|
||||
attemptAddComponent(type);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -16,6 +17,7 @@ using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using Web = osu.Game.Resources.Localisation.Web;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
@ -23,7 +25,9 @@ using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Overlays.OSD;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
@ -60,12 +64,17 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorClipboard clipboard { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private SkinEditorOverlay? skinEditorOverlay { get; set; }
|
||||
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||
|
||||
private readonly Bindable<SkinComponentsContainerLookup?> selectedTarget = new Bindable<SkinComponentsContainerLookup?>();
|
||||
|
||||
private bool hasBegunMutating;
|
||||
|
||||
private Container? content;
|
||||
@ -78,9 +87,21 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
private EditorMenuItem undoMenuItem = null!;
|
||||
private EditorMenuItem redoMenuItem = null!;
|
||||
|
||||
private EditorMenuItem cutMenuItem = null!;
|
||||
private EditorMenuItem copyMenuItem = null!;
|
||||
private EditorMenuItem cloneMenuItem = null!;
|
||||
private EditorMenuItem pasteMenuItem = null!;
|
||||
|
||||
private readonly BindableWithCurrent<bool> canCut = new BindableWithCurrent<bool>();
|
||||
private readonly BindableWithCurrent<bool> canCopy = new BindableWithCurrent<bool>();
|
||||
private readonly BindableWithCurrent<bool> canPaste = new BindableWithCurrent<bool>();
|
||||
|
||||
[Resolved]
|
||||
private OnScreenDisplay? onScreenDisplay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
public SkinEditor()
|
||||
{
|
||||
}
|
||||
@ -131,8 +152,8 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
Items = new[]
|
||||
{
|
||||
new EditorMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
|
||||
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, revert),
|
||||
new EditorMenuItem(Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
|
||||
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, () => dialogOverlay?.Push(new RevertConfirmDialog(revert))),
|
||||
new EditorMenuItemSpacer(),
|
||||
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
|
||||
},
|
||||
@ -143,6 +164,11 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
|
||||
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
|
||||
new EditorMenuItemSpacer(),
|
||||
cutMenuItem = new EditorMenuItem(CommonStrings.Cut, MenuItemType.Standard, Cut),
|
||||
copyMenuItem = new EditorMenuItem(CommonStrings.Copy, MenuItemType.Standard, Copy),
|
||||
pasteMenuItem = new EditorMenuItem(CommonStrings.Paste, MenuItemType.Standard, Paste),
|
||||
cloneMenuItem = new EditorMenuItem(CommonStrings.Clone, MenuItemType.Standard, Clone),
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -201,6 +227,21 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
|
||||
canCopy.Current.BindValueChanged(copy =>
|
||||
{
|
||||
copyMenuItem.Action.Disabled = !copy.NewValue;
|
||||
cloneMenuItem.Action.Disabled = !copy.NewValue;
|
||||
}, true);
|
||||
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
|
||||
|
||||
SelectedComponents.BindCollectionChanged((_, _) =>
|
||||
{
|
||||
canCopy.Value = canCut.Value = SelectedComponents.Any();
|
||||
}, true);
|
||||
|
||||
clipboard.Content.BindValueChanged(content => canPaste.Value = !string.IsNullOrEmpty(content.NewValue), true);
|
||||
|
||||
Show();
|
||||
|
||||
game?.RegisterImportHandler(this);
|
||||
@ -218,12 +259,26 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
}, true);
|
||||
|
||||
SelectedComponents.BindCollectionChanged((_, _) => Scheduler.AddOnce(populateSettings), true);
|
||||
|
||||
selectedTarget.BindValueChanged(targetChanged, true);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case PlatformAction.Cut:
|
||||
Cut();
|
||||
return true;
|
||||
|
||||
case PlatformAction.Copy:
|
||||
Copy();
|
||||
return true;
|
||||
|
||||
case PlatformAction.Paste:
|
||||
Paste();
|
||||
return true;
|
||||
|
||||
case PlatformAction.Undo:
|
||||
Undo();
|
||||
return true;
|
||||
@ -253,28 +308,83 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
changeHandler?.Dispose();
|
||||
|
||||
SelectedComponents.Clear();
|
||||
|
||||
// Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target.
|
||||
content?.Clear();
|
||||
if (content?.Child is SkinBlueprintContainer)
|
||||
content.Clear();
|
||||
|
||||
Scheduler.AddOnce(loadBlueprintContainer);
|
||||
Scheduler.AddOnce(populateSettings);
|
||||
|
||||
void loadBlueprintContainer()
|
||||
{
|
||||
selectedTarget.Default = getFirstTarget()?.Lookup;
|
||||
|
||||
if (!availableTargets.Any(t => t.Lookup.Equals(selectedTarget.Value)))
|
||||
selectedTarget.SetDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private void targetChanged(ValueChangedEvent<SkinComponentsContainerLookup?> target)
|
||||
{
|
||||
foreach (var toolbox in componentsSidebar.OfType<SkinComponentToolbox>())
|
||||
toolbox.Expire();
|
||||
|
||||
componentsSidebar.Clear();
|
||||
SelectedComponents.Clear();
|
||||
|
||||
Debug.Assert(content != null);
|
||||
|
||||
changeHandler = new SkinEditorChangeHandler(targetScreen);
|
||||
var skinComponentsContainer = getTarget(target.NewValue);
|
||||
|
||||
if (target.NewValue == null || skinComponentsContainer == null)
|
||||
{
|
||||
content.Child = new NonSkinnableScreenPlaceholder();
|
||||
return;
|
||||
}
|
||||
|
||||
changeHandler = new SkinEditorChangeHandler(skinComponentsContainer);
|
||||
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||
|
||||
content.Child = new SkinBlueprintContainer(targetScreen);
|
||||
content.Child = new SkinBlueprintContainer(skinComponentsContainer);
|
||||
|
||||
componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable)
|
||||
componentsSidebar.Children = new[]
|
||||
{
|
||||
RequestPlacement = placeComponent
|
||||
new EditorSidebarSection("Current working layer")
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SettingsDropdown<SkinComponentsContainerLookup?>
|
||||
{
|
||||
Items = availableTargets.Select(t => t.Lookup),
|
||||
Current = selectedTarget,
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// If the new target has a ruleset, let's show ruleset-specific items at the top, and the rest below.
|
||||
if (target.NewValue.Ruleset != null)
|
||||
{
|
||||
componentsSidebar.Add(new SkinComponentToolbox(skinComponentsContainer)
|
||||
{
|
||||
RequestPlacement = requestPlacement
|
||||
});
|
||||
}
|
||||
|
||||
// Remove the ruleset from the lookup to get base components.
|
||||
componentsSidebar.Add(new SkinComponentToolbox(getTarget(new SkinComponentsContainerLookup(target.NewValue.Target)))
|
||||
{
|
||||
RequestPlacement = requestPlacement
|
||||
});
|
||||
|
||||
void requestPlacement(Type type)
|
||||
{
|
||||
if (!(Activator.CreateInstance(type) is ISerialisableDrawable component))
|
||||
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}.");
|
||||
|
||||
SelectedComponents.Clear();
|
||||
placeComponent(component);
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,20 +410,18 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
hasBegunMutating = true;
|
||||
}
|
||||
|
||||
private void placeComponent(Type type)
|
||||
/// <summary>
|
||||
/// Attempt to place a given component in the current target. If successful, the new component will be added to <see cref="SelectedComponents"/>.
|
||||
/// </summary>
|
||||
/// <param name="component">The component to be placed.</param>
|
||||
/// <param name="applyDefaults">Whether to apply default anchor / origin / position values.</param>
|
||||
/// <returns>Whether placement succeeded. Could fail if no target is available, or if the current target has missing dependency requirements for the component.</returns>
|
||||
private bool placeComponent(ISerialisableDrawable component, bool applyDefaults = true)
|
||||
{
|
||||
if (!(Activator.CreateInstance(type) is ISerialisableDrawable component))
|
||||
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISerialisableDrawable)}.");
|
||||
|
||||
placeComponent(component);
|
||||
}
|
||||
|
||||
private void placeComponent(ISerialisableDrawable component, bool applyDefaults = true)
|
||||
{
|
||||
var targetContainer = getFirstTarget();
|
||||
var targetContainer = getTarget(selectedTarget.Value);
|
||||
|
||||
if (targetContainer == null)
|
||||
return;
|
||||
return false;
|
||||
|
||||
var drawableComponent = (Drawable)component;
|
||||
|
||||
@ -325,10 +433,18 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
drawableComponent.Y = targetContainer.DrawSize.Y / 2;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
targetContainer.Add(component);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// May fail if dependencies are not available, for instance.
|
||||
return false;
|
||||
}
|
||||
|
||||
SelectedComponents.Clear();
|
||||
SelectedComponents.Add(component);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void populateSettings()
|
||||
@ -341,11 +457,11 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
private IEnumerable<SkinComponentsContainer> availableTargets => targetScreen.ChildrenOfType<SkinComponentsContainer>();
|
||||
|
||||
private ISerialisableDrawableContainer? getFirstTarget() => availableTargets.FirstOrDefault();
|
||||
private SkinComponentsContainer? getFirstTarget() => availableTargets.FirstOrDefault();
|
||||
|
||||
private ISerialisableDrawableContainer? getTarget(SkinComponentsContainerLookup.TargetArea target)
|
||||
private SkinComponentsContainer? getTarget(SkinComponentsContainerLookup? target)
|
||||
{
|
||||
return availableTargets.FirstOrDefault(c => c.Lookup.Target == target);
|
||||
return availableTargets.FirstOrDefault(c => c.Lookup.Equals(target));
|
||||
}
|
||||
|
||||
private void revert()
|
||||
@ -357,10 +473,52 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
currentSkin.Value.ResetDrawableTarget(t);
|
||||
|
||||
// add back default components
|
||||
getTarget(t.Lookup.Target)?.Reload();
|
||||
getTarget(t.Lookup)?.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
protected void Cut()
|
||||
{
|
||||
Copy();
|
||||
DeleteItems(SelectedComponents.ToArray());
|
||||
}
|
||||
|
||||
protected void Copy()
|
||||
{
|
||||
clipboard.Content.Value = JsonConvert.SerializeObject(SelectedComponents.Cast<Drawable>().Select(s => s.CreateSerialisedInfo()).ToArray());
|
||||
}
|
||||
|
||||
protected void Clone()
|
||||
{
|
||||
// Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
|
||||
if (!canCopy.Value)
|
||||
return;
|
||||
|
||||
Copy();
|
||||
Paste();
|
||||
}
|
||||
|
||||
protected void Paste()
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
var drawableInfo = JsonConvert.DeserializeObject<SerialisedDrawableInfo[]>(clipboard.Content.Value);
|
||||
|
||||
if (drawableInfo == null)
|
||||
return;
|
||||
|
||||
var instances = drawableInfo.Select(d => d.CreateInstance())
|
||||
.OfType<ISerialisableDrawable>()
|
||||
.ToArray();
|
||||
|
||||
SelectedComponents.Clear();
|
||||
|
||||
foreach (var i in instances)
|
||||
placeComponent(i, false);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
protected void Undo() => changeHandler?.RestoreState(-1);
|
||||
|
||||
protected void Redo() => changeHandler?.RestoreState(1);
|
||||
@ -402,8 +560,55 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
public void DeleteItems(ISerialisableDrawable[] items)
|
||||
{
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
foreach (var item in items)
|
||||
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
|
||||
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item, true);
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
public void BringSelectionToFront()
|
||||
{
|
||||
if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target)
|
||||
return;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
// Iterating by target components order ensures we maintain the same order across selected components, regardless
|
||||
// of the order they were selected in.
|
||||
foreach (var d in target.Components.ToArray())
|
||||
{
|
||||
if (!SelectedComponents.Contains(d))
|
||||
continue;
|
||||
|
||||
target.Remove(d, false);
|
||||
|
||||
// Selection would be reset by the remove.
|
||||
SelectedComponents.Add(d);
|
||||
target.Add(d);
|
||||
}
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
public void SendSelectionToBack()
|
||||
{
|
||||
if (getTarget(selectedTarget.Value) is not SkinComponentsContainer target)
|
||||
return;
|
||||
|
||||
changeHandler?.BeginChange();
|
||||
|
||||
foreach (var d in target.Components.ToArray())
|
||||
{
|
||||
if (SelectedComponents.Contains(d))
|
||||
continue;
|
||||
|
||||
target.Remove(d, false);
|
||||
target.Add(d);
|
||||
}
|
||||
|
||||
changeHandler?.EndChange();
|
||||
}
|
||||
|
||||
#region Drag & drop import handling
|
||||
@ -440,6 +645,7 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
Position = skinnableTarget.ToLocalSpace(GetContainingInputManager().CurrentState.Mouse.Position),
|
||||
};
|
||||
|
||||
SelectedComponents.Clear();
|
||||
placeComponent(sprite, false);
|
||||
|
||||
SkinSelectionHandler.ApplyClosestAnchor(sprite);
|
||||
@ -469,6 +675,16 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
}
|
||||
}
|
||||
|
||||
public partial class RevertConfirmDialog : DangerousActionDialog
|
||||
{
|
||||
public RevertConfirmDialog(Action revert)
|
||||
{
|
||||
HeaderText = CommonStrings.RevertToDefault;
|
||||
BodyText = SkinEditorStrings.RevertToDefaultDescription;
|
||||
DangerousAction = revert;
|
||||
}
|
||||
}
|
||||
|
||||
#region Delegation of IEditorChangeHandler
|
||||
|
||||
public event Action? OnStateChange
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components;
|
||||
using osuTK;
|
||||
|
||||
@ -28,6 +29,9 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
|
||||
private SkinEditor? skinEditor;
|
||||
|
||||
[Cached]
|
||||
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
||||
|
||||
[Resolved]
|
||||
private OsuGame game { get; set; } = null!;
|
||||
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
@ -206,6 +207,14 @@ namespace osu.Game.Overlays.SkinEditor
|
||||
((Drawable)blueprint.Item).Position = Vector2.Zero;
|
||||
});
|
||||
|
||||
yield return new EditorMenuItemSpacer();
|
||||
|
||||
yield return new OsuMenuItem("Bring to front", MenuItemType.Standard, () => skinEditor.BringSelectionToFront());
|
||||
|
||||
yield return new OsuMenuItem("Send to back", MenuItemType.Standard, () => skinEditor.SendSelectionToBack());
|
||||
|
||||
yield return new EditorMenuItemSpacer();
|
||||
|
||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||
yield return item;
|
||||
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Wiki.Markdown
|
||||
{
|
||||
@ -19,24 +20,30 @@ namespace osu.Game.Overlays.Wiki.Markdown
|
||||
{
|
||||
private readonly bool isOutdated;
|
||||
private readonly bool needsCleanup;
|
||||
private readonly bool isStub;
|
||||
|
||||
public WikiNoticeContainer(YamlFrontMatterBlock yamlFrontMatterBlock)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Direction = FillDirection.Vertical;
|
||||
Spacing = new Vector2(10);
|
||||
|
||||
foreach (object line in yamlFrontMatterBlock.Lines)
|
||||
{
|
||||
switch (line.ToString())
|
||||
{
|
||||
case "outdated: true":
|
||||
case @"outdated: true":
|
||||
isOutdated = true;
|
||||
break;
|
||||
|
||||
case "needs_cleanup: true":
|
||||
case @"needs_cleanup: true":
|
||||
needsCleanup = true;
|
||||
break;
|
||||
|
||||
case @"stub: true":
|
||||
isStub = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -60,6 +67,14 @@ namespace osu.Game.Overlays.Wiki.Markdown
|
||||
Text = WikiStrings.ShowNeedsCleanupOrRewrite,
|
||||
});
|
||||
}
|
||||
|
||||
if (isStub)
|
||||
{
|
||||
Add(new NoticeBox
|
||||
{
|
||||
Text = WikiStrings.ShowStub,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private partial class NoticeBox : Container
|
||||
|
@ -7,7 +7,6 @@ using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo));
|
||||
|
||||
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsSlider<double, PercentSlider>))]
|
||||
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider<double>))]
|
||||
public BindableNumber<double> MinimumAccuracy { get; } = new BindableDouble
|
||||
{
|
||||
MinValue = 0.60,
|
||||
@ -69,12 +68,4 @@ namespace osu.Game.Rulesets.Mods
|
||||
return scoreProcessor.ComputeAccuracy(score);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class PercentSlider : RoundedSliderBar<double>
|
||||
{
|
||||
public PercentSlider()
|
||||
{
|
||||
DisplayAsPercentage = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -9,6 +10,7 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Shaders.Types;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
@ -82,10 +84,10 @@ namespace osu.Game.Rulesets.Mods
|
||||
flashlight.Colour = Color4.Black;
|
||||
|
||||
flashlight.Combo.BindTo(Combo);
|
||||
drawableRuleset.KeyBindingInputManager.Add(flashlight);
|
||||
|
||||
drawableRuleset.Overlays.Add(flashlight);
|
||||
// Stop flashlight from being drawn underneath other mods that generate HitObjects.
|
||||
drawableRuleset.KeyBindingInputManager.ChangeChildDepth(flashlight, -1);
|
||||
drawableRuleset.Overlays.ChangeChildDepth(flashlight, float.MinValue);
|
||||
}
|
||||
|
||||
protected abstract Flashlight CreateFlashlight();
|
||||
@ -248,6 +250,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
flashlightSmoothness = Source.flashlightSmoothness;
|
||||
}
|
||||
|
||||
private IUniformBuffer<FlashlightParameters>? flashlightParametersBuffer;
|
||||
|
||||
public override void Draw(IRenderer renderer)
|
||||
{
|
||||
base.Draw(renderer);
|
||||
@ -262,12 +266,17 @@ namespace osu.Game.Rulesets.Mods
|
||||
});
|
||||
}
|
||||
|
||||
shader.Bind();
|
||||
flashlightParametersBuffer ??= renderer.CreateUniformBuffer<FlashlightParameters>();
|
||||
flashlightParametersBuffer.Data = flashlightParametersBuffer.Data with
|
||||
{
|
||||
Position = flashlightPosition,
|
||||
Size = flashlightSize,
|
||||
Dim = flashlightDim,
|
||||
Smoothness = flashlightSmoothness
|
||||
};
|
||||
|
||||
shader.GetUniform<Vector2>("flashlightPos").UpdateValue(ref flashlightPosition);
|
||||
shader.GetUniform<Vector2>("flashlightSize").UpdateValue(ref flashlightSize);
|
||||
shader.GetUniform<float>("flashlightDim").UpdateValue(ref flashlightDim);
|
||||
shader.GetUniform<float>("flashlightSmoothness").UpdateValue(ref flashlightSmoothness);
|
||||
shader.Bind();
|
||||
shader.BindUniformBlock("m_FlashlightParameters", flashlightParametersBuffer);
|
||||
|
||||
renderer.DrawQuad(renderer.WhitePixel, screenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: addAction);
|
||||
|
||||
@ -278,6 +287,17 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
quadBatch?.Dispose();
|
||||
flashlightParametersBuffer?.Dispose();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private record struct FlashlightParameters
|
||||
{
|
||||
public UniformVector2 Position;
|
||||
public UniformVector2 Size;
|
||||
public UniformFloat Dim;
|
||||
public UniformFloat Smoothness;
|
||||
private readonly UniformPadding8 pad1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
MetronomeBeat metronomeBeat;
|
||||
|
||||
drawableRuleset.Overlays.Add(metronomeBeat = new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
// Importantly, this is added to FrameStableComponents and not Overlays as the latter would cause it to be self-muted by the mod's volume adjustment.
|
||||
drawableRuleset.FrameStableComponents.Add(metronomeBeat = new MetronomeBeat(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
|
||||
metronomeBeat.AddAdjustment(AdjustableProperty.Volume, metronomeVolumeAdjust);
|
||||
}
|
||||
|
@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Objects
|
||||
for (int i = 0; i < timingPoints.Count; i++)
|
||||
{
|
||||
TimingControlPoint currentTimingPoint = timingPoints[i];
|
||||
EffectControlPoint currentEffectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTimingPoint.Time);
|
||||
int currentBeat = 0;
|
||||
|
||||
// Don't generate barlines before the hit object or t=0 (whichever is earliest). Some beatmaps use very unrealistic values here (although none are ranked).
|
||||
@ -66,7 +65,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
startTime = currentTimingPoint.Time + barCount * barLength;
|
||||
}
|
||||
|
||||
if (currentEffectPoint.OmitFirstBarLine)
|
||||
if (currentTimingPoint.OmitFirstBarLine)
|
||||
{
|
||||
startTime += barLength;
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <summary>
|
||||
/// The key conversion input manager for this DrawableRuleset.
|
||||
/// </summary>
|
||||
public PassThroughInputManager KeyBindingInputManager;
|
||||
protected PassThroughInputManager KeyBindingInputManager;
|
||||
|
||||
public override double GameplayStartTime => Objects.FirstOrDefault()?.StartTime - 2000 ?? 0;
|
||||
|
||||
@ -66,6 +66,10 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
public override Container Overlays { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
public override IAdjustableAudioComponent Audio => audioContainer;
|
||||
|
||||
private readonly AudioContainer audioContainer = new AudioContainer { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
public override IFrameStableClock FrameStableClock => frameStabilityContainer;
|
||||
@ -102,14 +106,6 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private DrawableRulesetDependencies dependencies;
|
||||
|
||||
/// <summary>
|
||||
/// Audio adjustments which are applied to the playfield.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not affect <see cref="Overlays"/>.
|
||||
/// </remarks>
|
||||
public IAdjustableAudioComponent Audio { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ruleset visualisation for the provided ruleset and beatmap.
|
||||
/// </summary>
|
||||
@ -172,28 +168,22 @@ namespace osu.Game.Rulesets.UI
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(CancellationToken? cancellationToken)
|
||||
{
|
||||
AudioContainer audioContainer;
|
||||
|
||||
InternalChild = frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime)
|
||||
{
|
||||
FrameStablePlayback = FrameStablePlayback,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
FrameStableComponents,
|
||||
audioContainer = new AudioContainer
|
||||
audioContainer.WithChild(KeyBindingInputManager
|
||||
.WithChildren(new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = KeyBindingInputManager
|
||||
.WithChild(CreatePlayfieldAdjustmentContainer()
|
||||
.WithChild(Playfield)
|
||||
),
|
||||
},
|
||||
Overlays,
|
||||
CreatePlayfieldAdjustmentContainer()
|
||||
.WithChild(Playfield),
|
||||
Overlays
|
||||
})),
|
||||
}
|
||||
};
|
||||
|
||||
Audio = audioContainer;
|
||||
|
||||
if ((ResumeOverlay = CreateResumeOverlay()) != null)
|
||||
{
|
||||
AddInternal(CreateInputManager()
|
||||
@ -436,13 +426,18 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </summary>
|
||||
public readonly BindableBool IsPaused = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// Audio adjustments which are applied to the playfield.
|
||||
/// </summary>
|
||||
public abstract IAdjustableAudioComponent Audio { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The playfield.
|
||||
/// </summary>
|
||||
public abstract Playfield Playfield { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Content to be placed above hitobjects. Will be affected by frame stability.
|
||||
/// Content to be placed above hitobjects. Will be affected by frame stability and adjustments applied to <see cref="Audio"/>.
|
||||
/// </summary>
|
||||
public abstract Container Overlays { get; }
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
@ -68,11 +69,17 @@ namespace osu.Game.Rulesets.UI
|
||||
protected HitObject GetMostValidObject()
|
||||
{
|
||||
// The most optimal lookup case we have is when an object is alive. There are usually very few alive objects so there's no drawbacks in attempting this lookup each time.
|
||||
var hitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true)?.HitObject;
|
||||
var drawableHitObject = hitObjectContainer.AliveObjects.FirstOrDefault(h => h.Result?.HasResult != true);
|
||||
|
||||
if (drawableHitObject != null)
|
||||
{
|
||||
// A hit object may have a more valid nested object.
|
||||
drawableHitObject = getMostValidNestedDrawable(drawableHitObject);
|
||||
|
||||
return drawableHitObject.HitObject;
|
||||
}
|
||||
|
||||
// In the case a next object isn't available in drawable form, we need to do a somewhat expensive traversal to get a valid sound to play.
|
||||
if (hitObject == null)
|
||||
{
|
||||
// This lookup can be skipped if the last entry is still valid (in the future and not yet hit).
|
||||
if (fallbackObject == null || fallbackObject.Result?.HasResult == true)
|
||||
{
|
||||
@ -81,14 +88,42 @@ namespace osu.Game.Rulesets.UI
|
||||
fallbackObject = hitObjectContainer.Entries
|
||||
.Where(e => e.Result?.HasResult != true).MinBy(e => e.HitObject.StartTime);
|
||||
|
||||
// In the case there are no unjudged objects, the last hit object should be used instead.
|
||||
if (fallbackObject != null)
|
||||
return getEarliestNestedObject(fallbackObject.HitObject);
|
||||
|
||||
// In the case there are no non-judged objects, the last hit object should be used instead.
|
||||
fallbackObject ??= hitObjectContainer.Entries.LastOrDefault();
|
||||
}
|
||||
|
||||
hitObject = fallbackObject?.HitObject;
|
||||
if (fallbackObject == null)
|
||||
return null;
|
||||
|
||||
bool fallbackHasResult = fallbackObject.Result?.HasResult == true;
|
||||
|
||||
// If the fallback has been judged then we want the sample from the object itself.
|
||||
if (fallbackHasResult)
|
||||
return fallbackObject.HitObject;
|
||||
|
||||
// Else we want the earliest (including nested).
|
||||
// In cases of nested objects, they will always have earlier sample data than their parent object.
|
||||
return getEarliestNestedObject(fallbackObject.HitObject);
|
||||
}
|
||||
|
||||
return hitObject;
|
||||
private DrawableHitObject getMostValidNestedDrawable(DrawableHitObject o)
|
||||
{
|
||||
var nestedWithoutResult = o.NestedHitObjects.FirstOrDefault(n => n.Result?.HasResult != true);
|
||||
|
||||
if (nestedWithoutResult == null)
|
||||
return o;
|
||||
|
||||
return getMostValidNestedDrawable(nestedWithoutResult);
|
||||
}
|
||||
|
||||
private HitObject getEarliestNestedObject(HitObject hitObject)
|
||||
{
|
||||
var nested = hitObject.NestedHitObjects.FirstOrDefault();
|
||||
|
||||
return nested != null ? getEarliestNestedObject(nested) : hitObject;
|
||||
}
|
||||
|
||||
private SkinnableSound getNextSample()
|
||||
|
@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == recorder)
|
||||
return;
|
||||
|
||||
if (value != null && recorder != null)
|
||||
throw new InvalidOperationException("Cannot attach more than one recorder");
|
||||
|
||||
|
@ -7,12 +7,12 @@ using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public partial class DeleteDifficultyConfirmationDialog : DeleteConfirmationDialog
|
||||
public partial class DeleteDifficultyConfirmationDialog : DangerousActionDialog
|
||||
{
|
||||
public DeleteDifficultyConfirmationDialog(BeatmapInfo beatmapInfo, Action deleteAction)
|
||||
{
|
||||
BodyText = $"\"{beatmapInfo.DifficultyName}\" difficulty";
|
||||
DeleteAction = deleteAction;
|
||||
DangerousAction = deleteAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; } = null!;
|
||||
|
||||
public const float TIMING_COLUMN_WIDTH = 230;
|
||||
public const float TIMING_COLUMN_WIDTH = 300;
|
||||
|
||||
public IEnumerable<ControlPointGroup> ControlGroups
|
||||
{
|
||||
|
@ -14,7 +14,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
internal partial class EffectSection : Section<EffectControlPoint>
|
||||
{
|
||||
private LabelledSwitchButton kiai = null!;
|
||||
private LabelledSwitchButton omitBarLine = null!;
|
||||
|
||||
private SliderWithTextBoxInput<double> scrollSpeedSlider = null!;
|
||||
|
||||
@ -24,7 +23,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
Flow.AddRange(new Drawable[]
|
||||
{
|
||||
kiai = new LabelledSwitchButton { Label = "Kiai Time" },
|
||||
omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
|
||||
scrollSpeedSlider = new SliderWithTextBoxInput<double>("Scroll Speed")
|
||||
{
|
||||
Current = new EffectControlPoint().ScrollSpeedBindable,
|
||||
@ -38,7 +36,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
base.LoadComplete();
|
||||
|
||||
kiai.Current.BindValueChanged(_ => saveChanges());
|
||||
omitBarLine.Current.BindValueChanged(_ => saveChanges());
|
||||
scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges());
|
||||
|
||||
var drawableRuleset = Beatmap.BeatmapInfo.Ruleset.CreateInstance().CreateDrawableRulesetWith(Beatmap.PlayableBeatmap);
|
||||
@ -60,7 +57,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
isRebinding = true;
|
||||
|
||||
kiai.Current = point.NewValue.KiaiModeBindable;
|
||||
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
||||
scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
|
||||
|
||||
isRebinding = false;
|
||||
@ -74,7 +70,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
return new EffectControlPoint
|
||||
{
|
||||
KiaiMode = reference.KiaiMode,
|
||||
OmitFirstBarLine = reference.OmitFirstBarLine,
|
||||
ScrollSpeed = reference.ScrollSpeed,
|
||||
};
|
||||
}
|
||||
|
@ -11,18 +11,15 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
||||
public partial class EffectRowAttribute : RowAttribute
|
||||
{
|
||||
private readonly Bindable<bool> kiaiMode;
|
||||
private readonly Bindable<bool> omitBarLine;
|
||||
private readonly BindableNumber<double> scrollSpeed;
|
||||
|
||||
private AttributeText kiaiModeBubble = null!;
|
||||
private AttributeText omitBarLineBubble = null!;
|
||||
private AttributeText text = null!;
|
||||
|
||||
public EffectRowAttribute(EffectControlPoint effect)
|
||||
: base(effect, "effect")
|
||||
{
|
||||
kiaiMode = effect.KiaiModeBindable.GetBoundCopy();
|
||||
omitBarLine = effect.OmitFirstBarLineBindable.GetBoundCopy();
|
||||
scrollSpeed = effect.ScrollSpeedBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
@ -37,11 +34,9 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
||||
},
|
||||
text = new AttributeText(Point) { Width = 45 },
|
||||
kiaiModeBubble = new AttributeText(Point) { Text = "kiai" },
|
||||
omitBarLineBubble = new AttributeText(Point) { Text = "no barline" },
|
||||
});
|
||||
|
||||
kiaiMode.BindValueChanged(enabled => kiaiModeBubble.FadeTo(enabled.NewValue ? 1 : 0), true);
|
||||
omitBarLine.BindValueChanged(enabled => omitBarLineBubble.FadeTo(enabled.NewValue ? 1 : 0), true);
|
||||
scrollSpeed.BindValueChanged(_ => updateText(), true);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -14,24 +15,32 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
||||
public partial class TimingRowAttribute : RowAttribute
|
||||
{
|
||||
private readonly BindableNumber<double> beatLength;
|
||||
private readonly Bindable<bool> omitBarLine;
|
||||
private readonly Bindable<TimeSignature> timeSignature;
|
||||
private AttributeText omitBarLineBubble = null!;
|
||||
private OsuSpriteText text = null!;
|
||||
|
||||
public TimingRowAttribute(TimingControlPoint timing)
|
||||
: base(timing, "timing")
|
||||
{
|
||||
timeSignature = timing.TimeSignatureBindable.GetBoundCopy();
|
||||
omitBarLine = timing.OmitFirstBarLineBindable.GetBoundCopy();
|
||||
beatLength = timing.BeatLengthBindable.GetBoundCopy();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
Content.Add(text = new AttributeText(Point));
|
||||
Content.AddRange(new[]
|
||||
{
|
||||
text = new AttributeText(Point),
|
||||
omitBarLineBubble = new AttributeText(Point) { Text = "no barline" },
|
||||
});
|
||||
|
||||
Background.Colour = colourProvider.Background4;
|
||||
|
||||
timeSignature.BindValueChanged(_ => updateText());
|
||||
omitBarLine.BindValueChanged(enabled => omitBarLineBubble.FadeTo(enabled.NewValue ? 1 : 0), true);
|
||||
beatLength.BindValueChanged(_ => updateText(), true);
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
internal partial class TimingSection : Section<TimingControlPoint>
|
||||
{
|
||||
private LabelledTimeSignature timeSignature = null!;
|
||||
private LabelledSwitchButton omitBarLine = null!;
|
||||
private BPMTextBox bpmTextEntry = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -24,7 +25,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
timeSignature = new LabelledTimeSignature
|
||||
{
|
||||
Label = "Time Signature"
|
||||
}
|
||||
},
|
||||
omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" },
|
||||
});
|
||||
}
|
||||
|
||||
@ -33,6 +35,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
base.LoadComplete();
|
||||
|
||||
bpmTextEntry.Current.BindValueChanged(_ => saveChanges());
|
||||
omitBarLine.Current.BindValueChanged(_ => saveChanges());
|
||||
timeSignature.Current.BindValueChanged(_ => saveChanges());
|
||||
|
||||
void saveChanges()
|
||||
@ -51,6 +54,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
|
||||
timeSignature.Current = point.NewValue.TimeSignatureBindable;
|
||||
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
||||
|
||||
isRebinding = false;
|
||||
}
|
||||
@ -63,7 +67,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
return new TimingControlPoint
|
||||
{
|
||||
BeatLength = reference.BeatLength,
|
||||
TimeSignature = reference.TimeSignature
|
||||
TimeSignature = reference.TimeSignature,
|
||||
OmitFirstBarLine = reference.OmitFirstBarLine,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Screens.Play
|
||||
Anchor = Anchor.Centre,
|
||||
FillMode = FillMode.Fill,
|
||||
},
|
||||
loading = new LoadingLayer(true)
|
||||
loading = new LoadingLayer(dimBackground: true, blockInput: false)
|
||||
}
|
||||
},
|
||||
versionFlow = new FillFlowContainer
|
||||
|
@ -242,7 +242,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
length = value;
|
||||
mask.Width = value * DrawWidth;
|
||||
fill.Width = value * DrawWidth;
|
||||
}
|
||||
}
|
||||
|
||||
|
58
osu.Game/Screens/Play/HUD/PlayerAvatar.cs
Normal file
58
osu.Game/Screens/Play/HUD/PlayerAvatar.cs
Normal file
@ -0,0 +1,58 @@
|
||||
// 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.Game.Configuration;
|
||||
using osu.Game.Localisation.SkinComponents;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class PlayerAvatar : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription),
|
||||
SettingControlType = typeof(SettingsPercentageSlider<float>))]
|
||||
public new BindableFloat CornerRadius { get; set; } = new BindableFloat(0.25f)
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 0.5f,
|
||||
Precision = 0.01f
|
||||
};
|
||||
|
||||
private readonly UpdateableAvatar avatar;
|
||||
|
||||
private const float default_size = 80f;
|
||||
|
||||
public PlayerAvatar()
|
||||
{
|
||||
Size = new Vector2(default_size);
|
||||
|
||||
InternalChild = avatar = new UpdateableAvatar(isInteractive: false)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameplayState gameplayState)
|
||||
{
|
||||
avatar.User = gameplayState.Score.ScoreInfo.User;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
CornerRadius.BindValueChanged(e => avatar.CornerRadius = e.NewValue * default_size, true);
|
||||
}
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
}
|
||||
}
|
36
osu.Game/Screens/Play/HUD/PlayerFlag.cs
Normal file
36
osu.Game/Screens/Play/HUD/PlayerFlag.cs
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
public partial class PlayerFlag : CompositeDrawable, ISerialisableDrawable
|
||||
{
|
||||
private readonly UpdateableFlag flag;
|
||||
|
||||
private const float default_size = 40f;
|
||||
|
||||
public PlayerFlag()
|
||||
{
|
||||
Size = new Vector2(default_size, default_size / 1.4f);
|
||||
InternalChild = flag = new UpdateableFlag
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameplayState gameplayState)
|
||||
{
|
||||
flag.CountryCode = gameplayState.Score.ScoreInfo.User.CountryCode;
|
||||
}
|
||||
|
||||
public bool UsesFixedAnchor { get; set; }
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
@ -26,6 +27,7 @@ using osu.Game.Screens.Play.HUD.JudgementCounter;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
@ -100,20 +102,22 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, bool alwaysShowLeaderboard = true)
|
||||
{
|
||||
Drawable rulesetComponents;
|
||||
|
||||
this.drawableRuleset = drawableRuleset;
|
||||
this.mods = mods;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
Children = new[]
|
||||
{
|
||||
CreateFailingLayer(),
|
||||
//Needs to be initialized before skinnable drawables.
|
||||
tally = new JudgementTally(),
|
||||
mainComponents = new MainComponentsContainer
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
mainComponents = new HUDComponentsContainer { AlwaysPresent = true, },
|
||||
rulesetComponents = drawableRuleset != null
|
||||
? new HUDComponentsContainer(drawableRuleset.Ruleset.RulesetInfo) { AlwaysPresent = true, }
|
||||
: Empty(),
|
||||
topRightElements = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
@ -155,7 +159,7 @@ namespace osu.Game.Screens.Play
|
||||
clicksPerSecondCalculator = new ClicksPerSecondCalculator(),
|
||||
};
|
||||
|
||||
hideTargets = new List<Drawable> { mainComponents, KeyCounter, topRightElements };
|
||||
hideTargets = new List<Drawable> { mainComponents, rulesetComponents, KeyCounter, topRightElements };
|
||||
|
||||
if (!alwaysShowLeaderboard)
|
||||
hideTargets.Add(LeaderboardFlow);
|
||||
@ -390,15 +394,15 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
private partial class MainComponentsContainer : SkinComponentsContainer
|
||||
private partial class HUDComponentsContainer : SkinComponentsContainer
|
||||
{
|
||||
private Bindable<ScoringMode> scoringMode;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
public MainComponentsContainer()
|
||||
: base(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.MainHUDComponents))
|
||||
public HUDComponentsContainer([CanBeNull] RulesetInfo ruleset = null)
|
||||
: base(new SkinComponentsContainerLookup(SkinComponentsContainerLookup.TargetArea.MainHUDComponents, ruleset))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ namespace osu.Game.Screens.Play
|
||||
},
|
||||
FailOverlay = new FailOverlay
|
||||
{
|
||||
SaveReplay = prepareAndImportScore,
|
||||
SaveReplay = async () => await prepareAndImportScoreAsync(true).ConfigureAwait(false),
|
||||
OnRetry = () => Restart(),
|
||||
OnQuit = () => PerformExit(true),
|
||||
},
|
||||
@ -613,6 +613,9 @@ namespace osu.Game.Screens.Play
|
||||
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
// import current score if possible.
|
||||
prepareAndImportScoreAsync();
|
||||
|
||||
// The actual exit is performed if
|
||||
// - the pause / fail dialog was not requested
|
||||
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
|
||||
@ -735,14 +738,9 @@ namespace osu.Game.Screens.Play
|
||||
// is no chance that a user could return to the (already completed) Player instance from a child screen.
|
||||
ValidForResume = false;
|
||||
|
||||
// Ensure we are not writing to the replay any more, as we are about to consume and store the score.
|
||||
DrawableRuleset.SetRecordTarget(null);
|
||||
|
||||
if (!Configuration.ShowResults)
|
||||
return;
|
||||
|
||||
prepareScoreForDisplayTask ??= Task.Run(prepareAndImportScore);
|
||||
|
||||
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
|
||||
|
||||
if (storyboardHasOutro)
|
||||
@ -756,11 +754,70 @@ namespace osu.Game.Screens.Play
|
||||
progressToResults(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue the results screen for display.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
|
||||
/// </remarks>
|
||||
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
|
||||
private void progressToResults(bool withDelay)
|
||||
{
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
|
||||
|
||||
resultsDisplayDelegate = new ScheduledDelegate(() =>
|
||||
{
|
||||
if (prepareScoreForDisplayTask == null)
|
||||
{
|
||||
// Try importing score since the task hasn't been invoked yet.
|
||||
prepareAndImportScoreAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prepareScoreForDisplayTask.IsCompleted)
|
||||
// If the asynchronous preparation has not completed, keep repeating this delegate.
|
||||
return;
|
||||
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
if (prepareScoreForDisplayTask.GetResultSafely() == null)
|
||||
{
|
||||
// If score import did not occur, we do not want to show the results screen.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.IsCurrentScreen())
|
||||
// This player instance may already be in the process of exiting.
|
||||
return;
|
||||
|
||||
this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely()));
|
||||
}, Time.Current + delay, 50);
|
||||
|
||||
Scheduler.Add(resultsDisplayDelegate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously run score preparation operations (database import, online submission etc.).
|
||||
/// </summary>
|
||||
/// <param name="forceImport">Whether the score should be imported even if non-passing (or the current configuration doesn't allow for it).</param>
|
||||
/// <returns>The final score.</returns>
|
||||
private async Task<ScoreInfo> prepareAndImportScore()
|
||||
[ItemCanBeNull]
|
||||
private Task<ScoreInfo> prepareAndImportScoreAsync(bool forceImport = false)
|
||||
{
|
||||
// Ensure we are not writing to the replay any more, as we are about to consume and store the score.
|
||||
DrawableRuleset.SetRecordTarget(null);
|
||||
|
||||
if (prepareScoreForDisplayTask != null)
|
||||
return prepareScoreForDisplayTask;
|
||||
|
||||
// We do not want to import the score in cases where we don't show results
|
||||
bool canShowResults = Configuration.ShowResults && ScoreProcessor.HasCompleted.Value && GameplayState.HasPassed;
|
||||
if (!canShowResults && !forceImport)
|
||||
return Task.FromResult<ScoreInfo>(null);
|
||||
|
||||
return prepareScoreForDisplayTask = Task.Run(async () =>
|
||||
{
|
||||
var scoreCopy = Score.DeepClone();
|
||||
|
||||
@ -783,37 +840,7 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
|
||||
return scoreCopy.ScoreInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue the results screen for display.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
|
||||
/// </remarks>
|
||||
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
|
||||
private void progressToResults(bool withDelay)
|
||||
{
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
|
||||
|
||||
resultsDisplayDelegate = new ScheduledDelegate(() =>
|
||||
{
|
||||
if (prepareScoreForDisplayTask?.IsCompleted != true)
|
||||
// If the asynchronous preparation has not completed, keep repeating this delegate.
|
||||
return;
|
||||
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
if (!this.IsCurrentScreen())
|
||||
// This player instance may already be in the process of exiting.
|
||||
return;
|
||||
|
||||
this.Push(CreateResults(prepareScoreForDisplayTask.GetResultSafely()));
|
||||
}, Time.Current + delay, 50);
|
||||
|
||||
Scheduler.Add(resultsDisplayDelegate);
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool OnScroll(ScrollEvent e)
|
||||
|
@ -10,7 +10,7 @@ using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class BeatmapClearScoresDialog : DeleteConfirmationDialog
|
||||
public partial class BeatmapClearScoresDialog : DangerousActionDialog
|
||||
{
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
@ -18,7 +18,7 @@ namespace osu.Game.Screens.Select
|
||||
public BeatmapClearScoresDialog(BeatmapInfo beatmapInfo, Action onCompletion)
|
||||
{
|
||||
BodyText = $"All local scores on {beatmapInfo.GetDisplayTitle()}";
|
||||
DeleteAction = () =>
|
||||
DangerousAction = () =>
|
||||
{
|
||||
Task.Run(() => scoreManager.Delete(beatmapInfo))
|
||||
.ContinueWith(_ => onCompletion);
|
||||
|
@ -7,7 +7,7 @@ using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
public partial class BeatmapDeleteDialog : DeleteConfirmationDialog
|
||||
public partial class BeatmapDeleteDialog : DangerousActionDialog
|
||||
{
|
||||
private readonly BeatmapSetInfo beatmapSet;
|
||||
|
||||
@ -20,7 +20,7 @@ namespace osu.Game.Screens.Select
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(BeatmapManager beatmapManager)
|
||||
{
|
||||
DeleteAction = () => beatmapManager.Delete(beatmapSet);
|
||||
DangerousAction = () => beatmapManager.Delete(beatmapSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
base.Filter(criteria);
|
||||
|
||||
Filtered.Value = !checkMatch(criteria);
|
||||
}
|
||||
|
||||
private bool checkMatch(FilterCriteria criteria)
|
||||
{
|
||||
bool match =
|
||||
criteria.Ruleset == null ||
|
||||
BeatmapInfo.Ruleset.ShortName == criteria.Ruleset.ShortName ||
|
||||
@ -34,8 +39,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true)
|
||||
{
|
||||
// only check ruleset equality or convertability for selected beatmap
|
||||
Filtered.Value = !match;
|
||||
return;
|
||||
return match;
|
||||
}
|
||||
|
||||
match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarRating);
|
||||
@ -49,18 +53,38 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor);
|
||||
match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(BeatmapInfo.Status);
|
||||
|
||||
if (!match) return false;
|
||||
|
||||
match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username);
|
||||
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) ||
|
||||
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
|
||||
|
||||
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
|
||||
|
||||
if (match && criteria.SearchTerms.Length > 0)
|
||||
if (!match) return false;
|
||||
|
||||
if (criteria.SearchTerms.Length > 0)
|
||||
{
|
||||
string[] terms = BeatmapInfo.GetSearchableTerms();
|
||||
var terms = BeatmapInfo.GetSearchableTerms();
|
||||
|
||||
foreach (string criteriaTerm in criteria.SearchTerms)
|
||||
match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase));
|
||||
{
|
||||
bool any = false;
|
||||
|
||||
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (string term in terms)
|
||||
{
|
||||
if (!term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)) continue;
|
||||
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (any) continue;
|
||||
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs.
|
||||
// this should be done after text matching so we can prioritise matching numbers in metadata.
|
||||
@ -71,13 +95,14 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
}
|
||||
}
|
||||
|
||||
if (match)
|
||||
if (!match) return false;
|
||||
|
||||
match &= criteria.CollectionBeatmapMD5Hashes?.Contains(BeatmapInfo.MD5Hash) ?? true;
|
||||
|
||||
if (match && criteria.RulesetCriteria != null)
|
||||
match &= criteria.RulesetCriteria.Matches(BeatmapInfo);
|
||||
|
||||
Filtered.Value = !match;
|
||||
return match;
|
||||
}
|
||||
|
||||
public override int CompareTo(FilterCriteria criteria, CarouselItem other)
|
||||
|
@ -61,7 +61,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
if (!(other is CarouselBeatmapSet otherSet))
|
||||
return base.CompareTo(criteria, other);
|
||||
|
||||
int comparison = 0;
|
||||
int comparison;
|
||||
|
||||
switch (criteria.Sort)
|
||||
{
|
||||
@ -87,11 +87,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
break;
|
||||
|
||||
case SortMode.DateRanked:
|
||||
// Beatmaps which have no ranked date should already be filtered away in this mode.
|
||||
if (BeatmapSet.DateRanked == null || otherSet.BeatmapSet.DateRanked == null)
|
||||
break;
|
||||
|
||||
comparison = otherSet.BeatmapSet.DateRanked.Value.CompareTo(BeatmapSet.DateRanked.Value);
|
||||
comparison = Nullable.Compare(otherSet.BeatmapSet.DateRanked, BeatmapSet.DateRanked);
|
||||
break;
|
||||
|
||||
case SortMode.LastPlayed:
|
||||
@ -111,11 +107,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
break;
|
||||
|
||||
case SortMode.DateSubmitted:
|
||||
// Beatmaps which have no submitted date should already be filtered away in this mode.
|
||||
if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null)
|
||||
break;
|
||||
|
||||
comparison = otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
|
||||
comparison = Nullable.Compare(otherSet.BeatmapSet.DateSubmitted, BeatmapSet.DateSubmitted);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -153,12 +145,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
{
|
||||
base.Filter(criteria);
|
||||
|
||||
bool filtered = Items.All(i => i.Filtered.Value);
|
||||
|
||||
filtered |= criteria.Sort == SortMode.DateRanked && BeatmapSet.DateRanked == null;
|
||||
filtered |= criteria.Sort == SortMode.DateSubmitted && BeatmapSet.DateSubmitted == null;
|
||||
|
||||
Filtered.Value = filtered;
|
||||
Filtered.Value = Items.All(i => i.Filtered.Value);
|
||||
}
|
||||
|
||||
public override string ToString() => BeatmapSet.ToString();
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user