mirror of
https://github.com/ppy/osu.git
synced 2026-05-14 03:42:36 +08:00
Compare commits
51 Commits
2026.429.0
..
master
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.428.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.513.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -20,13 +20,24 @@ using Uri = Android.Net.Uri;
|
||||
namespace osu.Android
|
||||
{
|
||||
[Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeType = "application/x-osu-replay")]
|
||||
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import beatmap", DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*",
|
||||
DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import skin", DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*",
|
||||
DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import replay", DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*",
|
||||
DataMimeType = "*/*")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import beatmap", DataScheme = "content", DataMimeType = "application/x-osu-beatmap-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import skin", DataScheme = "content", DataMimeType = "application/x-osu-skin-archive")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import replay", DataScheme = "content", DataMimeType = "application/x-osu-replay")]
|
||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, Label = "Import file", DataScheme = "content", DataMimeTypes = new[]
|
||||
{
|
||||
"application/zip",
|
||||
"application/octet-stream",
|
||||
"application/download",
|
||||
"application/x-zip",
|
||||
"application/x-zip-compressed",
|
||||
})]
|
||||
[IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, Label = "Import", DataMimeTypes = new[]
|
||||
{
|
||||
"application/zip",
|
||||
"application/octet-stream",
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="System.IO.Packaging" Version="10.0.5" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.6.1.70" />
|
||||
<!-- Held back due to invite bug in newer versions. See https://github.com/Lachee/discord-rpc-csharp/issues/286-->
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.5.0.51" />
|
||||
<PackageReference Include="Velopack" Version="0.0.1298" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Resources">
|
||||
|
||||
@@ -14,6 +14,9 @@ namespace osu.Game.Rulesets.Catch.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
// Audio
|
||||
new CheckCatchFewHitsounds(),
|
||||
|
||||
// Compose
|
||||
new CheckBananaShowerGap(),
|
||||
new CheckConcurrentObjects(),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Edit.Checks
|
||||
{
|
||||
public class CheckCatchFewHitsounds : CheckFewHitsounds
|
||||
{
|
||||
protected override bool IsExcludedFromHitsounding(HitObject hitObject) => hitObject is BananaShower;
|
||||
}
|
||||
}
|
||||
@@ -117,6 +117,15 @@ namespace osu.Game.Rulesets.Catch.Edit.Setup
|
||||
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
|
||||
// in lazer catch, Overall Difficulty does *nothing* - as it should be in a sane world.
|
||||
// in stable, it does *one extremely specific thing* which is influence the infamous `difficultyPeppyStars`
|
||||
// which in turn affects score V1 (see `LegacyRulesetExtensions.CalculateDifficultyPeppyStars()`).
|
||||
// there is a Ranking Criteria rule saying that Overall Difficulty and Approach Rate should match:
|
||||
// https://osu.ppy.sh/wiki/en/Ranking_criteria/osu!catch
|
||||
// the one case wherein that breaks stable is on some marathon maps;
|
||||
// on those setting Overall Difficulty too high can lead to score V1 exceeding 32 bits ("score overflow").
|
||||
// that case can be manually handled by mappers.
|
||||
Beatmap.Difficulty.OverallDifficulty = approachRateSlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestKeyCountChange()
|
||||
{
|
||||
FormSliderBar<float> keyCount = null!;
|
||||
FormSliderBar<int> keyCount = null!;
|
||||
|
||||
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<float>>().First(), () => Is.Not.Null);
|
||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<int>>().First(), () => Is.Not.Null);
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
AddStep("change key count to 8", () =>
|
||||
{
|
||||
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("refuse", () => InputManager.Key(Key.Number2));
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
AddUntilStep("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
|
||||
AddStep("change key count to 8 again", () =>
|
||||
{
|
||||
@@ -41,5 +41,32 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
|
||||
AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDualStagesChange()
|
||||
{
|
||||
FormCheckBox dualStages = null!;
|
||||
FormSliderBar<int> keyCount = null!;
|
||||
|
||||
AddStep("go to setup screen", () => InputManager.Key(Key.F4));
|
||||
AddUntilStep("retrieve dual stages checkbox", () => dualStages = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormCheckBox>().First(), () => Is.Not.Null);
|
||||
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<int>>().First(), () => Is.Not.Null);
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
AddStep("set dual stages", () =>
|
||||
{
|
||||
dualStages.Current.Value = true;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("refuse", () => InputManager.Key(Key.Number2));
|
||||
AddUntilStep("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
|
||||
AddStep("set dual stages again", () =>
|
||||
{
|
||||
dualStages.Current.Value = true;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
|
||||
AddUntilStep("beatmap became 12K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(12));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,28 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
AddAssert("time is unchanged", () => EditorClock.CurrentTime, () => Is.EqualTo(initialTime));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoTwoObjectsAtSameTimeAndColumn()
|
||||
{
|
||||
AddStep("change seek setting to false", () => config.SetValue(OsuSetting.EditorAutoSeekOnPlacement, false));
|
||||
AddStep("clear beatmap", () => EditorBeatmap.Clear());
|
||||
|
||||
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to centre of last column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last().ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("place note", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("beatmap has 1 object", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(1));
|
||||
|
||||
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to centre of first column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().First().ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("place note", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("beatmap has 2 objects", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2));
|
||||
|
||||
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("move mouse to centre of last column", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Last().ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("place note", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("beatmap has 2 objects", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(2));
|
||||
}
|
||||
|
||||
private void placeObject()
|
||||
{
|
||||
AddStep("select note placement tool", () => InputManager.Key(Key.Number2));
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
@@ -19,13 +20,22 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
private FormSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||
private FormSliderBar<int> keyCountSlider { get; set; } = null!;
|
||||
private FormCheckBox dualStages { get; set; } = null!;
|
||||
private FormCheckBox specialStyle { get; set; } = null!;
|
||||
private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private FormSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
private readonly BindableInt singleStageKeyCount = new BindableInt
|
||||
{
|
||||
Default = (int)BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
Precision = 1,
|
||||
};
|
||||
|
||||
private readonly BindableInt actualKeyCount = new BindableInt();
|
||||
|
||||
[Resolved]
|
||||
private Editor? editor { get; set; }
|
||||
|
||||
@@ -37,20 +47,19 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
keyCountSlider = new FormSliderBar<float>
|
||||
keyCountSlider = new FormSliderBar<int>
|
||||
{
|
||||
Caption = BeatmapsetsStrings.ShowStatsCsMania,
|
||||
HintText = "The number of columns in the beatmap",
|
||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 1,
|
||||
},
|
||||
Current = singleStageKeyCount,
|
||||
TransferValueOnCommit = true,
|
||||
TabbableContentContainer = this,
|
||||
},
|
||||
dualStages = new FormCheckBox
|
||||
{
|
||||
Caption = "Dual stages",
|
||||
HintText = "Doubles the number of keys by adding a second stage."
|
||||
},
|
||||
specialStyle = new FormCheckBox
|
||||
{
|
||||
Caption = "Use special (N+1) style",
|
||||
@@ -117,16 +126,54 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
},
|
||||
};
|
||||
|
||||
keyCountSlider.Current.BindValueChanged(updateKeyCount);
|
||||
setStateFromActualKeyCount((int)Beatmap.Difficulty.CircleSize);
|
||||
|
||||
keyCountSlider.Current.BindValueChanged(_ => calculateActualKeyCount());
|
||||
dualStages.Current.BindValueChanged(_ =>
|
||||
{
|
||||
updateSingleStageKeyCountBounds();
|
||||
calculateActualKeyCount();
|
||||
});
|
||||
actualKeyCount.BindValueChanged(updateKeyCount);
|
||||
|
||||
healthDrainSlider.Current.BindValueChanged(_ => updateValues());
|
||||
overallDifficultySlider.Current.BindValueChanged(_ => updateValues());
|
||||
baseVelocitySlider.Current.BindValueChanged(_ => updateValues());
|
||||
tickRateSlider.Current.BindValueChanged(_ => updateValues());
|
||||
}
|
||||
|
||||
private void updateSingleStageKeyCountBounds()
|
||||
{
|
||||
singleStageKeyCount.MinValue = dualStages.Current.Value ? ManiaRuleset.MAX_STAGE_KEYS / 2 + 1 : 1;
|
||||
singleStageKeyCount.MaxValue = dualStages.Current.Value ? LegacyBeatmapDecoder.MAX_MANIA_KEY_COUNT / 2 : ManiaRuleset.MAX_STAGE_KEYS;
|
||||
}
|
||||
|
||||
private void setStateFromActualKeyCount(int keyCount)
|
||||
{
|
||||
actualKeyCount.Value = keyCount;
|
||||
|
||||
if (keyCount > 10)
|
||||
{
|
||||
dualStages.Current.Value = true;
|
||||
singleStageKeyCount.Value = keyCount / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
dualStages.Current.Value = false;
|
||||
singleStageKeyCount.Value = keyCount;
|
||||
}
|
||||
|
||||
updateSingleStageKeyCountBounds();
|
||||
}
|
||||
|
||||
private void calculateActualKeyCount()
|
||||
{
|
||||
actualKeyCount.Value = keyCountSlider.Current.Value * (dualStages.Current.Value ? 2 : 1);
|
||||
}
|
||||
|
||||
private bool updatingKeyCount;
|
||||
|
||||
private void updateKeyCount(ValueChangedEvent<float> keyCount)
|
||||
private void updateKeyCount(ValueChangedEvent<int> keyCount)
|
||||
{
|
||||
if (updatingKeyCount) return;
|
||||
|
||||
@@ -143,7 +190,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
Schedule(() =>
|
||||
{
|
||||
changeHandler!.RestoreState(-1);
|
||||
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value = keyCount.OldValue;
|
||||
Beatmap.Difficulty.CircleSize = keyCount.OldValue;
|
||||
setStateFromActualKeyCount(keyCount.OldValue);
|
||||
updatingKeyCount = false;
|
||||
});
|
||||
}
|
||||
@@ -158,7 +206,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
|
||||
Beatmap.Difficulty.CircleSize = actualKeyCount.Value;
|
||||
Beatmap.SpecialStyle = specialStyle.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
|
||||
|
||||
@@ -485,6 +485,22 @@ namespace osu.Game.Rulesets.Mania
|
||||
};
|
||||
}
|
||||
|
||||
public override IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForRankedPlayCard(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods)
|
||||
{
|
||||
var attributes = GetBeatmapAttributesForDisplay(beatmapInfo, mods).ToList();
|
||||
|
||||
// Key count attribute isn't relevant to ranked play (it's decided by the pool).
|
||||
attributes.RemoveAll(a => a.Acronym == "KC");
|
||||
|
||||
float holdNoteRatio = beatmapInfo.TotalObjectCount == 0 ? 0 : (float)beatmapInfo.EndTimeObjectCount / beatmapInfo.TotalObjectCount;
|
||||
attributes.Insert(0, new RulesetBeatmapAttribute("Hold notes", @"HN", holdNoteRatio, holdNoteRatio, 1)
|
||||
{
|
||||
ValueFormat = "P0"
|
||||
});
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()
|
||||
{
|
||||
return new ManiaFilterCriteria();
|
||||
|
||||
@@ -73,6 +73,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddStep("end slider placement", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddStep("seek to slider end", () =>
|
||||
{
|
||||
var slider = (Slider)EditorBeatmap.HitObjects.Single();
|
||||
EditorClock.Seek(slider.EndTime);
|
||||
});
|
||||
|
||||
AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2));
|
||||
|
||||
AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.205f, 0)));
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
{
|
||||
public class CheckOsuFewHitsounds : CheckFewHitsounds
|
||||
{
|
||||
protected override bool IsExcludedFromHitsounding(HitObject hitObject) => hitObject is Spinner;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
// Audio
|
||||
new CheckOsuFewHitsounds(),
|
||||
|
||||
// Compose
|
||||
new CheckOffscreenObjects(),
|
||||
new CheckTooShortSpinners(),
|
||||
|
||||
@@ -125,13 +125,23 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
startPositionXSlider = new ExpandableSlider<float>
|
||||
{
|
||||
Current = StartPositionX,
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = -OsuPlayfield.BASE_SIZE.X / 2,
|
||||
MaxValue = OsuPlayfield.BASE_SIZE.X / 2,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
KeyboardStep = 1,
|
||||
ExpandedLabelText = "X offset",
|
||||
},
|
||||
startPositionYSlider = new ExpandableSlider<float>
|
||||
{
|
||||
Current = StartPositionY,
|
||||
Current = new BindableFloat
|
||||
{
|
||||
MinValue = -OsuPlayfield.BASE_SIZE.Y / 2,
|
||||
MaxValue = OsuPlayfield.BASE_SIZE.Y / 2,
|
||||
Precision = 0.1f,
|
||||
},
|
||||
KeyboardStep = 1,
|
||||
ExpandedLabelText = "Y offset",
|
||||
},
|
||||
@@ -186,15 +196,27 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
StartPositionX.BindValueChanged(x =>
|
||||
{
|
||||
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}";
|
||||
startPositionXSlider.Current.Value = x.NewValue - OsuPlayfield.BASE_SIZE.X / 2;
|
||||
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
|
||||
}, true);
|
||||
|
||||
StartPositionY.BindValueChanged(y =>
|
||||
{
|
||||
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}";
|
||||
startPositionYSlider.Current.Value = y.NewValue - OsuPlayfield.BASE_SIZE.Y / 2;
|
||||
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
|
||||
}, true);
|
||||
|
||||
startPositionXSlider.Current.BindValueChanged(x =>
|
||||
{
|
||||
StartPositionX.Value = x.NewValue + OsuPlayfield.BASE_SIZE.X / 2;
|
||||
});
|
||||
|
||||
startPositionYSlider.Current.BindValueChanged(y =>
|
||||
{
|
||||
StartPositionY.Value = y.NewValue + OsuPlayfield.BASE_SIZE.Y / 2;
|
||||
});
|
||||
|
||||
StartPosition.BindValueChanged(pos =>
|
||||
{
|
||||
StartPositionX.Value = pos.NewValue.X;
|
||||
|
||||
@@ -21,7 +21,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public partial class OsuModBlinds : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToHealthProcessor
|
||||
public partial class OsuModBlinds : ModBlinds, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToHealthProcessor
|
||||
{
|
||||
public override string Name => "Blinds";
|
||||
public override LocalisableString Description => "Play with blinds on your screen.";
|
||||
|
||||
@@ -16,7 +16,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModTraceable : ModWithVisibilityAdjustment, IRequiresApproachCircles
|
||||
public class OsuModTraceable : ModTraceable, IRequiresApproachCircles
|
||||
{
|
||||
public override string Name => "Traceable";
|
||||
public override string Acronym => "TC";
|
||||
|
||||
@@ -18,11 +18,13 @@ namespace osu.Game.Rulesets.Taiko.Configuration
|
||||
base.InitialiseDefaults();
|
||||
|
||||
SetDefault(TaikoRulesetSetting.TouchControlScheme, TaikoTouchControlScheme.KDDK);
|
||||
SetDefault(TaikoRulesetSetting.RateAdjustedHitAnimation, true);
|
||||
}
|
||||
}
|
||||
|
||||
public enum TaikoRulesetSetting
|
||||
{
|
||||
TouchControlScheme
|
||||
TouchControlScheme,
|
||||
RateAdjustedHitAnimation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Configuration;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
@@ -34,12 +37,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private set;
|
||||
}
|
||||
|
||||
private bool validActionPressed;
|
||||
|
||||
private double? lastPressHandleTime;
|
||||
[Resolved(CanBeNull = true)]
|
||||
private TaikoRulesetConfigManager taikoConfig { get; set; }
|
||||
|
||||
private readonly Bindable<bool> rateAdjustedHitAnimations = new Bindable<bool>(true);
|
||||
private readonly Bindable<HitType> type = new Bindable<HitType>();
|
||||
|
||||
private bool validActionPressed;
|
||||
private double? lastPressHandleTime;
|
||||
|
||||
public DrawableHit()
|
||||
: this(null)
|
||||
{
|
||||
@@ -51,6 +57,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
FillMode = FillMode.Fit;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
taikoConfig?.BindWith(TaikoRulesetSetting.RateAdjustedHitAnimation, rateAdjustedHitAnimations);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
{
|
||||
type.BindTo(HitObject.TypeBindable);
|
||||
@@ -168,11 +180,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (SnapJudgementLocation)
|
||||
MainPiece.MoveToX(-X);
|
||||
|
||||
this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad);
|
||||
// Rate independent to match stable.
|
||||
double rate = Math.Abs((Clock as IGameplayClock)?.GetTrueGameplayRate() ?? Clock.Rate);
|
||||
double length = gravity_time * (rateAdjustedHitAnimations.Value ? 1 : rate);
|
||||
|
||||
this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out)
|
||||
this.ScaleTo(0.8f, length * 2, Easing.OutQuad);
|
||||
|
||||
this.MoveToY(-gravity_travel_height, length, Easing.Out)
|
||||
.Then()
|
||||
.MoveToY(gravity_travel_height * 2, gravity_time * 2, Easing.In);
|
||||
.MoveToY(gravity_travel_height * 2, length * 2, Easing.In);
|
||||
|
||||
this.FadeOut(800);
|
||||
break;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
@@ -31,7 +32,16 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
Caption = RulesetSettingsStrings.TouchControlScheme,
|
||||
Current = config.GetBindable<TaikoTouchControlScheme>(TaikoRulesetSetting.TouchControlScheme)
|
||||
}),
|
||||
new SettingsItemV2(new FormCheckBox
|
||||
{
|
||||
Caption = RulesetSettingsStrings.RateAdjustedHitAnimation,
|
||||
HintText = RulesetSettingsStrings.RateAdjustedHitAnimationTooltip,
|
||||
Current = config.GetBindable<bool>(TaikoRulesetSetting.RateAdjustedHitAnimation)
|
||||
})
|
||||
{
|
||||
ApplyClassicDefault = c => ((IHasCurrentValue<bool>)c).Current.Value = false,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ using NUnit.Framework;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Edit.Checks;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@@ -18,7 +20,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
[TestFixture]
|
||||
public class CheckFewHitsoundsTest
|
||||
{
|
||||
private CheckFewHitsounds check = null!;
|
||||
private CheckOsuFewHitsounds check = null!;
|
||||
|
||||
private List<HitSampleInfo> notHitsounded = null!;
|
||||
private List<HitSampleInfo> hitsounded = null!;
|
||||
@@ -26,7 +28,7 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckFewHitsounds();
|
||||
check = new CheckOsuFewHitsounds();
|
||||
notHitsounded = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
|
||||
hitsounded = new List<HitSampleInfo>
|
||||
{
|
||||
@@ -82,6 +84,43 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRarelyHitsoundedLongWallTimeMostlyBreak()
|
||||
{
|
||||
var hitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Samples = hitsounded },
|
||||
new HitCircle { StartTime = 1000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 2000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 3000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 4000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 5000, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 10000, Samples = hitsounded },
|
||||
};
|
||||
|
||||
// 10s since last hitsound, but 6s overlap a break → 4s without hitsounds (below warning threshold).
|
||||
assertOk(hitObjects, new BreakPeriod(4000, 10000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRarelyHitsoundedLongWallTimeMostlySpinner()
|
||||
{
|
||||
var hitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, Samples = hitsounded },
|
||||
new HitCircle { StartTime = 200, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 400, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 600, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 800, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 1000, Samples = notHitsounded },
|
||||
new Spinner { StartTime = 1200, EndTime = 21200, Samples = notHitsounded },
|
||||
new HitCircle { StartTime = 21400, Samples = hitsounded },
|
||||
};
|
||||
|
||||
// 21.4s since last hitsound, but 20s overlap a spinner → 1.4s without hitsounds.
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLightlyHitsounded()
|
||||
{
|
||||
@@ -194,9 +233,9 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitObjects)
|
||||
private void assertOk(List<HitObject> hitObjects, params BreakPeriod[] breaks)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
|
||||
Assert.That(check.Run(getContext(hitObjects, breaks)), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertLongPeriodProblem(List<HitObject> hitObjects, int count = 1)
|
||||
@@ -231,10 +270,13 @@ namespace osu.Game.Tests.Editing.Checks
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, params BreakPeriod[] breaks)
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
|
||||
|
||||
foreach (var b in breaks)
|
||||
beatmap.Breaks.Add(b);
|
||||
|
||||
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,15 @@ namespace osu.Game.Tests.Extensions
|
||||
[TestCase(0.4, true, 2, ExpectedResult = "40%")]
|
||||
[TestCase(1e-6, false, 6, ExpectedResult = "0,000001")]
|
||||
[TestCase(0.48333, true, 4, ExpectedResult = "48,33%")]
|
||||
public string TestCultureSensitivity(double input, bool percent, int decimalDigits)
|
||||
public string TestCultureSensitivityDecimalPoint(double input, bool percent, int decimalDigits)
|
||||
{
|
||||
return input.ToStandardFormattedString(decimalDigits, percent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[SetCulture("sv-SE")]
|
||||
[TestCase(-1e-6, false, 6, ExpectedResult = "−0,000001")]
|
||||
public string TestCultureSensitivityNegativeSign(double input, bool percent, int decimalDigits)
|
||||
{
|
||||
return input.ToStandardFormattedString(decimalDigits, percent);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
@@ -33,6 +35,17 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(invalid, Is.EquivalentTo(new[] { mod.Object }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModIsNotCompatibleWithItselfEvenIfSettingsDiffer()
|
||||
{
|
||||
var mod1 = new Mock<CustomMod3>();
|
||||
var mod2 = new Mock<CustomMod3>();
|
||||
mod2.Setup(m => m.Setting).Returns(new BindableBool(true));
|
||||
|
||||
Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }, out var invalid), Is.False);
|
||||
Assert.That(invalid, Is.EquivalentTo(new[] { mod2.Object }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModIsCompatibleByItself()
|
||||
{
|
||||
@@ -397,6 +410,12 @@ namespace osu.Game.Tests.Mods
|
||||
{
|
||||
}
|
||||
|
||||
public abstract class CustomMod3 : Mod, IModCompatibilitySpecification
|
||||
{
|
||||
[SettingSource("Setting")]
|
||||
public virtual BindableBool Setting { get; } = new BindableBool();
|
||||
}
|
||||
|
||||
private class InvalidMultiplayerMod : Mod
|
||||
{
|
||||
public override string Name => string.Empty;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{"beatmapset_id":2529695,"difficulty_rating":7.61701,"id":5590715,"mode":"mania","status":"ranked","total_length":396,"user_id":13371424,"version":"[4K] Memoriae Effervescentes","accuracy":7.2,"ar":5,"bpm":220,"convert":false,"count_circles":2097,"count_sliders":3556,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8.2,"hit_length":395,"is_scoreable":true,"last_updated":"2026-04-21T08:35:33Z","mode_int":3,"passcount":1485,"playcount":4006,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/5590715","checksum":"5b43b30845408f6bf0cc02d19fa475a4","beatmapset":{"anime_cover":false,"artist":"Laur","artist_unicode":"Laur","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/cover.jpg?1776760548","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/cover@2x.jpg?1776760548","card":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/card.jpg?1776760548","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/card@2x.jpg?1776760548","list":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/list.jpg?1776760548","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/list@2x.jpg?1776760548","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/slimcover.jpg?1776760548","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2529695\/covers\/slimcover@2x.jpg?1776760548"},"creator":"Ainer","favourite_count":55,"genre_id":10,"hype":null,"id":2529695,"language_id":5,"nsfw":false,"offset":0,"play_count":4102,"preview_url":"https:\/\/b.ppy.sh\/preview\/2529695.mp3","source":"osu!mania 7K World Cup 2026","spotlight":false,"status":"ranked","title":"SEV-26","title_unicode":"SEV-26","track_id":11695,"user_id":13371424,"video":false,"bpm":180,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-04-21T08:35:32Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/2191856","nominations_summary":{"current":2,"eligible_main_rulesets":["mania"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-28T09:22:15Z","rating":8.96296,"storyboard":false,"submitted_date":"2026-03-28T00:58:23Z","tags":"featured artist fa mappers' guild mg mpg osu! original electronic instrumental neurofunk hardcore psytrance speedcore tearout dubstep orchestral artcore sev26 sylvatic encephalitis virus grand finals grandfinals gf mwc 2026 world cup mwc2026 mwc7k2026 tb tiebreaker alicia antipole symmatrix- sardines bruh_shen tehfire polytetral hourius naraicat alexdunk lowgraphics sakura006","availability":{"download_disabled":false,"more_information":null},"ratings":[0,3,0,0,0,0,0,0,0,1,23]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,0,0,0,0,0,9,0,9,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,18,18,0,0,0,0,0,0,0,0,9,9,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,18,9,9,18,0,18,9,0,0,0,0,0],"exit":[0,36,36,45,63,9,9,54,18,63,72,18,9,0,9,9,0,18,27,18,18,45,0,9,9,9,9,27,9,9,9,9,9,0,0,9,0,9,0,0,0,0,0,0,18,0,0,0,0,9,0,9,0,0,0,0,18,18,9,9,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,9,9,0,0,9,18,0,9,0,0,0,0,0,0,0]},"max_combo":9163,"owners":[{"id":13371424,"username":"Ainer"},{"id":17258072,"username":"Alicia"}]},
|
||||
{"beatmapset_id":2518293,"difficulty_rating":3.61767,"id":5602450,"mode":"mania","status":"ranked","total_length":282,"user_id":8425052,"version":"[4K] Innocence","accuracy":8,"ar":5,"bpm":140,"convert":false,"count_circles":3618,"count_sliders":0,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8,"hit_length":281,"is_scoreable":true,"last_updated":"2026-04-13T00:00:32Z","mode_int":3,"passcount":369,"playcount":1963,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/5602450","checksum":"51319d68b9373eb98807ea5a2187d803","beatmapset":{"anime_cover":false,"artist":"rejection","artist_unicode":"rejection","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/cover.jpg?1776038446","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/cover@2x.jpg?1776038446","card":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/card.jpg?1776038446","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/card@2x.jpg?1776038446","list":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/list.jpg?1776038446","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/list@2x.jpg?1776038446","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/slimcover.jpg?1776038446","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2518293\/covers\/slimcover@2x.jpg?1776038446"},"creator":"Stella-","favourite_count":48,"genre_id":10,"hype":null,"id":2518293,"language_id":3,"nsfw":false,"offset":0,"play_count":2031,"preview_url":"https:\/\/b.ppy.sh\/preview\/2518293.mp3","source":"","spotlight":false,"status":"ranked","title":"White Canvas (feat. Aitsuki Nakuru)","title_unicode":"White Canvas (feat. \u85cd\u6708\u306a\u304f\u308b)","track_id":5057,"user_id":8425052,"video":false,"bpm":140,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-04-13T00:00:32Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/2185354","nominations_summary":{"current":2,"eligible_main_rulesets":["mania"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-21T23:42:39Z","rating":8.66667,"storyboard":false,"submitted_date":"2026-03-06T10:45:47Z","tags":"fa featured artist japanese pop jpop j-pop electronic encore emotional -emotional vocal pop 02- 2 megarex mrx-074 mrx074 female vocals vocalist m3-2020\u79cb fall m3-46 muse dash the future","availability":{"download_disabled":false,"more_information":null},"ratings":[0,0,0,0,0,1,0,1,0,0,4]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,0,9,0,9,0,0,9,0,0,0,0,0,0,0,0,0,0,0,9,0,9,0,0,0,9,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"exit":[0,0,18,18,9,63,9,27,18,9,18,9,18,9,27,9,36,0,0,9,9,0,9,0,18,27,0,0,0,9,9,0,0,0,0,9,0,9,0,9,9,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,9,0,0,0,9,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},"max_combo":3618,"owners":[{"id":8425052,"username":"Stella-"}]},
|
||||
{"beatmapset_id":1665948,"difficulty_rating":4.87529,"id":3871759,"mode":"mania","status":"ranked","total_length":379,"user_id":17272017,"version":"[4K] Time Freeze Illusion","accuracy":8.5,"ar":5,"bpm":200,"convert":false,"count_circles":5264,"count_sliders":629,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8,"hit_length":377,"is_scoreable":true,"last_updated":"2026-04-13T09:52:53Z","mode_int":3,"passcount":308,"playcount":1738,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/3871759","checksum":"34b98351e32ca3db24ea9fc0055a923b","beatmapset":{"anime_cover":true,"artist":"Release Hallucination","artist_unicode":"Release Hallucination","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/cover.jpg?1776073987","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/cover@2x.jpg?1776073987","card":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/card.jpg?1776073987","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/card@2x.jpg?1776073987","list":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/list.jpg?1776073987","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/list@2x.jpg?1776073987","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/slimcover.jpg?1776073987","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/1665948\/covers\/slimcover@2x.jpg?1776073987"},"creator":"Lazurent","favourite_count":51,"genre_id":11,"hype":null,"id":1665948,"language_id":3,"nsfw":false,"offset":0,"play_count":4811,"preview_url":"https:\/\/b.ppy.sh\/preview\/1665948.mp3","source":"","spotlight":false,"status":"ranked","title":"Chronostasis","title_unicode":"Chronostasis","track_id":4954,"user_id":17272017,"video":false,"bpm":200,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-04-13T09:52:53Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/1494910","nominations_summary":{"current":2,"eligible_main_rulesets":["mania"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-20T11:25:45Z","rating":8.33333,"storyboard":false,"submitted_date":"2022-01-03T14:00:15Z","tags":"m3-38 symphonic progressive metal marathon emi gothic kaorin kaoru hirato japanese \u30af\u30ed\u30ce\u30b9\u30bf\u30b7\u30b9 fa featured artist mg mpg mappers' guild l43yrnt","availability":{"download_disabled":false,"more_information":null},"ratings":[0,1,0,0,0,0,0,0,0,1,4]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,0,9,9,63,18,45,18,45,27,0,0,0,0,0,9,0,0,0,0,9,18,0,0,0,9,9,0,0,0,0,0,18,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0],"exit":[0,0,18,63,63,135,261,216,126,81,81,36,18,27,45,27,27,36,9,9,0,18,18,81,27,18,9,18,18,9,0,0,0,36,45,36,0,0,0,0,0,0,9,9,0,18,0,0,9,0,9,0,9,0,0,9,0,9,9,0,0,9,0,0,0,9,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,0,0,9,0,0,0,0,9]},"max_combo":6974,"owners":[{"id":17272017,"username":"Lazurent"}]},
|
||||
{"beatmapset_id":2535368,"difficulty_rating":5.82018,"id":5606802,"mode":"mania","status":"ranked","total_length":296,"user_id":10085090,"version":"[4K] Desperate Soul","accuracy":8.5,"ar":5,"bpm":240,"convert":false,"count_circles":6288,"count_sliders":0,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8,"hit_length":290,"is_scoreable":true,"last_updated":"2026-04-16T18:03:43Z","mode_int":3,"passcount":208,"playcount":1738,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/5606802","checksum":"55451c2f43654e4ec7b9bb156b93148e","beatmapset":{"anime_cover":false,"artist":"Imperial Circus Dead Decadence","artist_unicode":"Imperial Circus Dead Decadence","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/cover.jpg?1776362637","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/cover@2x.jpg?1776362637","card":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/card.jpg?1776362637","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/card@2x.jpg?1776362637","list":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/list.jpg?1776362637","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/list@2x.jpg?1776362637","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/slimcover.jpg?1776362637","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2535368\/covers\/slimcover@2x.jpg?1776362637"},"creator":"Carpihat","favourite_count":48,"genre_id":11,"hype":null,"id":2535368,"language_id":3,"nsfw":false,"offset":0,"play_count":1961,"preview_url":"https:\/\/b.ppy.sh\/preview\/2535368.mp3","source":"","spotlight":false,"status":"ranked","title":"Jashin no Konrei, Gi wa Ai to Shiru.","title_unicode":"\u90aa\u795e\u306e\u5a5a\u793c\u3001\u5100\u306f\u611b\u3068\u77e5\u308b\u3002","track_id":862,"user_id":10085090,"video":false,"bpm":240,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-04-16T18:03:42Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/2195210","nominations_summary":{"current":2,"eligible_main_rulesets":["mania"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-18T18:05:56Z","rating":10,"storyboard":false,"submitted_date":"2026-04-07T09:36:37Z","tags":"cthulhu wedding blackened melodic symphonic death black metal melodeath icdd \u72c2\u304a\u3057\u304f\u54b2\u3044\u305f\u51c4\u60e8\u306a\u9ab8\u306f\u594f\u3067\u3001\u611b\u304a\u3057\u304f\u88c2\u3044\u305f\u5c11\u5973\u306f\u8056\u9910\u306e\u8a5e\u3092\u8b33\u3046\u3002 kurooshiku saita seisan na mukuro wa kanaderu itooshiku shoujo seisen no kotoba wo utau fa featured artist japanese mpg mg mappers' guild","availability":{"download_disabled":false,"more_information":null},"ratings":[0,0,0,0,0,0,0,0,0,0,1]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,9,27,27,27,9,9,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"exit":[0,0,0,27,63,117,108,36,63,36,9,45,9,27,9,0,9,9,0,36,9,36,0,9,0,9,9,0,0,0,27,9,9,0,0,0,0,0,9,0,9,27,9,0,9,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]},"max_combo":6288,"owners":[{"id":10085090,"username":"Carpihat"}]},
|
||||
{"beatmapset_id":2348607,"difficulty_rating":5.57212,"id":5140134,"mode":"mania","status":"ranked","total_length":263,"user_id":23384715,"version":"[4K] Enraged Apparition","accuracy":8,"ar":10,"bpm":250,"convert":false,"count_circles":4682,"count_sliders":120,"count_spinners":0,"cs":4,"deleted_at":null,"drain":8,"hit_length":251,"is_scoreable":true,"last_updated":"2026-03-26T20:54:51Z","mode_int":3,"passcount":404,"playcount":2674,"ranked":1,"url":"https:\/\/osu.ppy.sh\/beatmaps\/5140134","checksum":"a2dd0c1b8aa0d3396b47621b758c6adf","beatmapset":{"anime_cover":true,"artist":"Imperial Circus Dead Decadence","artist_unicode":"Imperial Circus Dead Decadence","covers":{"cover":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/cover.jpg?1774558514","cover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/cover@2x.jpg?1774558514","card":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/card.jpg?1774558514","card@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/card@2x.jpg?1774558514","list":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/list.jpg?1774558514","list@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/list@2x.jpg?1774558514","slimcover":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/slimcover.jpg?1774558514","slimcover@2x":"https:\/\/assets.ppy.sh\/beatmaps\/2348607\/covers\/slimcover@2x.jpg?1774558514"},"creator":"Sayuka","favourite_count":435,"genre_id":11,"hype":null,"id":2348607,"language_id":3,"nsfw":false,"offset":0,"play_count":118018,"preview_url":"https:\/\/b.ppy.sh\/preview\/2348607.mp3","source":"","spotlight":false,"status":"ranked","title":"Shinbatsu o Tadori Kyoukotsu ni Itaru","title_unicode":"\u795e\u7f70\u3092\u8fbf\u308a\u72c2\u9aa8\u306b\u81f3\u308b","track_id":8223,"user_id":11322604,"video":false,"bpm":250,"can_be_hyped":false,"deleted_at":null,"discussion_enabled":true,"discussion_locked":false,"is_scoreable":true,"last_updated":"2026-03-26T20:54:46Z","legacy_thread_url":"https:\/\/osu.ppy.sh\/community\/forums\/topics\/2061550","nominations_summary":{"current":3,"eligible_main_rulesets":["osu"],"required_meta":{"main_ruleset":2,"non_main_ruleset":1}},"ranked":1,"ranked_date":"2026-04-12T17:43:03Z","rating":9.11539,"storyboard":false,"submitted_date":"2025-04-02T11:30:44Z","tags":"maaadbot rhythmnoodles ciiyus icdd sinyus20 oomf chan ciyus miapah fort danilmaz1 mekadon -aly arthro roupus julie maiev worthlessnut9 frawog mithew m1ts shoyeu take amats sprixx chocomilk \u9ec4\u6cc9\u3088\u308a\u8074\u3053\u3086\u3001\u7687\u56fd\u306e\u71c8\u3068\u7114\u306e\u5c11\u5973\u3002 japanese male vocals symphonic melodic black death metal doujin hull kim rib:y(uhki) rib yuhki shuhei js jumpstream hs handstream stream stamina jack chordjack rice fa featured artist","availability":{"download_disabled":false,"more_information":null},"ratings":[0,7,0,0,1,0,1,1,3,10,81]},"current_user_playcount":0,"failtimes":{"fail":[0,0,0,18,72,54,36,36,18,9,9,9,0,0,0,9,63,18,0,0,0,0,0,0,9,0,0,0,9,9,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0],"exit":[0,0,9,243,90,198,216,126,18,81,63,36,9,0,18,135,45,27,27,18,18,9,0,0,0,0,9,9,18,27,36,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,27,18,9,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,0,27,0,0,0,0,0,0,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,9,9,0]},"max_combo":5113,"owners":[{"id":16018038,"username":"frawog"},{"id":23384715,"username":"Worthlessnut9"}]}
|
||||
|
||||
]
|
||||
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
@@ -26,17 +25,15 @@ namespace osu.Game.Tests.Skins
|
||||
public partial class TestSceneBeatmapSkinLookupDisables : OsuTestScene
|
||||
{
|
||||
private UserSkinSource userSource;
|
||||
private BeatmapSkinProvidingContainer beatmapSkinProvider;
|
||||
private BeatmapSkinSource beatmapSource;
|
||||
private SkinRequester requester;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Add(new SkinProvidingContainer(userSource = new UserSkinSource())
|
||||
.WithChild(new BeatmapSkinProvidingContainer(beatmapSource = new BeatmapSkinSource())
|
||||
.WithChild(beatmapSkinProvider = new BeatmapSkinProvidingContainer(beatmapSource = new BeatmapSkinSource())
|
||||
.WithChild(requester = new SkinRequester())));
|
||||
});
|
||||
|
||||
@@ -44,7 +41,7 @@ namespace osu.Game.Tests.Skins
|
||||
[TestCase(true)]
|
||||
public void TestDrawableLookup(bool allowBeatmapLookups)
|
||||
{
|
||||
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => config.SetValue(OsuSetting.BeatmapSkins, allowBeatmapLookups));
|
||||
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => beatmapSkinProvider.BeatmapSkins.Value = allowBeatmapLookups);
|
||||
|
||||
string expected = allowBeatmapLookups ? "beatmap" : "user";
|
||||
|
||||
@@ -55,7 +52,7 @@ namespace osu.Game.Tests.Skins
|
||||
[TestCase(true)]
|
||||
public void TestProviderLookup(bool allowBeatmapLookups)
|
||||
{
|
||||
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => config.SetValue(OsuSetting.BeatmapSkins, allowBeatmapLookups));
|
||||
AddStep($"Set beatmap skin enabled to {allowBeatmapLookups}", () => beatmapSkinProvider.BeatmapSkins.Value = allowBeatmapLookups);
|
||||
|
||||
ISkin expected() => allowBeatmapLookups ? beatmapSource : userSource;
|
||||
|
||||
|
||||
@@ -100,8 +100,10 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[Test]
|
||||
public void TestPlacementOfConcurrentObjectWithDuration()
|
||||
{
|
||||
AddStep("seek to timing point", () => EditorClock.Seek(2170));
|
||||
AddStep("add hit circle", () => EditorBeatmap.Add(createHitCircle(2170, Vector2.Zero)));
|
||||
const double spinner_start_time = 2170;
|
||||
const double spinner_end_seek_time = 2500;
|
||||
|
||||
AddStep("seek to timing point", () => EditorClock.Seek(spinner_start_time));
|
||||
|
||||
AddStep("choose spinner placement tool", () =>
|
||||
{
|
||||
@@ -116,10 +118,15 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
AddStep("end placing spinner", () =>
|
||||
{
|
||||
EditorClock.Seek(2500);
|
||||
EditorClock.Seek(spinner_end_seek_time);
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddStep("add hit circle mid-spinner", () =>
|
||||
{
|
||||
EditorBeatmap.Add(createHitCircle((spinner_start_time + spinner_end_seek_time) / 2, Vector2.Zero));
|
||||
});
|
||||
|
||||
AddAssert("two timeline blueprints present", () => Editor.ChildrenOfType<TimelineHitObjectBlueprint>().Count() == 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -12,10 +13,11 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
@@ -51,6 +53,90 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacementReplacesObjectAtSameStartTime()
|
||||
{
|
||||
HitCircle existing = null!;
|
||||
var existingPosition = new Vector2(128, 160);
|
||||
var replacementPosition = new Vector2(400, 280);
|
||||
Playfield playfield = null!;
|
||||
|
||||
AddStep("add existing circle", () =>
|
||||
{
|
||||
EditorBeatmap.Add(existing = new HitCircle
|
||||
{
|
||||
StartTime = 500,
|
||||
Position = existingPosition,
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("seek to same time", () => EditorClock.Seek(500));
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("grab playfield", () => playfield = this.ChildrenOfType<Playfield>().Single());
|
||||
AddStep("move mouse to replacement coordinates", () => InputManager.MoveMouseTo(playfield.GamefieldToScreenSpace(replacementPosition)));
|
||||
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("only one hit object", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
|
||||
AddAssert("original instance removed from beatmap", () => EditorBeatmap.HitObjects.Single(), () => Is.Not.SameAs(existing));
|
||||
AddAssert("start time unchanged", () => Precision.AlmostEquals(EditorBeatmap.HitObjects.Single().StartTime, 500));
|
||||
AddAssert("circle at new coordinates", () =>
|
||||
{
|
||||
var circle = (HitCircle)EditorBeatmap.HitObjects.Single();
|
||||
return circle != null
|
||||
&& Precision.AlmostEquals(circle.Position.X, replacementPosition.X)
|
||||
&& Precision.AlmostEquals(circle.Position.Y, replacementPosition.Y);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlacementOnSliderBodyDoesNotRemoveSlider()
|
||||
{
|
||||
Slider originalSlider = null!;
|
||||
|
||||
AddStep("add slider", () =>
|
||||
{
|
||||
EditorBeatmap.Add(originalSlider = new Slider
|
||||
{
|
||||
StartTime = 0,
|
||||
Position = new Vector2(256, 192),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(Vector2.Zero),
|
||||
new PathControlPoint(new Vector2(256, 0)),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
AddUntilStep("slider duration resolved", () => originalSlider.EndTime > originalSlider.StartTime + 1);
|
||||
|
||||
double midTime = 0;
|
||||
double endTime = 0;
|
||||
|
||||
AddStep("capture slider times", () =>
|
||||
{
|
||||
midTime = originalSlider.StartTime + originalSlider.Duration / 2;
|
||||
endTime = originalSlider.EndTime;
|
||||
});
|
||||
|
||||
Playfield playfield = null!;
|
||||
|
||||
AddStep("seek to slider mid", () => EditorClock.Seek(midTime));
|
||||
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
|
||||
AddStep("grab playfield", () => playfield = this.ChildrenOfType<Playfield>().Single());
|
||||
AddStep("move mouse for mid placement", () => InputManager.MoveMouseTo(playfield.GamefieldToScreenSpace(new Vector2(300, 200))));
|
||||
AddStep("place circle at slider mid time", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("slider preserved after mid placement", () => EditorBeatmap.HitObjects.Contains(originalSlider));
|
||||
AddAssert("one circle after mid placement", () => EditorBeatmap.HitObjects.Count(h => h is HitCircle), () => Is.EqualTo(1));
|
||||
|
||||
AddStep("seek to slider end", () => EditorClock.Seek(endTime));
|
||||
AddStep("place circle at slider end time", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddAssert("slider still preserved", () => EditorBeatmap.HitObjects.Contains(originalSlider));
|
||||
AddAssert("three hit objects total", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(3));
|
||||
AddAssert("two circles placed", () => EditorBeatmap.HitObjects.Count(h => h is HitCircle), () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTimingLost()
|
||||
{
|
||||
|
||||
@@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddRepeatStep("Add judgement", () => applyOneJudgement(HitResult.LargeTickHit), 2);
|
||||
AddAssert("Check value added whilst hidden", () => hiddenCount() == 2);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay());
|
||||
|
||||
AddStep("Set max judgement to hide itself", () => counterDisplay.ShowMaxJudgement.Value = false);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().First().Alpha == 0);
|
||||
}
|
||||
@@ -165,7 +165,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestNoDuplicates()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay());
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
AddAssert("Check no duplicates",
|
||||
() => counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().Count(),
|
||||
() => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().Select(c => c.Result.DisplayName).Distinct().Count()));
|
||||
@@ -176,11 +176,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestArgonJudgementCounterDisplay());
|
||||
|
||||
AddStep("Show basic judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.Simple);
|
||||
AddStep("Show basic judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Simple);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Check only basic", () => counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().Last().Alpha == 0);
|
||||
AddStep("Show normal judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.Normal);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All);
|
||||
AddStep("Show normal judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.Normal);
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
AddWaitStep("wait some", 2);
|
||||
AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType<ArgonJudgementCounter>().Last().Alpha == 1);
|
||||
AddToggleStep("toggle wireframe display", t => counterDisplay.WireframeOpacity.Value = t ? 0.3f : 0);
|
||||
|
||||
@@ -73,6 +73,15 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
iteration++;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplayModes()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
|
||||
foreach (JudgementCounterDisplay.DisplayMode mode in Enum.GetValues<JudgementCounterDisplay.DisplayMode>())
|
||||
AddStep($"Change mode to {mode}", () => counterDisplay.Mode.Value = mode);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAddJudgementsToCounters()
|
||||
{
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
allowResponseCallback.Wait(10000);
|
||||
allowResponseCallback.Reset();
|
||||
Schedule(() => d?.Invoke("Incorrect password", new InvalidPasswordException()));
|
||||
Schedule(() => d("Incorrect password", new InvalidPasswordException()));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -330,6 +330,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModSelectOverlayNonDefaultSettings()
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
RequiredMods =
|
||||
[
|
||||
new APIMod(new OsuModSuddenDeath { FailOnSliderTail = { Value = true } }),
|
||||
],
|
||||
AllowedMods = [],
|
||||
Freestyle = true
|
||||
}
|
||||
];
|
||||
});
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
ClickButtonWhenEnabled<UserModSelectButton>();
|
||||
AddAssert("sudden death not visible", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().ChildrenOfType<ModPanel>().Single(m => m.Mod is ModSuddenDeath).Visible == false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeSettingsButtonVisibleForHost()
|
||||
{
|
||||
|
||||
@@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4);
|
||||
|
||||
AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = rooms.First().Name });
|
||||
AddStep("filter one room", () => container.Filter.Value = new LoungeFilterCriteria { SearchString = rooms.First().Name });
|
||||
|
||||
AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1);
|
||||
|
||||
@@ -160,13 +160,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, new CatchRuleset().RulesetInfo)));
|
||||
|
||||
// Todo: What even is this case...?
|
||||
AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria());
|
||||
AddStep("set empty filter criteria", () => container.Filter.Value = new LoungeFilterCriteria());
|
||||
AddUntilStep("5 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 5);
|
||||
|
||||
AddStep("filter osu! rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo });
|
||||
AddStep("filter osu! rooms", () => container.Filter.Value = new LoungeFilterCriteria { Ruleset = new OsuRuleset().RulesetInfo });
|
||||
AddUntilStep("2 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2);
|
||||
|
||||
AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo });
|
||||
AddStep("filter catch rooms", () => container.Filter.Value = new LoungeFilterCriteria { Ruleset = new CatchRuleset().RulesetInfo });
|
||||
AddUntilStep("3 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 3);
|
||||
}
|
||||
|
||||
@@ -183,11 +183,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("both rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2);
|
||||
|
||||
AddStep("filter public rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Public });
|
||||
AddStep("filter public rooms", () => container.Filter.Value = new LoungeFilterCriteria { Permissions = RoomPermissionsFilter.Public });
|
||||
|
||||
AddUntilStep("private room hidden", () => container.DrawableRooms.All(r => !r.Room.HasPassword));
|
||||
|
||||
AddStep("filter private rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Private });
|
||||
AddStep("filter private rooms", () => container.Filter.Value = new LoungeFilterCriteria { Permissions = RoomPermissionsFilter.Private });
|
||||
|
||||
AddUntilStep("public room hidden", () => container.DrawableRooms.All(r => r.Room.HasPassword));
|
||||
}
|
||||
|
||||
@@ -114,6 +114,27 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
&& songSelect.Beatmap.Value is DummyWorkingBeatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditorRestoresModeOnReload()
|
||||
{
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
|
||||
makeMetadataChange(commit: false);
|
||||
|
||||
Editor editor1 = null!;
|
||||
|
||||
AddStep("store current editor", () => editor1 = getEditor());
|
||||
|
||||
AddStep("reload", () => getEditor().SwitchToDifficulty(getEditorBeatmap().BeatmapInfo));
|
||||
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
|
||||
AddStep("confirm", () => InputManager.Key(Key.Number1));
|
||||
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
AddAssert("editor is new instance", () => getEditor() != editor1);
|
||||
AddAssert("mode is still song setup", () => getEditor().Mode.Value == EditorScreenMode.SongSetup);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave()
|
||||
{
|
||||
|
||||
@@ -253,6 +253,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
InputManager.MoveMouseTo(btn);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddStep("Set reason to other", () =>
|
||||
{
|
||||
var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single();
|
||||
reason.Current.Value = CommentReportReason.Other;
|
||||
});
|
||||
AddStep("Try to report", () =>
|
||||
{
|
||||
var btn = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<RoundedButton>().Single();
|
||||
@@ -261,12 +266,10 @@ namespace osu.Game.Tests.Visual.Online
|
||||
});
|
||||
AddWaitStep("Wait", 3);
|
||||
AddAssert("Nothing happened", () => this.ChildrenOfType<ReportCommentPopover>().Any());
|
||||
AddStep("Set report data", () =>
|
||||
AddStep("Add comment", () =>
|
||||
{
|
||||
var field = this.ChildrenOfType<ReportCommentPopover>().Single().ChildrenOfType<OsuTextBox>().First();
|
||||
field.Current.Value = report_text;
|
||||
var reason = this.ChildrenOfType<OsuEnumDropdown<CommentReportReason>>().Single();
|
||||
reason.Current.Value = CommentReportReason.Other;
|
||||
});
|
||||
AddStep("Try to report", () =>
|
||||
{
|
||||
|
||||
@@ -56,6 +56,17 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Username = @"flyte",
|
||||
Id = 3103765,
|
||||
Rank = new APIUser.GlobalRank
|
||||
{
|
||||
Rank = null,
|
||||
},
|
||||
Team = new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
},
|
||||
CountryCode = CountryCode.JP,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
|
||||
WasRecentlyOnline = true
|
||||
@@ -64,6 +75,17 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Rank = new APIUser.GlobalRank
|
||||
{
|
||||
Rank = 9999999,
|
||||
},
|
||||
Team = new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
},
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
IsSupporter = true,
|
||||
@@ -72,7 +94,18 @@ namespace osu.Game.Tests.Visual.Online
|
||||
new OnlineUserListPanel(new APIUser
|
||||
{
|
||||
Username = @"flyte",
|
||||
Rank = new APIUser.GlobalRank
|
||||
{
|
||||
Rank = null,
|
||||
},
|
||||
Id = 3103765,
|
||||
Team = new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
},
|
||||
CountryCode = CountryCode.JP,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
|
||||
WasRecentlyOnline = true
|
||||
@@ -81,6 +114,17 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Rank = new APIUser.GlobalRank
|
||||
{
|
||||
Rank = 9999999,
|
||||
},
|
||||
Team = new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
},
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
LastVisit = DateTimeOffset.Now
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// 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.Net.Http;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Overlays.Chat;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public partial class TestSceneReportPopover : OsuTestScene
|
||||
{
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
private ReportPopoverContainer popover = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUp()
|
||||
{
|
||||
AddStep("create popover", () =>
|
||||
{
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = popover = new ReportPopoverContainer(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSuccess()
|
||||
{
|
||||
ChatReportRequest pendingRequest = null!;
|
||||
|
||||
AddStep("setup request handling", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest += request =>
|
||||
{
|
||||
if (request is ChatReportRequest chatReportRequest)
|
||||
{
|
||||
pendingRequest = chatReportRequest;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("show popover", () => popover.ShowPopover());
|
||||
AddStep("input reason", () => this.ChildrenOfType<OsuTextBox>().First().Text = "reason");
|
||||
AddStep("send report", () => this.ChildrenOfType<Button>().First().TriggerClick());
|
||||
AddUntilStep("wait for loading layer to hide", () => this.ChildrenOfType<LoadingLayer>().First().IsPresent, () => Is.True);
|
||||
AddWaitStep("wait some", 3);
|
||||
AddStep("complete request", () => pendingRequest.TriggerSuccess());
|
||||
AddUntilStep("wait for loading layer to hide", () => this.ChildrenOfType<LoadingLayer>().First().IsPresent, () => Is.False);
|
||||
AddAssert("ensure form is not present", () => this.ChildrenOfType<ReverseChildIDFillFlowContainer<Drawable>>().First().IsPresent, () => Is.False);
|
||||
AddAssert("ensure confirmation is present", () => this.ChildrenOfType<ReportPopover<ChatReportReason>.ReportConfirmation>().First().IsPresent, () => Is.True);
|
||||
AddUntilStep("wait for popover to hide", () => this.ChildrenOfType<ReportPopoverContainer.TestReportPopover>().First().IsPresent, () => Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFailure()
|
||||
{
|
||||
ChatReportRequest pendingRequest = null!;
|
||||
|
||||
AddStep("setup request handling", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest += request =>
|
||||
{
|
||||
if (request is ChatReportRequest chatReportRequest)
|
||||
{
|
||||
pendingRequest = chatReportRequest;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("show popover", () => popover.ShowPopover());
|
||||
AddStep("input reason", () => this.ChildrenOfType<OsuTextBox>().First().Text = "reason");
|
||||
AddStep("send report", () => this.ChildrenOfType<Button>().First().TriggerClick());
|
||||
AddUntilStep("wait for loading layer to hide", () => this.ChildrenOfType<LoadingLayer>().First().IsPresent, () => Is.True);
|
||||
AddWaitStep("wait some", 3);
|
||||
AddStep("fail request", () => pendingRequest.TriggerFailure(new APIException("test error", new HttpRequestException("test error"))));
|
||||
AddUntilStep("wait for loading layer to hide", () => this.ChildrenOfType<LoadingLayer>().First().IsPresent, () => Is.False);
|
||||
AddAssert("ensure form is present", () => this.ChildrenOfType<ReverseChildIDFillFlowContainer<Drawable>>().First().IsPresent, () => Is.True);
|
||||
AddAssert("ensure error is present", () => this.ChildrenOfType<ErrorTextFlowContainer>().First().IsPresent, () => Is.True);
|
||||
AddAssert("ensure confirmation is not present", () => this.ChildrenOfType<ReportPopover<ChatReportReason>.ReportConfirmation>().First().IsPresent, () => Is.False);
|
||||
}
|
||||
|
||||
protected partial class ReportPopoverContainer : Drawable, IHasPopover
|
||||
{
|
||||
public Popover GetPopover() => new TestReportPopover("test");
|
||||
|
||||
public partial class TestReportPopover : ReportPopover<ChatReportReason>
|
||||
{
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
public TestReportPopover(string name)
|
||||
: base($"Report {name}?")
|
||||
{
|
||||
}
|
||||
|
||||
protected override APIRequest GetRequest(ChatReportReason reason, string comment) => new ChatReportRequest(1, reason, comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
@@ -18,14 +19,26 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
public abstract partial class RankedPlayTestScene : MultiplayerTestScene
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns 5 sample <see cref="APIBeatmap"/>s.
|
||||
/// Returns 5 sample of the chosen ruleset <see cref="APIBeatmap"/>s.
|
||||
/// </summary>
|
||||
protected static APIBeatmap[] GetSampleBeatmaps()
|
||||
protected static APIBeatmap[] GetSampleBeatmaps(RulesetInfo ruleset)
|
||||
{
|
||||
using var resourceStream = TestResources.OpenResource("Requests/api-beatmaps-rankedplay.json");
|
||||
using var reader = new StreamReader(resourceStream);
|
||||
switch (ruleset.OnlineID)
|
||||
{
|
||||
case 3:
|
||||
{
|
||||
using var resourceStream = TestResources.OpenResource("Requests/api-beatmaps-rankedplay-mania4k.json");
|
||||
using var reader = new StreamReader(resourceStream);
|
||||
return JsonConvert.DeserializeObject<APIBeatmap[]>(reader.ReadToEnd())!;
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<APIBeatmap[]>(reader.ReadToEnd())!;
|
||||
default:
|
||||
{
|
||||
using var resourceStream = TestResources.OpenResource("Requests/api-beatmaps-rankedplay.json");
|
||||
using var reader = new StreamReader(resourceStream);
|
||||
return JsonConvert.DeserializeObject<APIBeatmap[]>(reader.ReadToEnd())!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -33,7 +46,12 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
/// </summary>
|
||||
public class BeatmapRequestHandler
|
||||
{
|
||||
public readonly APIBeatmap[] Beatmaps = GetSampleBeatmaps();
|
||||
public APIBeatmap[] Beatmaps;
|
||||
|
||||
public BeatmapRequestHandler(RulesetInfo ruleset)
|
||||
{
|
||||
Beatmaps = GetSampleBeatmaps(ruleset);
|
||||
}
|
||||
|
||||
public bool HandleRequest(APIRequest request)
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.RankedPlay
|
||||
@@ -26,7 +27,8 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
|
||||
AddUntilStep("screen loaded", () => screen.IsLoaded);
|
||||
|
||||
var requestHandler = new BeatmapRequestHandler();
|
||||
BeatmapRequestHandler requestHandler = null!;
|
||||
AddStep("setup ruleset", () => requestHandler = new BeatmapRequestHandler(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
|
||||
namespace osu.Game.Tests.Visual.RankedPlay
|
||||
@@ -26,7 +27,8 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
|
||||
AddUntilStep("screen loaded", () => screen.IsLoaded);
|
||||
|
||||
var requestHandler = new BeatmapRequestHandler();
|
||||
BeatmapRequestHandler requestHandler = null!;
|
||||
AddStep("setup ruleset", () => requestHandler = new BeatmapRequestHandler(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -9,6 +10,8 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand;
|
||||
using osuTK.Input;
|
||||
@@ -30,8 +33,63 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
|
||||
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
|
||||
AddUntilStep("screen loaded", () => screen.IsLoaded);
|
||||
}
|
||||
|
||||
var requestHandler = new BeatmapRequestHandler();
|
||||
[Test]
|
||||
public void TestOsu()
|
||||
{
|
||||
BeatmapRequestHandler requestHandler = null!;
|
||||
AddStep("setup ruleset", () => requestHandler = new BeatmapRequestHandler(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
|
||||
|
||||
AddStep("set pick state", () => MultiplayerClient.RankedPlayChangeStage(RankedPlayStage.CardPlay, state => state.ActiveUserId = API.LocalUser.Value.OnlineID).WaitSafely());
|
||||
|
||||
AddWaitStep("wait some", 5);
|
||||
|
||||
AddStep("reveal cards", () =>
|
||||
{
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
int i2 = i;
|
||||
MultiplayerClient.RankedPlayRevealCard(hand => hand[i2], new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i2,
|
||||
BeatmapID = requestHandler.Beatmaps[i2].OnlineID
|
||||
}).WaitSafely();
|
||||
}
|
||||
});
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
int i2 = i;
|
||||
AddStep($"click card {i2}", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PlayerHandOfCards.PlayerHandCard>().ElementAt(i2));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
|
||||
AddWaitStep("wait", 3);
|
||||
|
||||
AddStep("click play button", () =>
|
||||
{
|
||||
var button = screen
|
||||
.ChildrenOfType<PlayerHandOfCards.PlayerHandCard>()
|
||||
.First(it => it.Selected)
|
||||
.ChildrenOfType<ShearedButton>()
|
||||
.First();
|
||||
|
||||
InputManager.MoveMouseTo(button);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMania()
|
||||
{
|
||||
BeatmapRequestHandler requestHandler = null!;
|
||||
AddStep("setup ruleset", () => requestHandler = new BeatmapRequestHandler(new ManiaRuleset().RulesetInfo));
|
||||
|
||||
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand;
|
||||
using osuTK;
|
||||
@@ -31,8 +35,6 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
[Cached]
|
||||
private readonly SongPreviewParticleContainer particleContainer;
|
||||
|
||||
private readonly BeatmapRequestHandler requestHandler = new BeatmapRequestHandler();
|
||||
|
||||
public TestSceneRankedPlayCard()
|
||||
{
|
||||
base.Content.AddRange(new Drawable[]
|
||||
@@ -121,6 +123,8 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
[Test]
|
||||
public void TestCardHand()
|
||||
{
|
||||
BeatmapRequestHandler requestHandler = null!;
|
||||
AddStep("setup ruleset", () => requestHandler = new BeatmapRequestHandler(new OsuRuleset().RulesetInfo));
|
||||
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
|
||||
|
||||
AddStep("add cards", () =>
|
||||
@@ -143,13 +147,16 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
});
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; } = null!;
|
||||
|
||||
[Test]
|
||||
public void TestRulesets()
|
||||
{
|
||||
var rulesets = rulesetStore.AvailableRulesets.Where(it => it.OnlineID >= 0);
|
||||
RulesetInfo[] rulesets =
|
||||
[
|
||||
new OsuRuleset().RulesetInfo,
|
||||
new TaikoRuleset().RulesetInfo,
|
||||
new CatchRuleset().RulesetInfo,
|
||||
new ManiaRuleset().RulesetInfo
|
||||
];
|
||||
|
||||
foreach (var ruleset in rulesets)
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand;
|
||||
@@ -108,7 +109,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
|
||||
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
|
||||
|
||||
var requestHandler = new BeatmapRequestHandler();
|
||||
var requestHandler = new BeatmapRequestHandler(Ruleset.Value);
|
||||
|
||||
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
|
||||
|
||||
@@ -224,7 +225,8 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
|
||||
AddStep("load screen", () => LoadScreen(screen = new RankedPlayScreen(MultiplayerClient.ClientRoom!)));
|
||||
|
||||
var requestHandler = new BeatmapRequestHandler();
|
||||
BeatmapRequestHandler requestHandler = null!;
|
||||
AddStep("setup ruleset", () => requestHandler = new BeatmapRequestHandler(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand;
|
||||
using osuTK;
|
||||
@@ -17,18 +18,18 @@ namespace osu.Game.Tests.Visual.RankedPlay
|
||||
{
|
||||
private readonly Bindable<bool> previewEnabled = new BindableBool(true);
|
||||
|
||||
private readonly BeatmapRequestHandler requestHandler = new BeatmapRequestHandler();
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
BeatmapRequestHandler requestHandler = null!;
|
||||
AddStep("setup ruleset", () => requestHandler = new BeatmapRequestHandler(new OsuRuleset().RulesetInfo));
|
||||
|
||||
AddStep("setup request handler", () => ((DummyAPIAccess)API).HandleRequest = requestHandler.HandleRequest);
|
||||
|
||||
AddStep("add cards", () =>
|
||||
{
|
||||
PlayerHandOfCards handOfCards;
|
||||
|
||||
Child = handOfCards = new PlayerHandOfCards
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
@@ -76,6 +76,12 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
[Test]
|
||||
public void TestLocalRank()
|
||||
{
|
||||
AddStep("set null rank", () => this.ChildrenOfType<UpdateableRank>().ForEach(p =>
|
||||
{
|
||||
p.Hide();
|
||||
p.Rank = null;
|
||||
}));
|
||||
|
||||
foreach (var rank in Enum.GetValues<ScoreRank>())
|
||||
{
|
||||
AddStep($"set {rank.GetDescription()} rank", () => this.ChildrenOfType<UpdateableRank>().ForEach(p =>
|
||||
|
||||
@@ -661,23 +661,22 @@ namespace osu.Game.Database
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Log(@"Querying for beatmaps that do not have user tags");
|
||||
Logger.Log(@"Updating user tags");
|
||||
|
||||
// it is not an abnormal situation for a map not to have user tags.
|
||||
// while this is constrained to run every month or so (every time a new online.db cache is retrieved), there's some chance that this will still run much too often and be annoying to users.
|
||||
// if that turns out to be the case we may need a better way to debounce this (or just delete the backpopulation logic after some time has passed?)
|
||||
HashSet<Guid> beatmapIds = realmAccess.Run(r => new HashSet<Guid>(
|
||||
r.All<BeatmapInfo>()
|
||||
.Filter($"{nameof(BeatmapInfo.Metadata)}.{nameof(BeatmapMetadata.UserTags)}.@count == 0 AND {nameof(BeatmapInfo.StatusInt)} IN {{ 1,2,4 }}")
|
||||
.Filter($"{nameof(BeatmapInfo.StatusInt)} IN {{ 1,2,4 }}")
|
||||
.AsEnumerable()
|
||||
.Select(b => b.ID)));
|
||||
|
||||
if (beatmapIds.Count == 0)
|
||||
return;
|
||||
|
||||
Logger.Log($@"Found {beatmapIds.Count} beatmaps with missing user tags.");
|
||||
Logger.Log($@"Checking for tag updates for {beatmapIds.Count} beatmaps.");
|
||||
|
||||
var notification = showProgressNotification(beatmapIds.Count, @"Populating missing user tags",
|
||||
var notification = showProgressNotification(beatmapIds.Count, @"Updating user tags",
|
||||
@"beatmaps have had their tags updated. This runs once a month to allow searching user tags.");
|
||||
|
||||
int processedCount = 0;
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace osu.Game.Extensions
|
||||
return floatValue.ToString($@"0.{new string('0', Math.Max(0, significantDigits - 2))}%", CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? "-" : string.Empty;
|
||||
string negativeSign = Math.Round(floatValue, significantDigits) < 0 ? CultureInfo.CurrentCulture.NumberFormat.NegativeSign : string.Empty;
|
||||
|
||||
return $"{negativeSign}{Math.Abs(floatValue).ToString($"N{significantDigits}", CultureInfo.CurrentCulture)}";
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Screens.Footer;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@@ -24,7 +26,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
Anchor = Anchor.TopLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
Text = @"back",
|
||||
Text = CommonStrings.Back.ToLower(),
|
||||
Icon = OsuIcon.LeftCircle,
|
||||
Action = () => Action?.Invoke()
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online;
|
||||
using osuTK;
|
||||
|
||||
@@ -49,19 +50,19 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Background.FadeColour(colours.Gray4, 500, Easing.InOutExpo);
|
||||
Icon.MoveToX(0, 500, Easing.InOutExpo);
|
||||
checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo);
|
||||
TooltipText = "Download";
|
||||
TooltipText = CommonStrings.Download;
|
||||
break;
|
||||
|
||||
case DownloadState.Downloading:
|
||||
Background.FadeColour(colours.Blue, 500, Easing.InOutExpo);
|
||||
Icon.MoveToX(0, 500, Easing.InOutExpo);
|
||||
checkmark.ScaleTo(Vector2.Zero, 500, Easing.InOutExpo);
|
||||
TooltipText = "Downloading...";
|
||||
TooltipText = CommonStrings.Downloading;
|
||||
break;
|
||||
|
||||
case DownloadState.Importing:
|
||||
Background.FadeColour(colours.Yellow, 500, Easing.InOutExpo);
|
||||
TooltipText = "Importing";
|
||||
TooltipText = CommonStrings.Importing;
|
||||
break;
|
||||
|
||||
case DownloadState.LocallyAvailable:
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@@ -161,7 +162,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
set => bouncingIcon.Icon = value;
|
||||
}
|
||||
|
||||
public string Text
|
||||
public LocalisableString Text
|
||||
{
|
||||
set => text.Text = value;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
@@ -23,36 +24,223 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
public abstract partial class ReportPopover<TReportReason> : OsuPopover
|
||||
where TReportReason : struct, Enum
|
||||
{
|
||||
/// <summary>
|
||||
/// The action to run when the report is finalised.
|
||||
/// The arguments to this action are: the reason for the report, and an optional additional comment.
|
||||
/// </summary>
|
||||
public Action<TReportReason, string>? Action;
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The action to run when the report is submitted.
|
||||
/// </summary>
|
||||
public event Action? Submitted;
|
||||
|
||||
/// <summary>
|
||||
/// The action to run when the report is submitted successfully.
|
||||
/// </summary>
|
||||
public event Action? Success;
|
||||
|
||||
/// <summary>
|
||||
/// The action to run when the report failed to submit.
|
||||
/// </summary>
|
||||
public event Action? Failure;
|
||||
|
||||
private ReverseChildIDFillFlowContainer<Drawable> form = null!;
|
||||
private ReportConfirmation confirmation = null!;
|
||||
private OsuEnumDropdown<TReportReason> reasonDropdown = null!;
|
||||
private OsuTextBox commentsTextBox = null!;
|
||||
private ErrorTextFlowContainer errorMessage = null!;
|
||||
private RoundedButton submitButton = null!;
|
||||
private LoadingLayer loadingLayer = null!;
|
||||
|
||||
private readonly LocalisableString header;
|
||||
|
||||
private readonly bool showConfirmation;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ReportPopover{TReportReason}"/>.
|
||||
/// </summary>
|
||||
/// <param name="headerString">The text to display in the header of the popover.</param>
|
||||
protected ReportPopover(LocalisableString headerString)
|
||||
/// <param name="showConfirmation">
|
||||
/// Whether the popover should show a generic "Thank you for your report" confirmation message.
|
||||
/// Set this to `true` if you're displaying a custom message outside of this popover.
|
||||
/// </param>
|
||||
protected ReportPopover(LocalisableString headerString, bool showConfirmation = true)
|
||||
: base(false)
|
||||
{
|
||||
header = headerString;
|
||||
this.showConfirmation = showConfirmation;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Child = new ReverseChildIDFillFlowContainer<Drawable>
|
||||
Content.AutoSizeAxes = Axes.Y;
|
||||
Content.Width = 500;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
Width = 500,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
form = new ReverseChildIDFillFlowContainer<Drawable>
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(7),
|
||||
Padding = new MarginPadding(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle,
|
||||
Size = new Vector2(36),
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = header,
|
||||
Font = OsuFont.Torus.With(size: 25),
|
||||
Margin = new MarginPadding { Bottom = 10 }
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = UsersStrings.ReportReason,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 40,
|
||||
Child = reasonDropdown = new OsuEnumDropdown<TReportReason>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = UsersStrings.ReportComments,
|
||||
},
|
||||
commentsTextBox = new OsuTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
PlaceholderText = UsersStrings.ReportPlaceholder,
|
||||
},
|
||||
errorMessage = new ErrorTextFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
submitButton = new RoundedButton
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Width = 200,
|
||||
BackgroundColour = colours.Red3,
|
||||
Text = UsersStrings.ReportActionsSend,
|
||||
Action = () =>
|
||||
{
|
||||
if (showConfirmation)
|
||||
loadingLayer.Show();
|
||||
|
||||
// we don't want size easing to mess up any transforms that are happening
|
||||
// when the popover is appearing, hence easing is only enabled after
|
||||
// the report is submitted
|
||||
Content.AutoSizeEasing = Easing.OutQuint;
|
||||
Content.AutoSizeDuration = 500F;
|
||||
|
||||
Submitted?.Invoke();
|
||||
performRequest();
|
||||
|
||||
if (!showConfirmation)
|
||||
this.HidePopover();
|
||||
},
|
||||
Margin = new MarginPadding { Bottom = 5, Top = 10 },
|
||||
},
|
||||
},
|
||||
},
|
||||
confirmation = new ReportConfirmation(),
|
||||
loadingLayer = new LoadingLayer(true)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
};
|
||||
|
||||
commentsTextBox.Current.BindValueChanged(_ => updateStatus());
|
||||
|
||||
reasonDropdown.Current.BindValueChanged(_ => updateStatus());
|
||||
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
private void performRequest()
|
||||
{
|
||||
var request = GetRequest(reasonDropdown.Current.Value, commentsTextBox.Text);
|
||||
|
||||
request.Success += handleSuccess;
|
||||
request.Failure += handleFailure;
|
||||
|
||||
api.Queue(request);
|
||||
}
|
||||
|
||||
private void handleSuccess()
|
||||
{
|
||||
if (showConfirmation)
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
form.Hide();
|
||||
confirmation.Show();
|
||||
|
||||
loadingLayer.Hide();
|
||||
Scheduler.AddDelayed(this.HidePopover, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
Success?.Invoke();
|
||||
}
|
||||
|
||||
private void handleFailure(Exception e)
|
||||
{
|
||||
if (showConfirmation)
|
||||
{
|
||||
Schedule(() => errorMessage.AddErrors([e.Message]));
|
||||
loadingLayer.Hide();
|
||||
}
|
||||
|
||||
Failure?.Invoke();
|
||||
}
|
||||
|
||||
private void updateStatus()
|
||||
{
|
||||
submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || !IsCommentRequired(reasonDropdown.Current.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the API request responsible for submitting this report.
|
||||
/// </summary>
|
||||
/// <param name="reason">The reason for this report.</param>
|
||||
/// <param name="comments">An optional comment explaining the report.</param>
|
||||
/// <returns></returns>
|
||||
protected abstract APIRequest GetRequest(TReportReason reason, string comments);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an additional comment is required for submitting the report with the supplied <paramref name="reason"/>.
|
||||
/// </summary>
|
||||
protected virtual bool IsCommentRequired(TReportReason reason) => true;
|
||||
|
||||
public partial class ReportConfirmation : FillFlowContainer
|
||||
{
|
||||
public ReportConfirmation()
|
||||
{
|
||||
Direction = FillDirection.Vertical;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Spacing = new Vector2(7);
|
||||
Padding = new MarginPadding(20);
|
||||
Alpha = 0;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new SpriteIcon
|
||||
@@ -66,68 +254,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = header,
|
||||
Text = UsersStrings.ReportThanks,
|
||||
Font = OsuFont.Torus.With(size: 25),
|
||||
Margin = new MarginPadding { Bottom = 10 }
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = UsersStrings.ReportReason,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 40,
|
||||
Child = reasonDropdown = new OsuEnumDropdown<TReportReason>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X
|
||||
}
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Text = UsersStrings.ReportComments,
|
||||
},
|
||||
commentsTextBox = new OsuTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
PlaceholderText = UsersStrings.ReportPlaceholder,
|
||||
},
|
||||
submitButton = new RoundedButton
|
||||
{
|
||||
Origin = Anchor.TopCentre,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Width = 200,
|
||||
BackgroundColour = colours.Red3,
|
||||
Text = UsersStrings.ReportActionsSend,
|
||||
Action = () =>
|
||||
{
|
||||
Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text);
|
||||
this.HidePopover();
|
||||
},
|
||||
Margin = new MarginPadding { Bottom = 5, Top = 10 },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
commentsTextBox.Current.BindValueChanged(_ => updateStatus());
|
||||
|
||||
reasonDropdown.Current.BindValueChanged(_ => updateStatus());
|
||||
|
||||
updateStatus();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void updateStatus()
|
||||
{
|
||||
submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(commentsTextBox.Current.Value) || !IsCommentRequired(reasonDropdown.Current.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether an additional comment is required for submitting the report with the supplied <paramref name="reason"/>.
|
||||
/// </summary>
|
||||
protected virtual bool IsCommentRequired(TReportReason reason) => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Height => new TranslatableString(getKey(@"height"), @"Height");
|
||||
|
||||
/// <summary>
|
||||
/// "Download"
|
||||
/// </summary>
|
||||
public static LocalisableString Download => new TranslatableString(getKey(@"download"), @"Download");
|
||||
|
||||
/// <summary>
|
||||
/// "Downloading..."
|
||||
/// </summary>
|
||||
|
||||
@@ -24,6 +24,51 @@ Tomorrow's challenge is now being prepared and will appear soon.");
|
||||
/// </summary>
|
||||
public static LocalisableString ChallengeLiveNotification => new TranslatableString(getKey(@"todays_daily_challenge_is_now"), @"Today's daily challenge is now live! Click here to play.");
|
||||
|
||||
/// <summary>
|
||||
/// "Today's Challenge"
|
||||
/// </summary>
|
||||
public static LocalisableString TodaysChallenge => new TranslatableString(getKey(@"todays_challenge"), @"Today's Challenge");
|
||||
|
||||
/// <summary>
|
||||
/// "Difficulty: {0}"
|
||||
/// </summary>
|
||||
public static LocalisableString DifficultyInfo(string difficultyName) => new TranslatableString(getKey(@"difficulty_info"), @"Difficulty: {0}", difficultyName);
|
||||
|
||||
/// <summary>
|
||||
/// "Time remaining"
|
||||
/// </summary>
|
||||
public static LocalisableString SectionTimeRemaining => new TranslatableString(getKey(@"section_time_remaining"), @"Time remaining");
|
||||
|
||||
/// <summary>
|
||||
/// "Score breakdown"
|
||||
/// </summary>
|
||||
public static LocalisableString SectionScoreBreakdown => new TranslatableString(getKey(@"section_score_breakdown"), @"Score breakdown");
|
||||
|
||||
/// <summary>
|
||||
/// "Total pass count"
|
||||
/// </summary>
|
||||
public static LocalisableString SectionTotalPasses => new TranslatableString(getKey(@"section_total_passes"), @"Total pass count");
|
||||
|
||||
/// <summary>
|
||||
/// "Cumulative total score"
|
||||
/// </summary>
|
||||
public static LocalisableString SectionCumulativeScore => new TranslatableString(getKey(@"section_cumulative_score"), @"Cumulative total score");
|
||||
|
||||
/// <summary>
|
||||
/// "Events"
|
||||
/// </summary>
|
||||
public static LocalisableString SectionEvents => new TranslatableString(getKey(@"section_events"), @"Events");
|
||||
|
||||
/// <summary>
|
||||
/// "You"
|
||||
/// </summary>
|
||||
public static LocalisableString You => new TranslatableString(getKey(@"you"), @"You");
|
||||
|
||||
/// <summary>
|
||||
/// "{0:N0} passes in {1:N0} - {2:N0} range"
|
||||
/// </summary>
|
||||
public static LocalisableString ScoreBreakdownBarTooltip(long passesCount, int minScore, int maxScore) => new TranslatableString(getKey(@"score_breakdown_bar_tooltip"), @"{0:N0} passes in {1:N0} - {2:N0} range", passesCount, minScore, maxScore);
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString DeleteDifficultyDetails(string difficultyName, int objectCount) => new TranslatableString(getKey(@"delete_difficulty_details"), @"Difficulty ""{0}"" with {1} objects", difficultyName, objectCount);
|
||||
|
||||
/// <summary>
|
||||
/// "This overwrites artist, title, source, and tags on all other difficulties. This cannot be undone."
|
||||
/// </summary>
|
||||
public static LocalisableString SyncMetadataConfirmationBody => new TranslatableString(getKey(@"sync_metadata_confirmation_body"), @"This overwrites artist, title, source, and tags on all other difficulties. This cannot be undone.");
|
||||
|
||||
/// <summary>
|
||||
/// "All Bookmarks"
|
||||
/// </summary>
|
||||
|
||||
@@ -218,6 +218,18 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString ApplyToThisDifficulty => new TranslatableString(getKey(@"apply_to_this_difficulty"), @"Only apply to this difficulty");
|
||||
|
||||
/// <summary>
|
||||
/// "Sync metadata with all difficulties"
|
||||
/// </summary>
|
||||
public static LocalisableString SyncMetadataWithAllDifficulties =>
|
||||
new TranslatableString(getKey(@"sync_metadata_with_all_difficulties"), @"Sync metadata with all difficulties");
|
||||
|
||||
/// <summary>
|
||||
/// "Copies artist, title, source, and tags to all difficulties."
|
||||
/// </summary>
|
||||
public static LocalisableString SyncMetadataWithAllDifficultiesTooltip => new TranslatableString(getKey(@"sync_metadata_with_all_difficulties_tooltip"),
|
||||
@"Copies artist, title, source, and tags to all difficulties.");
|
||||
|
||||
/// <summary>
|
||||
/// "Ruleset ({0})"
|
||||
/// </summary>
|
||||
|
||||
@@ -12,38 +12,43 @@ namespace osu.Game.Localisation.HUD
|
||||
/// <summary>
|
||||
/// "Display mode"
|
||||
/// </summary>
|
||||
public static LocalisableString JudgementDisplayMode => new TranslatableString(getKey(@"judgement_display_mode"), "Display mode");
|
||||
public static LocalisableString JudgementDisplayMode => new TranslatableString(getKey(@"judgement_display_mode"), @"Display mode");
|
||||
|
||||
/// <summary>
|
||||
/// "Counter direction"
|
||||
/// </summary>
|
||||
public static LocalisableString FlowDirection => new TranslatableString(getKey(@"flow_direction"), "Counter direction");
|
||||
public static LocalisableString FlowDirection => new TranslatableString(getKey(@"flow_direction"), @"Counter direction");
|
||||
|
||||
/// <summary>
|
||||
/// "Show judgement names"
|
||||
/// </summary>
|
||||
public static LocalisableString ShowJudgementNames => new TranslatableString(getKey(@"show_judgement_names"), "Show judgement names");
|
||||
public static LocalisableString ShowJudgementNames => new TranslatableString(getKey(@"show_judgement_names"), @"Show judgement names");
|
||||
|
||||
/// <summary>
|
||||
/// "Show max judgement"
|
||||
/// </summary>
|
||||
public static LocalisableString ShowMaxJudgement => new TranslatableString(getKey(@"show_max_judgement"), "Show max judgement");
|
||||
public static LocalisableString ShowMaxJudgement => new TranslatableString(getKey(@"show_max_judgement"), @"Show max judgement");
|
||||
|
||||
/// <summary>
|
||||
/// "Simple"
|
||||
/// </summary>
|
||||
public static LocalisableString JudgementDisplayModeSimple => new TranslatableString(getKey(@"judgement_display_mode_simple"), "Simple");
|
||||
public static LocalisableString JudgementDisplayModeSimple => new TranslatableString(getKey(@"judgement_display_mode_simple"), @"Simple");
|
||||
|
||||
/// <summary>
|
||||
/// "Misses only"
|
||||
/// </summary>
|
||||
public static LocalisableString JudgementDisplayModeMissesOnly => new TranslatableString(getKey(@"judgement_display_mode_misses_only"), @"Misses only");
|
||||
|
||||
/// <summary>
|
||||
/// "Normal"
|
||||
/// </summary>
|
||||
public static LocalisableString JudgementDisplayModeNormal => new TranslatableString(getKey(@"judgement_display_mode_normal"), "Normal");
|
||||
public static LocalisableString JudgementDisplayModeNormal => new TranslatableString(getKey(@"judgement_display_mode_normal"), @"Normal");
|
||||
|
||||
/// <summary>
|
||||
/// "All"
|
||||
/// </summary>
|
||||
public static LocalisableString JudgementDisplayModeAll => new TranslatableString(getKey(@"judgement_display_mode_all"), "All");
|
||||
public static LocalisableString JudgementDisplayModeAll => new TranslatableString(getKey(@"judgement_display_mode_all"), @"All");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString TimingBasedColouring => new TranslatableString(getKey(@"Timing_based_colouring"), @"Timing-based note colouring");
|
||||
|
||||
/// <summary>
|
||||
/// "Rate-adjusted hit animations"
|
||||
/// </summary>
|
||||
public static LocalisableString RateAdjustedHitAnimation => new TranslatableString(getKey(@"rate_adjusted_hit_animation"), @"Rate-adjusted hit animations");
|
||||
|
||||
/// <summary>
|
||||
/// "Hits will fly faster or slower when beatmap rate is adjusted via mods."
|
||||
/// </summary>
|
||||
public static LocalisableString RateAdjustedHitAnimationTooltip => new TranslatableString(getKey(@"rate_adjusted_hit_animation_tooltip"), @"Hits will fly faster or slower when beatmap rate is adjusted via mods.");
|
||||
|
||||
/// <summary>
|
||||
/// "{0}ms (speed {1:N1})"
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Overlays.Profile;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class UserReportRequest : APIRequest
|
||||
{
|
||||
public readonly int UserID;
|
||||
public readonly UserReportReason Reason;
|
||||
public readonly string Comment;
|
||||
|
||||
public UserReportRequest(int userID, UserReportReason reason, string comment)
|
||||
{
|
||||
UserID = userID;
|
||||
Reason = reason;
|
||||
Comment = comment;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var request = base.CreateWebRequest();
|
||||
request.Method = HttpMethod.Post;
|
||||
|
||||
request.AddParameter(@"reportable_type", @"user");
|
||||
request.AddParameter(@"reportable_id", $"{UserID}");
|
||||
request.AddParameter(@"reason", Reason.ToString());
|
||||
request.AddParameter(@"comments", Comment);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
protected override string Target => @"reports";
|
||||
}
|
||||
}
|
||||
@@ -26,5 +26,18 @@ namespace osu.Game.Online
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
Task DisconnectRequested();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when server begins a shutdown sequence.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Server shutdowns are graceful.
|
||||
///
|
||||
/// This will fire with hours of notice for clients to do what they need to and subsequently
|
||||
/// disconnect. It's in the client's best interest to switch over to the new hubs as soon as
|
||||
/// it can, so that the user can be on the same server as the majority of others (and avoid a
|
||||
/// "server split" scenario where users are split across multiple shutting-down hubs).
|
||||
/// </remarks>
|
||||
Task ServerShuttingDown();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -19,6 +20,11 @@ namespace osu.Game.Online.Metadata
|
||||
{
|
||||
public abstract IBindable<bool> IsConnected { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of all watched multiplayer rooms (see <see cref="BeginWatchingMultiplayerRoom"/>).
|
||||
/// </summary>
|
||||
protected readonly HashSet<long> WatchedRooms = new HashSet<long>();
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
@@ -179,11 +185,28 @@ namespace osu.Game.Online.Metadata
|
||||
|
||||
#region Disconnection handling
|
||||
|
||||
/// <summary>
|
||||
/// Invoked just prior to disconnection.
|
||||
/// </summary>
|
||||
public event Action? Disconnecting;
|
||||
|
||||
public virtual Task DisconnectRequested()
|
||||
public abstract Task Reconnect();
|
||||
|
||||
protected abstract Task DisconnectInternal();
|
||||
|
||||
Task IStatefulUserHubClient.DisconnectRequested()
|
||||
{
|
||||
Schedule(() => Disconnecting?.Invoke());
|
||||
Schedule(() =>
|
||||
{
|
||||
Disconnecting?.Invoke();
|
||||
DisconnectInternal().FireAndForget();
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IStatefulUserHubClient.ServerShuttingDown()
|
||||
{
|
||||
this.ReconnectWhenReady(IsConnected, () => WatchedRooms.Count == 0, Reconnect);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,8 @@ namespace osu.Game.Online.Metadata
|
||||
connection.On<int, UserPresence?>(nameof(IMetadataClient.FriendPresenceUpdated), ((IMetadataClient)this).FriendPresenceUpdated);
|
||||
connection.On<DailyChallengeInfo?>(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated);
|
||||
connection.On<MultiplayerRoomScoreSetEvent>(nameof(IMetadataClient.MultiplayerRoomScoreSet), ((IMetadataClient)this).MultiplayerRoomScoreSet);
|
||||
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested);
|
||||
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
|
||||
connection.On(nameof(IStatefulUserHubClient.ServerShuttingDown), ((IStatefulUserHubClient)this).ServerShuttingDown);
|
||||
};
|
||||
|
||||
IsConnected.BindTo(connector.IsConnected);
|
||||
@@ -109,6 +110,7 @@ namespace osu.Game.Online.Metadata
|
||||
{
|
||||
userPresences.Clear();
|
||||
friendPresences.Clear();
|
||||
WatchedRooms.Clear();
|
||||
dailyChallengeInfo.Value = null;
|
||||
localUserPresence = default;
|
||||
});
|
||||
@@ -272,6 +274,8 @@ namespace osu.Game.Online.Metadata
|
||||
if (connector?.IsConnected.Value != true)
|
||||
throw new OperationCanceledException();
|
||||
|
||||
WatchedRooms.Add(id);
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
var result = await connection.InvokeAsync<MultiplayerPlaylistItemStats[]>(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false);
|
||||
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching multiplayer room with ID {id}", LoggingTarget.Network);
|
||||
@@ -283,6 +287,8 @@ namespace osu.Game.Online.Metadata
|
||||
if (connector?.IsConnected.Value != true)
|
||||
throw new OperationCanceledException();
|
||||
|
||||
WatchedRooms.Remove(id);
|
||||
|
||||
Debug.Assert(connection != null);
|
||||
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom), id).ConfigureAwait(false);
|
||||
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching multiplayer room with ID {id}", LoggingTarget.Network);
|
||||
@@ -297,17 +303,22 @@ namespace osu.Game.Online.Metadata
|
||||
await connection.InvokeAsync(nameof(IMetadataServer.RefreshFriends)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task DisconnectRequested()
|
||||
{
|
||||
await base.DisconnectRequested().ConfigureAwait(false);
|
||||
if (connector != null)
|
||||
await connector.Disconnect().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
connector?.Dispose();
|
||||
}
|
||||
|
||||
public override async Task Reconnect()
|
||||
{
|
||||
if (connector != null)
|
||||
await connector.Reconnect().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override async Task DisconnectInternal()
|
||||
{
|
||||
if (connector != null)
|
||||
await connector.Disconnect().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@@ -117,11 +115,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
/// </summary>
|
||||
public event Action? ResultsReady;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked just prior to disconnection requested by the server via <see cref="IStatefulUserHubClient.DisconnectRequested"/>.
|
||||
/// </summary>
|
||||
public event Action? Disconnecting;
|
||||
|
||||
public event Action<MultiplayerCountdown>? CountdownStarted;
|
||||
|
||||
public event Action<MultiplayerCountdown>? CountdownStopped;
|
||||
@@ -490,8 +483,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);
|
||||
|
||||
public abstract Task DisconnectInternal();
|
||||
|
||||
public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId);
|
||||
|
||||
/// <summary>
|
||||
@@ -1082,15 +1073,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
});
|
||||
}
|
||||
|
||||
Task IStatefulUserHubClient.DisconnectRequested()
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
Disconnecting?.Invoke();
|
||||
DisconnectInternal();
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
#region Matchmaking / Ranked Play
|
||||
|
||||
Task IMatchmakingClient.MatchmakingQueueJoined()
|
||||
{
|
||||
@@ -1240,14 +1223,35 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
public abstract Task MatchmakingSkipToNextStage();
|
||||
|
||||
private partial class MultiplayerInvitationNotification : UserAvatarNotification
|
||||
{
|
||||
protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
|
||||
#endregion
|
||||
|
||||
public MultiplayerInvitationNotification(APIUser user, Room room)
|
||||
: base(user, NotificationsStrings.InvitedYouToTheMultiplayer(user.Username, room.Name))
|
||||
#region Disconnection handling
|
||||
|
||||
/// <summary>
|
||||
/// Invoked just prior to disconnection.
|
||||
/// </summary>
|
||||
public event Action? Disconnecting;
|
||||
|
||||
protected abstract Task DisconnectInternal();
|
||||
|
||||
public abstract Task Reconnect();
|
||||
|
||||
Task IStatefulUserHubClient.DisconnectRequested()
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
}
|
||||
Disconnecting?.Invoke();
|
||||
DisconnectInternal().FireAndForget();
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IStatefulUserHubClient.ServerShuttingDown()
|
||||
{
|
||||
this.ReconnectWhenReady(IsConnected, () => room == null, Reconnect);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ExceptionExtensions;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Utils;
|
||||
|
||||
@@ -43,6 +45,43 @@ namespace osu.Game.Online.Multiplayer
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Start a background process to disconnect/reconnect as soon as a specific condition is met.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a reconnect happens via another means, this will abort attempts.
|
||||
/// We only want to reconnect once.
|
||||
/// </remarks>
|
||||
/// <param name="client">The client to operate on.</param>
|
||||
/// <param name="isConnected">Connected state of client.</param>
|
||||
/// <param name="readyFunction">The condition which should be <c>true</c> to continue with the shutdown.</param>
|
||||
/// <param name="reconnectFunction">The method to run to perform the reconnect.</param>
|
||||
public static void ReconnectWhenReady(this IStatefulUserHubClient client, IBindable<bool> isConnected, Func<bool> readyFunction, Func<Task> reconnectFunction)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
bool didReconnect = false;
|
||||
var connected = isConnected.GetBoundCopy();
|
||||
connected.ValueChanged += _ => didReconnect = true;
|
||||
|
||||
string clientName = client.GetType().ReadableName();
|
||||
|
||||
Logger.Log($"{clientName} has signalled shutdown");
|
||||
|
||||
while (!readyFunction())
|
||||
{
|
||||
Logger.Log($"{clientName} shutdown waiting for idle conditions...");
|
||||
await Task.Delay(10000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Logger.Log($"{clientName} disconnecting due to shutdown signal");
|
||||
if (!didReconnect)
|
||||
await reconnectFunction().ConfigureAwait(false);
|
||||
|
||||
connected.UnbindAll();
|
||||
}).FireAndForget();
|
||||
}
|
||||
|
||||
public static string? GetHubExceptionMessage(this Exception exception)
|
||||
{
|
||||
if (exception is HubException hubException)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public partial class MultiplayerInvitationNotification : UserAvatarNotification
|
||||
{
|
||||
protected override IconUsage CloseButtonIcon => FontAwesome.Solid.Times;
|
||||
|
||||
public MultiplayerInvitationNotification(APIUser user, Room room)
|
||||
: base(user, NotificationsStrings.InvitedYouToTheMultiplayer(user.Username, room.Name))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,15 +10,15 @@ using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Matchmaking;
|
||||
using osu.Game.Online.Matchmaking.Requests;
|
||||
using osu.Game.Online.Matchmaking.Responses;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
|
||||
using osu.Game.Online.RankedPlay;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
@@ -92,7 +92,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
connection.On<RankedPlayCardItem, MultiplayerPlaylistItem>(nameof(IRankedPlayClient.RankedPlayCardRevealed), ((IRankedPlayClient)this).RankedPlayCardRevealed);
|
||||
connection.On<RankedPlayCardItem>(nameof(IRankedPlayClient.RankedPlayCardPlayed), ((IRankedPlayClient)this).RankedPlayCardPlayed);
|
||||
|
||||
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMultiplayerClient)this).DisconnectRequested);
|
||||
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
|
||||
connection.On(nameof(IStatefulUserHubClient.ServerShuttingDown), ((IStatefulUserHubClient)this).ServerShuttingDown);
|
||||
};
|
||||
|
||||
IsConnected.BindTo(connector.IsConnected);
|
||||
@@ -335,14 +336,6 @@ namespace osu.Game.Online.Multiplayer
|
||||
return connection.InvokeAsync(nameof(IMultiplayerServer.VoteToSkipIntro));
|
||||
}
|
||||
|
||||
public override Task DisconnectInternal()
|
||||
{
|
||||
if (connector == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return connector.Disconnect();
|
||||
}
|
||||
|
||||
public override Task DiscardCards(RankedPlayCardItem[] cards)
|
||||
{
|
||||
if (!IsConnected.Value)
|
||||
@@ -467,5 +460,17 @@ namespace osu.Game.Online.Multiplayer
|
||||
base.Dispose(isDisposing);
|
||||
connector?.Dispose();
|
||||
}
|
||||
|
||||
public override async Task Reconnect()
|
||||
{
|
||||
if (connector != null)
|
||||
await connector.Reconnect().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override async Task DisconnectInternal()
|
||||
{
|
||||
if (connector != null)
|
||||
await connector.Disconnect().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Online.Rooms
|
||||
private readonly RoomStatusFilter? status;
|
||||
private readonly string category;
|
||||
|
||||
public GetRoomsRequest(FilterCriteria filterCriteria)
|
||||
public GetRoomsRequest(LoungeFilterCriteria filterCriteria)
|
||||
{
|
||||
mode = filterCriteria.Mode;
|
||||
category = filterCriteria.Category;
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace osu.Game.Online.Spectator
|
||||
connection.On<SpectatorUser[]>(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching);
|
||||
connection.On<int>(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching);
|
||||
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
|
||||
connection.On(nameof(IStatefulUserHubClient.ServerShuttingDown), ((IStatefulUserHubClient)this).ServerShuttingDown);
|
||||
};
|
||||
|
||||
IsConnected.BindTo(connector.IsConnected);
|
||||
@@ -122,14 +123,16 @@ namespace osu.Game.Online.Spectator
|
||||
return connection.InvokeAsync(nameof(ISpectatorServer.EndWatchingUser), userId);
|
||||
}
|
||||
|
||||
public override async Task Reconnect()
|
||||
{
|
||||
if (connector != null)
|
||||
await connector.Reconnect().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected override async Task DisconnectInternal()
|
||||
{
|
||||
await base.DisconnectInternal().ConfigureAwait(false);
|
||||
|
||||
if (connector == null)
|
||||
return;
|
||||
|
||||
await connector.Disconnect().ConfigureAwait(false);
|
||||
if (connector != null)
|
||||
await connector.Disconnect().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,11 +75,6 @@ namespace osu.Game.Online.Spectator
|
||||
/// </summary>
|
||||
public event Action<int, long>? OnUserScoreProcessed;
|
||||
|
||||
/// <summary>
|
||||
/// Invoked just prior to disconnection requested by the server via <see cref="IStatefulUserHubClient.DisconnectRequested"/>.
|
||||
/// </summary>
|
||||
public event Action? Disconnecting;
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary containing all users currently being watched, with the number of watching components for each user.
|
||||
/// </summary>
|
||||
@@ -203,12 +198,6 @@ namespace osu.Game.Online.Spectator
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IStatefulUserHubClient.DisconnectRequested()
|
||||
{
|
||||
Schedule(() => DisconnectInternal().FireAndForget());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void BeginPlaying(long? scoreToken, GameplayState state, Score score)
|
||||
{
|
||||
// This schedule is only here to match the one below in `EndPlaying`.
|
||||
@@ -373,12 +362,6 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
protected abstract Task StopWatchingUserInternal(int userId);
|
||||
|
||||
protected virtual Task DisconnectInternal()
|
||||
{
|
||||
Disconnecting?.Invoke();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@@ -446,5 +429,34 @@ namespace osu.Game.Online.Spectator
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#region Disconnection handling
|
||||
|
||||
/// <summary>
|
||||
/// Invoked just prior to disconnection.
|
||||
/// </summary>
|
||||
public event Action? Disconnecting;
|
||||
|
||||
protected abstract Task DisconnectInternal();
|
||||
|
||||
public abstract Task Reconnect();
|
||||
|
||||
Task IStatefulUserHubClient.DisconnectRequested()
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
Disconnecting?.Invoke();
|
||||
DisconnectInternal().FireAndForget();
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Task IStatefulUserHubClient.ServerShuttingDown()
|
||||
{
|
||||
this.ReconnectWhenReady(IsConnected, () => watchedUsersRefCounts.Count == 0, Reconnect);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Utils;
|
||||
using osuTK.Graphics;
|
||||
using CommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
|
||||
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
|
||||
|
||||
namespace osu.Game.Overlays.BeatmapListing
|
||||
{
|
||||
@@ -200,7 +200,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = CommonStrings.ButtonsCancel,
|
||||
Text = WebCommonStrings.ButtonsCancel,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,11 +82,16 @@ namespace osu.Game.Overlays
|
||||
|
||||
Show();
|
||||
|
||||
string[] split = version.Split('-');
|
||||
|
||||
if (split.Length < 2)
|
||||
return;
|
||||
|
||||
string versionPart = split[0];
|
||||
string updateStream = split[1];
|
||||
|
||||
performAfterFetch(() =>
|
||||
{
|
||||
string versionPart = version.Split('-')[0];
|
||||
string updateStream = version.Split('-')[1];
|
||||
|
||||
var build = builds.Find(b => b.Version == versionPart && b.UpdateStream.Name == updateStream)
|
||||
?? Streams.Find(s => s.Name == updateStream)?.LatestBuild;
|
||||
|
||||
|
||||
@@ -12,28 +12,21 @@ namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
public partial class ReportChatPopover : ReportPopover<ChatReportReason>
|
||||
{
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ChannelManager channelManager { get; set; } = null!;
|
||||
|
||||
private readonly Message message;
|
||||
|
||||
public ReportChatPopover(Message message)
|
||||
: base(ReportStrings.UserTitle(message.Sender?.Username ?? @"Someone"))
|
||||
: base(ReportStrings.UserTitle(message.Sender?.Username ?? @"Someone"), false)
|
||||
{
|
||||
this.message = message;
|
||||
Action = report;
|
||||
}
|
||||
|
||||
protected override bool IsCommentRequired(ChatReportReason reason) => reason == ChatReportReason.Other;
|
||||
|
||||
private void report(ChatReportReason reason, string comments)
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var request = new ChatReportRequest(message.Id, reason, comments);
|
||||
|
||||
request.Success += () =>
|
||||
Success += () =>
|
||||
{
|
||||
string thanksMessage;
|
||||
|
||||
@@ -41,10 +34,10 @@ namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
case ChannelType.PM:
|
||||
thanksMessage = """
|
||||
Chat moderators have been alerted. You have reported a private message so they will not be able to read history to maintain your privacy. Please make sure to include as much details as you can.
|
||||
You can submit a second report with more details if required, or contact abuse@ppy.sh if a user is being extremely offensive.
|
||||
You can also block a user via the block button on their user profile, or by right-clicking on their name in the chat and selecting "Block".
|
||||
""";
|
||||
Chat moderators have been alerted. You have reported a private message so they will not be able to read history to maintain your privacy. Please make sure to include as much details as you can.
|
||||
You can submit a second report with more details if required, or contact abuse@ppy.sh if a user is being extremely offensive.
|
||||
You can also block a user via the block button on their user profile, or by right-clicking on their name in the chat and selecting "Block".
|
||||
""";
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -54,8 +47,10 @@ namespace osu.Game.Overlays.Chat
|
||||
|
||||
channelManager.CurrentChannel.Value.AddNewMessages(new InfoMessage(thanksMessage));
|
||||
};
|
||||
|
||||
api.Queue(request);
|
||||
}
|
||||
|
||||
protected override APIRequest GetRequest(ChatReportReason reason, string comments) => new ChatReportRequest(message.Id, reason, comments);
|
||||
|
||||
protected override bool IsCommentRequired(ChatReportReason reason) => reason == ChatReportReason.Other;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
@@ -29,9 +27,6 @@ namespace osu.Game.Overlays.Comments
|
||||
private LinkFlowContainer link = null!;
|
||||
private LoadingSpinner loading = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider? colourProvider { get; set; }
|
||||
|
||||
@@ -60,19 +55,17 @@ namespace osu.Game.Overlays.Comments
|
||||
link.AddLink(ReportStrings.CommentButton.ToLower(), this.ShowPopover);
|
||||
}
|
||||
|
||||
public Popover GetPopover() => new ReportCommentPopover(comment)
|
||||
public Popover GetPopover()
|
||||
{
|
||||
Action = report
|
||||
};
|
||||
var popover = new ReportCommentPopover(comment);
|
||||
|
||||
private void report(CommentReportReason reason, string comments)
|
||||
{
|
||||
var request = new CommentReportRequest(comment.Id, reason, comments);
|
||||
popover.Submitted += () =>
|
||||
{
|
||||
link.Hide();
|
||||
loading.Show();
|
||||
};
|
||||
|
||||
link.Hide();
|
||||
loading.Show();
|
||||
|
||||
request.Success += () => Schedule(() =>
|
||||
popover.Success += () => Schedule(() =>
|
||||
{
|
||||
loading.Hide();
|
||||
|
||||
@@ -83,13 +76,13 @@ namespace osu.Game.Overlays.Comments
|
||||
this.FadeOut(2000, Easing.InQuint).Expire();
|
||||
});
|
||||
|
||||
request.Failure += _ => Schedule(() =>
|
||||
popover.Failure += () => Schedule(() =>
|
||||
{
|
||||
loading.Hide();
|
||||
link.Show();
|
||||
});
|
||||
|
||||
api.Queue(request);
|
||||
return popover;
|
||||
}
|
||||
|
||||
public float LineBaseHeight => link.ChildrenOfType<IHasLineBaseHeight>().FirstOrDefault()?.LineBaseHeight ?? DrawHeight;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
@@ -9,9 +11,16 @@ namespace osu.Game.Overlays.Comments
|
||||
{
|
||||
public partial class ReportCommentPopover : ReportPopover<CommentReportReason>
|
||||
{
|
||||
public ReportCommentPopover(Comment? comment)
|
||||
: base(ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone"))
|
||||
private readonly Comment comment;
|
||||
|
||||
protected override bool IsCommentRequired(CommentReportReason reason) => reason == CommentReportReason.Other;
|
||||
|
||||
public ReportCommentPopover(Comment comment)
|
||||
: base(ReportStrings.CommentTitle(comment.User?.Username ?? comment.LegacyName ?? @"Someone"), false)
|
||||
{
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
protected override APIRequest GetRequest(CommentReportReason reason, string comments) => new CommentReportRequest(comment.Id, reason, comments);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,17 +149,17 @@ namespace osu.Game.Overlays.Dashboard.UserSearch
|
||||
return;
|
||||
}
|
||||
|
||||
queryChangedDebounce = Scheduler.AddDelayed(performSearch, 500);
|
||||
}
|
||||
|
||||
private void performSearch()
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchTextBox.Current.Value))
|
||||
{
|
||||
loading.Value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
queryChangedDebounce = Scheduler.AddDelayed(performSearch, 500);
|
||||
}
|
||||
|
||||
private void performSearch()
|
||||
{
|
||||
loading.Value = true;
|
||||
var getUsersRequest = new SearchUsersRequest(searchTextBox.Current.Value);
|
||||
getUsersRequest.Success += showResults;
|
||||
|
||||
@@ -24,8 +24,8 @@ namespace osu.Game.Overlays.Mods.Input
|
||||
[Key.A] = new[] { typeof(ModHardRock) },
|
||||
[Key.S] = new[] { typeof(ModSuddenDeath), typeof(ModPerfect) },
|
||||
[Key.D] = new[] { typeof(ModDoubleTime), typeof(ModNightcore) },
|
||||
[Key.F] = new[] { typeof(ModHidden) },
|
||||
[Key.G] = new[] { typeof(ModFlashlight) },
|
||||
[Key.F] = new[] { typeof(ModHidden), typeof(ModTraceable) },
|
||||
[Key.G] = new[] { typeof(ModFlashlight), typeof(ModBlinds) },
|
||||
[Key.Z] = new[] { typeof(ModRelax) },
|
||||
[Key.V] = new[] { typeof(ModAutoplay), typeof(ModCinema) }
|
||||
};
|
||||
|
||||
@@ -5,6 +5,9 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
@@ -18,25 +21,50 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
|
||||
|
||||
private UserReportPopoverTarget reportPopoverTarget = null!;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
// This is a bit of a dirty hack. Because `ReportUserPopover` is spawned from `UserActionsPopover`,
|
||||
// and that they both share the same `PopoverContainer`, the former will get destroyed when the latter
|
||||
// is opened, causing it to get destroyed as well.
|
||||
//
|
||||
// This is worked around by having an additional dummy popover target on the actions button,
|
||||
// which is then passed to `UserActionsPopover` and the user report action. This way the popover
|
||||
// can remain attached to it once the actions popover is destroyed.
|
||||
Add(reportPopoverTarget = new UserReportPopoverTarget
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
User.BindValueChanged(_ => Alpha = User.Value?.User.OnlineID == api.LocalUser.Value.OnlineID ? 0 : 1, true);
|
||||
User.BindValueChanged(_ =>
|
||||
{
|
||||
Alpha = User.Value?.User.OnlineID == api.LocalUser.Value.OnlineID ? 0 : 1;
|
||||
reportPopoverTarget.User = User.Value?.User;
|
||||
}, true);
|
||||
}
|
||||
|
||||
public override Popover GetPopover() => new UserActionPopover(User.Value!.User);
|
||||
public override Popover GetPopover() => new UserActionPopover(User.Value!.User, reportPopoverTarget);
|
||||
|
||||
private partial class UserActionPopover : ProfileActionPopover
|
||||
{
|
||||
private readonly APIUser user;
|
||||
|
||||
public UserActionPopover(APIUser user)
|
||||
private readonly IHasPopover reportPopoverTarget;
|
||||
|
||||
public UserActionPopover(APIUser user, IHasPopover reportPopoverTarget)
|
||||
{
|
||||
this.user = user;
|
||||
this.reportPopoverTarget = reportPopoverTarget;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -53,9 +81,24 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
dialogOverlay?.Push(userBlocked ? ConfirmBlockActionDialog.Unblock(user) : ConfirmBlockActionDialog.Block(user));
|
||||
this.HidePopover();
|
||||
}
|
||||
}
|
||||
},
|
||||
new ProfilePopoverAction(FontAwesome.Solid.ExclamationTriangle, ReportStrings.UserButton)
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
this.HidePopover();
|
||||
reportPopoverTarget.ShowPopover();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private partial class UserReportPopoverTarget : Container, IHasPopover
|
||||
{
|
||||
public APIUser? User;
|
||||
|
||||
public Popover? GetPopover() => User != null ? new ReportUserPopover(User) : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Profile
|
||||
{
|
||||
public partial class ReportUserPopover : ReportPopover<UserReportReason>
|
||||
{
|
||||
private readonly APIUser user;
|
||||
|
||||
public ReportUserPopover(APIUser user)
|
||||
: base(ReportStrings.UserTitle(user.Username))
|
||||
{
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
protected override APIRequest GetRequest(UserReportReason reason, string comments) => new UserReportRequest(user.Id, reason, comments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Profile
|
||||
{
|
||||
public enum UserReportReason
|
||||
{
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsCheating))]
|
||||
Cheating,
|
||||
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsMultipleAccounts))]
|
||||
MultipleAccounts,
|
||||
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsInappropriateChat))]
|
||||
InappropriateChat,
|
||||
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsUnwantedContent))]
|
||||
UnwantedContent,
|
||||
|
||||
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsOther))]
|
||||
Other,
|
||||
}
|
||||
}
|
||||
@@ -34,9 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
Caption = GraphicsSettingsStrings.Renderer,
|
||||
Current = renderer,
|
||||
Items = host.GetPreferredRenderersForCurrentPlatform().Order()
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
.Where(t => t != RendererType.Vulkan && t != RendererType.OpenGLLegacy),
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
.Where(t => t != RendererType.Vulkan),
|
||||
})
|
||||
{
|
||||
Keywords = new[] { @"compatibility", @"directx" },
|
||||
|
||||
@@ -53,6 +53,11 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// </summary>
|
||||
public AdditionalMetric[] AdditionalMetrics { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// An optional formatting specifier for the values of this attribute.
|
||||
/// </summary>
|
||||
public string ValueFormat { get; init; } = string.Empty;
|
||||
|
||||
public RulesetBeatmapAttribute(LocalisableString label, string acronym, float originalValue, float adjustedValue, float maxValue)
|
||||
{
|
||||
Label = label;
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
new CheckAudioPresence(),
|
||||
new CheckAudioQuality(),
|
||||
new CheckMutedObjects(),
|
||||
new CheckFewHitsounds(),
|
||||
new CheckTooShortAudioFiles(),
|
||||
new CheckAudioInVideo(),
|
||||
new CheckDelayedHitsounds(),
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckFewHitsounds : ICheck
|
||||
public abstract class CheckFewHitsounds : ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// 2 measures (4/4) of 120 BPM, typically makes up a few patterns in the map.
|
||||
@@ -45,12 +47,21 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
private bool mapHasHitsounds;
|
||||
private int objectsWithoutHitsounds;
|
||||
private double lastHitsoundTime;
|
||||
private IReadOnlyList<(double StartTime, double EndTime)> excludedTimeRanges = Array.Empty<(double StartTime, double EndTime)>();
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
if (!context.CurrentDifficulty.Playable.HitObjects.Any())
|
||||
yield break;
|
||||
|
||||
excludedTimeRanges = context.CurrentDifficulty.Playable.Breaks
|
||||
.Select(b => (b.StartTime, b.EndTime))
|
||||
.Concat(context.CurrentDifficulty.Playable.HitObjects
|
||||
.Where(IsExcludedFromHitsounding)
|
||||
.Select(h => (h.StartTime, EndTime: h.GetEndTime())))
|
||||
.Where(period => period.EndTime > period.StartTime)
|
||||
.ToList();
|
||||
|
||||
mapHasHitsounds = false;
|
||||
objectsWithoutHitsounds = 0;
|
||||
lastHitsoundTime = context.CurrentDifficulty.Playable.HitObjects.First().StartTime;
|
||||
@@ -61,9 +72,13 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
// Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat).
|
||||
foreach (var nestedHitObject in hitObject.NestedHitObjects)
|
||||
hitObjectsIncludingNested.Add(nestedHitObject);
|
||||
{
|
||||
if (!IsExcludedFromHitsounding(nestedHitObject))
|
||||
hitObjectsIncludingNested.Add(nestedHitObject);
|
||||
}
|
||||
|
||||
hitObjectsIncludingNested.Add(hitObject);
|
||||
if (!IsExcludedFromHitsounding(hitObject))
|
||||
hitObjectsIncludingNested.Add(hitObject);
|
||||
}
|
||||
|
||||
var hitObjectsByEndTime = hitObjectsIncludingNested.OrderBy(o => o.GetEndTime()).ToList();
|
||||
@@ -94,7 +109,7 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
// If there are no hitsounds we let the "No hitsounds" template take precedence.
|
||||
if (hasHitsound || (isLastObject && mapHasHitsounds))
|
||||
{
|
||||
double timeWithoutHitsounds = time - lastHitsoundTime;
|
||||
double timeWithoutHitsounds = getTimeWithoutHitsounds(lastHitsoundTime, time);
|
||||
|
||||
if (timeWithoutHitsounds > problem_threshold_time && objectsWithoutHitsounds > problem_threshold_objects)
|
||||
yield return new IssueTemplateLongPeriodProblem(this).Create(lastHitsoundTime, timeWithoutHitsounds);
|
||||
@@ -114,6 +129,31 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
++objectsWithoutHitsounds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Milliseconds between <paramref name="start"/> and <paramref name="end"/> that are not covered by a <see cref="BreakPeriod"/> or excluded object duration.
|
||||
/// </summary>
|
||||
private double getTimeWithoutHitsounds(double start, double end)
|
||||
{
|
||||
if (end <= start)
|
||||
return 0;
|
||||
|
||||
double duration = end - start;
|
||||
|
||||
foreach (var period in excludedTimeRanges)
|
||||
{
|
||||
double overlapStart = Math.Max(start, period.StartTime);
|
||||
double overlapEnd = Math.Min(end, period.EndTime);
|
||||
|
||||
if (overlapEnd > overlapStart)
|
||||
duration -= overlapEnd - overlapStart;
|
||||
}
|
||||
|
||||
return Math.Max(0, duration);
|
||||
}
|
||||
|
||||
// Intended to be overridden by ruleset-specific objects e.g. spinners, banana showers.
|
||||
protected virtual bool IsExcludedFromHitsounding(HitObject hitObject) => false;
|
||||
|
||||
private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.ALL_ADDITIONS.Any(sample.Name.Contains);
|
||||
private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL);
|
||||
|
||||
|
||||
@@ -541,7 +541,12 @@ namespace osu.Game.Rulesets.Edit
|
||||
public void CommitPlacement(HitObject hitObject)
|
||||
{
|
||||
EditorBeatmap.PlacementObject.Value = null;
|
||||
|
||||
EditorBeatmap.BeginChange();
|
||||
foreach (var h in EditorBeatmap.HitObjects.Where(ho => HitObjectPlacementBlueprint.PlacementReplacesExisting(ho, hitObject)).ToArray())
|
||||
EditorBeatmap.Remove(h);
|
||||
EditorBeatmap.Add(hitObject);
|
||||
EditorBeatmap.EndChange();
|
||||
|
||||
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
|
||||
EditorClock.SeekSmoothlyTo(hitObject.StartTime);
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -53,6 +54,12 @@ namespace osu.Game.Rulesets.Edit
|
||||
[Resolved]
|
||||
private IPlacementHandler placementHandler { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Acceptable leniency to account for rounding errors and minor unsnaps that we generally
|
||||
/// don't consider a problem, but still need to account for in certain operations.
|
||||
/// </summary>
|
||||
private const double placement_replace_start_time_leniency_ms = 2;
|
||||
|
||||
protected HitObjectPlacementBlueprint(HitObject hitObject)
|
||||
{
|
||||
HitObject = hitObject;
|
||||
@@ -61,6 +68,23 @@ namespace osu.Game.Rulesets.Edit
|
||||
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether <paramref name="existing"/> should be removed because <paramref name="placement"/> is being placed on top of it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Matches when start times are within ±<see cref="placement_replace_start_time_leniency_ms"/> ms of each other.
|
||||
/// </remarks>
|
||||
public static bool PlacementReplacesExisting(HitObject existing, HitObject placement)
|
||||
{
|
||||
if (!Precision.AlmostEquals(existing.StartTime, placement.StartTime, placement_replace_start_time_leniency_ms))
|
||||
return false;
|
||||
|
||||
if (placement is IHasColumn placementColumn && existing is IHasColumn existingColumn)
|
||||
return existingColumn.Column == placementColumn.Column;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModBlinds : Mod
|
||||
{
|
||||
// Class only exists for classic hotkey support.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModTraceable : ModWithVisibilityAdjustment
|
||||
{
|
||||
// Class only exists for classic hotkey support.
|
||||
}
|
||||
}
|
||||
@@ -430,6 +430,12 @@ namespace osu.Game.Rulesets
|
||||
yield return new RulesetBeatmapAttribute(SongSelectStrings.HPDrain, @"HP", originalDifficulty.DrainRate, adjustedDifficulty.DrainRate, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overload of <see cref="GetAdjustedDisplayDifficulty"/> for display on Ranked Cards
|
||||
/// </summary>
|
||||
public virtual IEnumerable<RulesetBeatmapAttribute> GetBeatmapAttributesForRankedPlayCard(IBeatmapInfo beatmapInfo, IReadOnlyCollection<Mod> mods) =>
|
||||
GetBeatmapAttributesForDisplay(beatmapInfo, mods);
|
||||
|
||||
/// <summary>
|
||||
/// Creates ruleset-specific beatmap filter criteria to be used on the song select screen.
|
||||
/// </summary>
|
||||
|
||||
@@ -55,6 +55,7 @@ namespace osu.Game.Scoring
|
||||
/// <summary>
|
||||
/// The <see cref="osu.Game.Beatmaps.BeatmapInfo.Hash"/> at the point in time when the score was set.
|
||||
/// </summary>
|
||||
[Indexed]
|
||||
public string BeatmapHash { get; set; } = string.Empty;
|
||||
|
||||
public RulesetInfo Ruleset { get; set; } = null!;
|
||||
|
||||
@@ -513,6 +513,7 @@ namespace osu.Game.Screens.Edit
|
||||
/// </param>
|
||||
public EditorState GetState([CanBeNull] RulesetInfo nextRuleset = null) => new EditorState
|
||||
{
|
||||
Mode = Mode.Value,
|
||||
Time = clock.CurrentTimeAccurate,
|
||||
ClipboardContent = nextRuleset == null || editorBeatmap.BeatmapInfo.Ruleset.ShortName == nextRuleset.ShortName ? Clipboard.Content.Value : string.Empty
|
||||
};
|
||||
@@ -523,6 +524,7 @@ namespace osu.Game.Screens.Edit
|
||||
/// <param name="state">The state to restore.</param>
|
||||
public void RestoreState([NotNull] EditorState state) => Schedule(() =>
|
||||
{
|
||||
Mode.Value = state.Mode;
|
||||
clock.Seek(state.Time);
|
||||
Clipboard.Content.Value = state.ClipboardContent;
|
||||
});
|
||||
@@ -1528,24 +1530,33 @@ namespace osu.Game.Screens.Edit
|
||||
loader?.CancelPendingDifficultySwitch();
|
||||
}
|
||||
|
||||
public Task<bool> SaveAndReload()
|
||||
public Task<bool> SaveAndReload(bool withDialog = true)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
dialogOverlay.Push(new SaveAndReloadEditorDialog(
|
||||
reload: () =>
|
||||
void performReload()
|
||||
{
|
||||
bool reloadedSuccessfully = attemptMutationOperation(() =>
|
||||
{
|
||||
bool reloadedSuccessfully = attemptMutationOperation(() =>
|
||||
{
|
||||
if (!Save())
|
||||
return false;
|
||||
if (!Save()) return false;
|
||||
|
||||
SwitchToDifficulty(editorBeatmap.BeatmapInfo);
|
||||
return true;
|
||||
});
|
||||
tcs.SetResult(reloadedSuccessfully);
|
||||
}
|
||||
|
||||
if (withDialog)
|
||||
{
|
||||
dialogOverlay.Push(new SaveAndReloadEditorDialog(
|
||||
reload: performReload,
|
||||
cancel: () => tcs.SetResult(false)));
|
||||
}
|
||||
else
|
||||
{
|
||||
performReload();
|
||||
}
|
||||
|
||||
SwitchToDifficulty(editorBeatmap.BeatmapInfo);
|
||||
return true;
|
||||
});
|
||||
tcs.SetResult(reloadedSuccessfully);
|
||||
},
|
||||
cancel: () => tcs.SetResult(false)));
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@ namespace osu.Game.Screens.Edit
|
||||
/// </summary>
|
||||
public class EditorState
|
||||
{
|
||||
/// <summary>
|
||||
/// The current editor mode.
|
||||
/// </summary>
|
||||
public EditorScreenMode Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current audio time.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
@@ -33,10 +38,19 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
[Resolved]
|
||||
private Editor? editor { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> working { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IDialogOverlay? dialogOverlay { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SetupScreen? setupScreen)
|
||||
{
|
||||
Children = new[]
|
||||
Children = new Drawable[]
|
||||
{
|
||||
ArtistTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Artist),
|
||||
RomanisedArtistTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedArtist),
|
||||
@@ -45,7 +59,16 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
creatorTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Creator),
|
||||
difficultyTextBox = createTextBox<FormTextBox>(EditorSetupStrings.DifficultyName),
|
||||
sourceTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoSource),
|
||||
tagsTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoMapperTags)
|
||||
tagsTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoMapperTags),
|
||||
new RoundedButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = EditorSetupStrings.SyncMetadataWithAllDifficulties,
|
||||
TooltipText = EditorSetupStrings.SyncMetadataWithAllDifficultiesTooltip,
|
||||
Margin = new MarginPadding { Top = 10 },
|
||||
Action = () => dialogOverlay?.Push(new SyncMetadataConfirmationDialog(syncMetadataToAllOtherDifficulties)),
|
||||
Enabled = { Value = working.Value.BeatmapSetInfo.Beatmaps.Count > 1 }
|
||||
}
|
||||
};
|
||||
|
||||
if (setupScreen != null)
|
||||
@@ -54,6 +77,46 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
reloadMetadata();
|
||||
}
|
||||
|
||||
private void syncMetadataToAllOtherDifficulties()
|
||||
{
|
||||
if (working.Value.BeatmapSetInfo.Beatmaps.Count <= 1)
|
||||
return;
|
||||
|
||||
applyMetadata();
|
||||
|
||||
var set = working.Value.BeatmapSetInfo;
|
||||
var current = Beatmap.BeatmapInfo;
|
||||
var source = Beatmap.Metadata;
|
||||
|
||||
foreach (var b in set.Beatmaps)
|
||||
{
|
||||
if (b.Equals(current))
|
||||
continue;
|
||||
|
||||
b.Metadata.ArtistUnicode = source.ArtistUnicode;
|
||||
b.Metadata.Artist = source.Artist;
|
||||
b.Metadata.TitleUnicode = source.TitleUnicode;
|
||||
b.Metadata.Title = source.Title;
|
||||
b.Metadata.Source = source.Source;
|
||||
b.Metadata.Tags = source.Tags;
|
||||
|
||||
try
|
||||
{
|
||||
var targetWorking = beatmaps.GetWorkingBeatmap(b);
|
||||
beatmaps.Save(b, targetWorking.GetPlayableBeatmap(b.Ruleset), targetWorking.GetSkin());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, $@"Failed to sync metadata to {b.GetDisplayTitle()}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the current difficulty and align with how resource changes re-save the current beatmap.
|
||||
// The reload is a crude measure to ensure places like the verify tab do not continue showing problems with mismatching metadata.
|
||||
editor?.SaveAndReload(withDialog: false);
|
||||
}
|
||||
|
||||
private TTextBox createTextBox<TTextBox>(LocalisableString label)
|
||||
where TTextBox : FormTextBox, new()
|
||||
=> new TTextBox
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public partial class SyncMetadataConfirmationDialog : DangerousActionDialog
|
||||
{
|
||||
public SyncMetadataConfirmationDialog(Action syncAction)
|
||||
{
|
||||
BodyText = EditorDialogsStrings.SyncMetadataConfirmationBody;
|
||||
DangerousAction = syncAction;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,7 +322,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
footerButtons.Insert(-1, new UserModSelectButton
|
||||
{
|
||||
Text = "Free mods",
|
||||
Text = OnlinePlayStrings.FooterButtonFreemods,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
@@ -30,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new SectionHeader("Events"),
|
||||
new SectionHeader(DailyChallengeStrings.SectionEvents),
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
@@ -17,9 +17,11 @@ using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
@@ -145,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Today's Challenge",
|
||||
Text = DailyChallengeStrings.TodaysChallenge,
|
||||
Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f },
|
||||
Shear = -OsuGame.SHEAR,
|
||||
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
|
||||
@@ -254,7 +256,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
},
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = $"Difficulty: {beatmap.DifficultyName}",
|
||||
Text = DailyChallengeStrings.DifficultyInfo(beatmap.DifficultyName),
|
||||
Font = OsuFont.GetFont(size: 20, italics: true),
|
||||
MaxWidth = horizontal_info_size,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
@@ -263,7 +265,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
},
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = $"by {beatmap.Metadata.Author.Username}",
|
||||
Text = BeatmappacksStrings.ShowCreatedBy(beatmap.Metadata.Author.Username),
|
||||
Font = OsuFont.GetFont(size: 16, italics: true),
|
||||
MaxWidth = horizontal_info_size,
|
||||
Shear = -OsuGame.SHEAR,
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
@@ -80,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
],
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[] { new SectionHeader("Leaderboard") },
|
||||
new Drawable[] { new SectionHeader(OnlinePlayStrings.PlaylistLeaderboard) },
|
||||
new Drawable[]
|
||||
{
|
||||
new Container
|
||||
@@ -109,7 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
}
|
||||
}
|
||||
},
|
||||
new Drawable[] { userBestHeader = new SectionHeader("Personal best") { Alpha = 0, } },
|
||||
new Drawable[] { userBestHeader = new SectionHeader(BeatmapLeaderboardWedgeStrings.PersonalBest) { Alpha = 0, } },
|
||||
new Drawable[]
|
||||
{
|
||||
userBestContainer = new Container
|
||||
|
||||
@@ -14,6 +14,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
@@ -36,7 +37,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new SectionHeader("Score breakdown"),
|
||||
new SectionHeader(DailyChallengeStrings.SectionScoreBreakdown),
|
||||
barsContainer = new FillFlowContainer<Bar>
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
@@ -215,7 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = colours.Orange1,
|
||||
Text = "You",
|
||||
Text = DailyChallengeStrings.You,
|
||||
Font = OsuFont.Default.With(weight: FontWeight.Bold),
|
||||
Alpha = 0,
|
||||
RelativePositionAxes = Axes.Y,
|
||||
@@ -285,7 +286,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
flashLayer.FadeOutFromOne(600, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public LocalisableString TooltipText => LocalisableString.Format("{0:N0} passes in {1:N0} - {2:N0} range", count, BinStart, BinEnd);
|
||||
public LocalisableString TooltipText => DailyChallengeStrings.ScoreBreakdownBarTooltip(count, BinStart, BinEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Threading;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
@@ -40,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new SectionHeader("Time remaining"),
|
||||
new SectionHeader(DailyChallengeStrings.SectionTimeRemaining),
|
||||
new DrawSizePreservingFillContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
|
||||
using osuTK;
|
||||
|
||||
@@ -42,7 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new SectionHeader("Total pass count")
|
||||
new SectionHeader(DailyChallengeStrings.SectionTotalPasses)
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
@@ -60,7 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
new SectionHeader("Cumulative total score")
|
||||
new SectionHeader(DailyChallengeStrings.SectionCumulativeScore)
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
|
||||
@@ -605,7 +605,7 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
List<MenuItem> items = new List<MenuItem>();
|
||||
|
||||
if (beatmapOverlay != null)
|
||||
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(Item.Beatmap.OnlineID)));
|
||||
items.Add(new OsuMenuItem(CommonStrings.Details, MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(Item.Beatmap.OnlineID)));
|
||||
|
||||
if (beatmap != null)
|
||||
{
|
||||
@@ -617,9 +617,9 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
.Select(c => new CollectionToggleMenuItem(c.ToLive(realm), beatmap)).Cast<OsuMenuItem>().ToList();
|
||||
|
||||
if (manageCollectionsDialog != null)
|
||||
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||
collectionItems.Add(new OsuMenuItem(CommonStrings.Manage, MenuItemType.Standard, manageCollectionsDialog.Show));
|
||||
|
||||
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
|
||||
items.Add(new OsuMenuItem(CommonStrings.Collections) { Items = collectionItems });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
{
|
||||
public class FilterCriteria
|
||||
public class LoungeFilterCriteria
|
||||
{
|
||||
public string SearchString = string.Empty;
|
||||
public RoomModeFilter Mode;
|
||||
@@ -31,7 +31,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
/// <summary>
|
||||
/// The current filter criteria. Should be managed externally.
|
||||
/// </summary>
|
||||
public readonly Bindable<FilterCriteria?> Filter = new Bindable<FilterCriteria?>();
|
||||
public readonly Bindable<LoungeFilterCriteria?> Filter = new Bindable<LoungeFilterCriteria?>();
|
||||
|
||||
/// <summary>
|
||||
/// The currently user-selected room.
|
||||
@@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
|
||||
Filter.BindValueChanged(criteria => applyFilterCriteria(criteria.NewValue), true);
|
||||
}
|
||||
|
||||
private void applyFilterCriteria(FilterCriteria? criteria)
|
||||
private void applyFilterCriteria(LoungeFilterCriteria? criteria)
|
||||
{
|
||||
roomFlow.Children.ForEach(r =>
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user