1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 13:37:25 +08:00

Merge branch 'master' into footer-v2-side-buttons

This commit is contained in:
Salman Ahmed 2024-06-29 08:19:49 +03:00
commit c6c75ae48d
85 changed files with 2137 additions and 773 deletions

1
.gitignore vendored
View File

@ -265,6 +265,7 @@ __pycache__/
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/*/.idea/projectSettingsUpdater.xml
# Generated files
.idea/**/contentModel.xml

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RiderProjectSettingsUpdater">
<option name="vcsConfiguration" value="2" />
</component>
</project>

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.618.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.627.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public partial class TestSceneCatchEditorSaving : EditorSavingTestScene
{
protected override Ruleset CreateRuleset() => new CatchRuleset();
[Test]
public void TestCatchJuiceStreamTickCorrect()
{
AddStep("enter timing mode", () => InputManager.Key(Key.F3));
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("enter compose mode", () => InputManager.Key(Key.F1));
Vector2 startPoint = Vector2.Zero;
float increment = 0;
AddUntilStep("wait for playfield", () => this.ChildrenOfType<CatchPlayfield>().FirstOrDefault()?.IsLoaded == true);
AddStep("move to centre", () =>
{
var playfield = this.ChildrenOfType<CatchPlayfield>().Single();
startPoint = playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Height / 3);
increment = playfield.ScreenSpaceDrawQuad.Height / 10;
InputManager.MoveMouseTo(startPoint);
});
AddStep("choose juice stream placing tool", () => InputManager.Key(Key.Number3));
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(2 * increment, -increment)));
AddStep("add node", () => InputManager.Click(MouseButton.Left));
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(-2 * increment, -2 * increment)));
AddStep("add node", () => InputManager.Click(MouseButton.Left));
AddStep("move to next", () => InputManager.MoveMouseTo(startPoint + new Vector2(0, -3 * increment)));
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
AddUntilStep("juice stream placed", () => EditorBeatmap.HitObjects, () => Has.Count.EqualTo(1));
int largeDropletCount = 0, tinyDropletCount = 0;
AddStep("store droplet count", () =>
{
largeDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet));
tinyDropletCount = EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet));
});
SaveEditor();
ReloadEditorToSameBeatmap();
AddAssert("large droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(Droplet)), () => Is.EqualTo(largeDropletCount));
AddAssert("tiny droplet count is the same", () => EditorBeatmap.HitObjects[0].NestedHitObjects.Count(t => t.GetType() == typeof(TinyDroplet)), () => Is.EqualTo(tinyDropletCount));
}
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1 : ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity,
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield();

View File

@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
@ -46,16 +47,10 @@ namespace osu.Game.Rulesets.Catch.Objects
public double TickDistanceMultiplier = 1;
[JsonIgnore]
private double velocityFactor;
public double Velocity { get; private set; }
[JsonIgnore]
private double tickDistanceFactor;
[JsonIgnore]
public double Velocity => velocityFactor * SliderVelocityMultiplier;
[JsonIgnore]
public double TickDistance => tickDistanceFactor * TickDistanceMultiplier;
public double TickDistance { get; private set; }
/// <summary>
/// The length of one span of this <see cref="JuiceStream"/>.
@ -68,8 +63,13 @@ namespace osu.Game.Rulesets.Catch.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
velocityFactor = base_scoring_distance * difficulty.SliderMultiplier / timingPoint.BeatLength;
tickDistanceFactor = base_scoring_distance * difficulty.SliderMultiplier / difficulty.SliderTickRate;
Velocity = base_scoring_distance * difficulty.SliderMultiplier / LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(this, timingPoint, CatchRuleset.SHORT_NAME);
// WARNING: this is intentionally not computed as `BASE_SCORING_DISTANCE * difficulty.SliderMultiplier`
// for backwards compatibility reasons (intentionally introducing floating point errors to match stable).
double scoringDistance = Velocity * timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
// i dunno this looks about right??
// the guard against zero draw height is intended for zero-length hold notes. yes, such cases have been spotted in the wild.
if (sprite.DrawHeight > 0)
bodySprite.Scale = new Vector2(1, MathF.Max(1, scaleDirection * 32800 / sprite.DrawHeight));
bodySprite.Scale = new Vector2(1, scaleDirection * MathF.Max(1, 32800 / sprite.DrawHeight));
}
break;

View File

@ -51,6 +51,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons())
@ -101,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Edit
updatePositionSnapGrid();
RightToolbox.AddRange(new EditorToolboxGroup[]
RightToolbox.AddRange(new Drawable[]
{
OsuGridToolboxGroup,
new TransformToolboxGroup

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using System.Linq;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuHitObjectInspector : HitObjectInspector
{
protected override void AddInspectorValues()
{
base.AddInspectorValues();
if (EditorBeatmap.SelectedHitObjects.Count > 0)
{
var firstInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MinBy(ho => ho.StartTime)!;
var lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!;
Debug.Assert(firstInSelection != null && lastInSelection != null);
var precedingObject = (OsuHitObject?)EditorBeatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() < firstInSelection.StartTime);
var nextObject = (OsuHitObject?)EditorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime > lastInSelection.GetEndTime());
if (precedingObject != null && precedingObject is not Spinner)
{
AddHeader("To previous");
AddValue($"{(firstInSelection.StackedPosition - precedingObject.StackedEndPosition).Length:#,0.##}px");
}
if (nextObject != null && nextObject is not Spinner)
{
AddHeader("To next");
AddValue($"{(nextObject.StackedPosition - lastInSelection.StackedEndPosition).Length:#,0.##}px");
}
}
}
}
}

View File

@ -53,9 +53,11 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Begin()
{
if (objectsInRotation != null)
if (OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a rotate operation while another is in progress!");
base.Begin();
changeHandler?.BeginChange();
objectsInRotation = selectedMovableObjects.ToArray();
@ -68,10 +70,10 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Update(float rotation, Vector2? origin = null)
{
if (objectsInRotation == null)
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Update)} a rotate operation without calling {nameof(Begin)} first!");
Debug.Assert(originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
Debug.Assert(objectsInRotation != null && originalPositions != null && originalPathControlPointPositions != null && defaultOrigin != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
@ -91,11 +93,13 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Commit()
{
if (objectsInRotation == null)
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange();
base.Commit();
objectsInRotation = null;
originalPositions = null;
originalPathControlPointPositions = null;

View File

@ -72,9 +72,11 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Begin()
{
if (objectsInScale != null)
if (OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Begin)} a scale operation while another is in progress!");
base.Begin();
changeHandler?.BeginChange();
objectsInScale = selectedMovableObjects.ToDictionary(ho => ho, ho => new OriginalHitObjectState(ho));
@ -86,10 +88,10 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Update(Vector2 scale, Vector2? origin = null, Axes adjustAxis = Axes.Both)
{
if (objectsInScale == null)
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Update)} a scale operation without calling {nameof(Begin)} first!");
Debug.Assert(defaultOrigin != null && OriginalSurroundingQuad != null);
Debug.Assert(objectsInScale != null && defaultOrigin != null && OriginalSurroundingQuad != null);
Vector2 actualOrigin = origin ?? defaultOrigin.Value;
@ -117,11 +119,13 @@ namespace osu.Game.Rulesets.Osu.Edit
public override void Commit()
{
if (objectsInScale == null)
if (!OperationInProgress.Value)
throw new InvalidOperationException($"Cannot {nameof(Commit)} a rotate operation without calling {nameof(Begin)} first!");
changeHandler?.EndChange();
base.Commit();
objectsInScale = null;
OriginalSurroundingQuad = null;
defaultOrigin = null;

View File

@ -77,13 +77,15 @@ namespace osu.Game.Rulesets.Osu.Edit
{
case GlobalAction.EditorToggleRotateControl:
{
rotateButton.TriggerClick();
if (!RotationHandler.OperationInProgress.Value || rotateButton.Selected.Value)
rotateButton.TriggerClick();
return true;
}
case GlobalAction.EditorToggleScaleControl:
{
scaleButton.TriggerClick();
if (!ScaleHandler.OperationInProgress.Value || scaleButton.Selected.Value)
scaleButton.TriggerClick();
return true;
}
}

View File

@ -6,7 +6,9 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
@ -23,6 +25,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModDepth) };
protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick);
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
@ -328,15 +329,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
UpdateState(ArmedState.Idle);
UpdateComboColour();
// This method is called every frame. If we need to, the following can likely be converted
// to code which doesn't use transforms at all.
// This method is called every frame in editor contexts, thus the lack of need for transforms.
// Matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
using (BeginAbsoluteSequence(StateUpdateTime - 5))
this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime));
if (Time.Current >= HitStateUpdateTime)
{
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
AccentColour.Value = Color4.White;
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
}
using (BeginAbsoluteSequence(HitStateUpdateTime))
this.FadeOut(700).Expire();
LifetimeEnd = HitStateUpdateTime + 700;
}
internal void RestoreHitAnimations()

View File

@ -48,10 +48,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!positionTransferred && JudgedObject is DrawableOsuHitObject osuObject && JudgedObject.IsInUse)
{
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
Scale = new Vector2(osuObject.HitObject.Scale);
switch (osuObject)
{
case DrawableSlider slider:
Position = slider.TailCircle.ToSpaceOfOtherDrawable(slider.TailCircle.OriginPosition, Parent!);
break;
default:
Position = osuObject.ToSpaceOfOtherDrawable(osuObject.OriginPosition, Parent!);
break;
}
positionTransferred = true;
Scale = new Vector2(osuObject.HitObject.Scale);
}
}

View File

@ -382,7 +382,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
internal void RestoreHitAnimations()
{
UpdateState(ArmedState.Hit, force: true);
UpdateState(ArmedState.Hit);
HeadCircle.RestoreHitAnimations();
TailCircle.RestoreHitAnimations();
}

View File

@ -3,12 +3,12 @@
#nullable disable
using System;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
@ -135,11 +135,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
UpdateState(ArmedState.Idle);
UpdateComboColour();
using (BeginAbsoluteSequence(StateUpdateTime - 5))
this.TransformBindableTo(AccentColour, Color4.White, Math.Max(0, HitStateUpdateTime - StateUpdateTime));
// This method is called every frame in editor contexts, thus the lack of need for transforms.
using (BeginAbsoluteSequence(HitStateUpdateTime))
this.FadeOut(700).Expire();
if (Time.Current >= HitStateUpdateTime)
{
// More or less matches stable (see https://github.com/peppy/osu-stable-reference/blob/bb57924c1552adbed11ee3d96cdcde47cf96f2b6/osu!/GameplayElements/HitObjects/Osu/HitCircleOsu.cs#L336-L338)
AccentColour.Value = Color4.White;
Alpha = Interpolation.ValueAt(Time.Current, 1f, 0f, HitStateUpdateTime, HitStateUpdateTime + 700);
}
LifetimeEnd = HitStateUpdateTime + 700;
}
internal void RestoreHitAnimations()

View File

@ -1,93 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
public class Peaks : Skill
{
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
private const double colour_skill_multiplier = 0.375 * final_multiplier;
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
private const double final_multiplier = 0.0625;
private readonly Rhythm rhythm;
private readonly Colour colour;
private readonly Stamina stamina;
public double ColourDifficultyValue => colour.DifficultyValue() * colour_skill_multiplier;
public double RhythmDifficultyValue => rhythm.DifficultyValue() * rhythm_skill_multiplier;
public double StaminaDifficultyValue => stamina.DifficultyValue() * stamina_skill_multiplier;
public Peaks(Mod[] mods)
: base(mods)
{
rhythm = new Rhythm(mods);
colour = new Colour(mods);
stamina = new Stamina(mods);
}
/// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
/// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
/// <param name="values">The coefficients of the vector.</param>
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
public override void Process(DifficultyHitObject current)
{
rhythm.Process(current);
colour.Process(current);
stamina.Process(current);
}
/// <summary>
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
/// </summary>
/// <remarks>
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
/// </remarks>
public override double DifficultyValue()
{
List<double> peaks = new List<double>();
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
for (int i = 0; i < colourPeaks.Count; i++)
{
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
double peak = norm(1.5, colourPeak, staminaPeak);
peak = norm(2, peak, rhythmPeak);
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
if (peak > 0)
peaks.Add(peak);
}
double difficulty = 0;
double weight = 1;
foreach (double strain in peaks.OrderDescending())
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
}
}

View File

@ -23,6 +23,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
private const double difficulty_multiplier = 1.35;
private const double final_multiplier = 0.0625;
private const double rhythm_skill_multiplier = 0.2 * final_multiplier;
private const double colour_skill_multiplier = 0.375 * final_multiplier;
private const double stamina_skill_multiplier = 0.375 * final_multiplier;
public override int Version => 20221107;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
@ -34,7 +39,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
return new Skill[]
{
new Peaks(mods)
new Rhythm(mods),
new Colour(mods),
new Stamina(mods)
};
}
@ -72,13 +79,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods };
var combined = (Peaks)skills[0];
Colour colour = (Colour)skills.First(x => x is Colour);
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
double colourRating = combined.ColourDifficultyValue * difficulty_multiplier;
double rhythmRating = combined.RhythmDifficultyValue * difficulty_multiplier;
double staminaRating = combined.StaminaDifficultyValue * difficulty_multiplier;
double colourRating = colour.DifficultyValue() * colour_skill_multiplier * difficulty_multiplier;
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier * difficulty_multiplier;
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier * difficulty_multiplier;
double combinedRating = combined.DifficultyValue() * difficulty_multiplier;
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina) * difficulty_multiplier;
double starRating = rescale(combinedRating * 1.4);
HitWindows hitWindows = new TaikoHitWindows();
@ -109,5 +118,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return 10.43 * Math.Log(sr / 8 + 1);
}
/// <summary>
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
/// </summary>
/// <remarks>
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
/// </remarks>
private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina)
{
List<double> peaks = new List<double>();
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
for (int i = 0; i < colourPeaks.Count; i++)
{
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
double peak = norm(1.5, colourPeak, staminaPeak);
peak = norm(2, peak, rhythmPeak);
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
if (peak > 0)
peaks.Add(peak);
}
double difficulty = 0;
double weight = 1;
foreach (double strain in peaks.OrderDescending())
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
/// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
/// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
/// <param name="values">The coefficients of the vector.</param>
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
}
}

View File

@ -5,6 +5,8 @@ using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
@ -74,6 +76,50 @@ namespace osu.Game.Tests.Editing
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestHoldNote()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HoldNote { StartTime = 1000, Duration = 10000 },
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Has.Count.EqualTo(0));
}
[Test]
public void TestHoldNoteWithOverlappingNote()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HoldNote { StartTime = 1000, Duration = 10000 },
new Note { StartTime = 2000 },
new Note { StartTime = 12000 },
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new ManiaRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Has.Count.EqualTo(0));
}
[Test]
public void TestTwoObjectsFarApart()
{
@ -296,5 +342,135 @@ namespace osu.Game.Tests.Editing
Assert.That(beatmap.Breaks[0].EndTime, Is.EqualTo(8800));
});
}
[Test]
public void TestBreaksAtEndOfBeatmapAreRemoved()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
},
Breaks =
{
new BreakPeriod(10000, 15000),
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestManualBreaksAtEndOfBeatmapAreRemoved()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 },
},
Breaks =
{
new ManualBreakPeriod(10000, 15000),
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestManualBreaksAtEndOfBeatmapAreRemovedCorrectlyEvenWithConcurrentObjects()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HoldNote { StartTime = 1000, EndTime = 20000 },
new HoldNote { StartTime = 2000, EndTime = 3000 },
},
Breaks =
{
new ManualBreakPeriod(10000, 15000),
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestBreaksAtStartOfBeatmapAreRemoved()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 10000 },
new HitCircle { StartTime = 11000 },
},
Breaks =
{
new BreakPeriod(0, 9000),
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Is.Empty);
}
[Test]
public void TestManualBreaksAtStartOfBeatmapAreRemoved()
{
var controlPoints = new ControlPointInfo();
controlPoints.Add(0, new TimingControlPoint { BeatLength = 500 });
var beatmap = new Beatmap
{
ControlPointInfo = controlPoints,
HitObjects =
{
new HitCircle { StartTime = 10000 },
new HitCircle { StartTime = 11000 },
},
Breaks =
{
new ManualBreakPeriod(0, 9000),
}
};
var beatmapProcessor = new EditorBeatmapProcessor(beatmap, new OsuRuleset());
beatmapProcessor.PreProcess();
beatmapProcessor.PostProcess();
Assert.That(beatmap.Breaks, Is.Empty);
}
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Tests.NonVisual
public class TestSceneTimedDifficultyCalculation
{
[Test]
public void TestAttributesGeneratedForAllNonSkippedObjects()
public void TestAttributesGeneratedForEachObjectOnce()
{
var beatmap = new Beatmap<TestHitObject>
{
@ -40,15 +40,14 @@ namespace osu.Game.Tests.NonVisual
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(4));
Assert.That(attribs.Count, Is.EqualTo(3));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1]); // From the nested object.
assertEquals(attribs[3], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestAttributesNotGeneratedForSkippedObjects()
public void TestAttributesGeneratedForSkippedObjects()
{
var beatmap = new Beatmap<TestHitObject>
{
@ -72,35 +71,14 @@ namespace osu.Game.Tests.NonVisual
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(1));
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestNestedObjectOnlyAddsParentOnce()
{
var beatmap = new Beatmap<TestHitObject>
{
HitObjects =
{
new TestHitObject
{
StartTime = 1,
Skip = true,
Nested = 2
},
}
};
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(2));
Assert.That(attribs.Count, Is.EqualTo(3));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
[Test]
public void TestSkippedLastObjectAddedInLastIteration()
public void TestAttributesGeneratedOnceForSkippedObjects()
{
var beatmap = new Beatmap<TestHitObject>
{
@ -110,6 +88,7 @@ namespace osu.Game.Tests.NonVisual
new TestHitObject
{
StartTime = 2,
Nested = 5,
Skip = true
},
new TestHitObject
@ -122,8 +101,10 @@ namespace osu.Game.Tests.NonVisual
List<TimedDifficultyAttributes> attribs = new TestDifficultyCalculator(new TestWorkingBeatmap(beatmap)).CalculateTimed();
Assert.That(attribs.Count, Is.EqualTo(1));
assertEquals(attribs[0], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
Assert.That(attribs.Count, Is.EqualTo(3));
assertEquals(attribs[0], beatmap.HitObjects[0]);
assertEquals(attribs[1], beatmap.HitObjects[0], beatmap.HitObjects[1]);
assertEquals(attribs[2], beatmap.HitObjects[0], beatmap.HitObjects[1], beatmap.HitObjects[2]);
}
private void assertEquals(TimedDifficultyAttributes attribs, params HitObject[] expected)

View File

@ -0,0 +1,176 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallengeCarousel : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
private readonly Bindable<Room> room = new Bindable<Room>(new Room());
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new CachedModelDependencyContainer<Room>(base.CreateChildDependencies(parent))
{
Model = { BindTarget = room }
};
[Test]
public void TestBasicAppearance()
{
DailyChallengeCarousel carousel = null!;
AddStep("create content", () => Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
carousel = new DailyChallengeCarousel
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{
if (carousel.IsNotNull())
carousel.Width = width;
});
AddSliderStep("adjust height", 0.1f, 1, 1, height =>
{
if (carousel.IsNotNull())
carousel.Height = height;
});
AddRepeatStep("add content", () => carousel.Add(new FakeContent()), 3);
}
[Test]
public void TestIntegration()
{
GridContainer grid = null!;
DailyChallengeEventFeed feed = null!;
DailyChallengeScoreBreakdown breakdown = null!;
AddStep("create content", () => Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
grid = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RowDimensions =
[
new Dimension(),
new Dimension()
],
Content = new[]
{
new Drawable[]
{
new DailyChallengeCarousel
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new DailyChallengeTimeRemainingRing(),
breakdown = new DailyChallengeScoreBreakdown(),
}
}
},
[
feed = new DailyChallengeEventFeed
{
RelativeSizeAxes = Axes.Both,
}
],
}
},
});
AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{
if (grid.IsNotNull())
grid.Width = width;
});
AddSliderStep("adjust height", 0.1f, 1, 1, height =>
{
if (grid.IsNotNull())
grid.Height = height;
});
AddSliderStep("update time remaining", 0f, 1f, 0f, progress =>
{
var startedTimeAgo = TimeSpan.FromHours(24) * progress;
room.Value.StartDate.Value = DateTimeOffset.Now - startedTimeAgo;
room.Value.EndDate.Value = room.Value.StartDate.Value.Value.AddDays(1);
});
AddStep("add normal score", () =>
{
var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000);
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, null));
breakdown.AddNewScore(testScore);
});
AddStep("add new user best", () =>
{
var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000);
feed.AddNewScore(new DailyChallengeEventFeed.NewScoreEvent(testScore, RNG.Next(1, 1000)));
breakdown.AddNewScore(testScore);
});
}
private partial class FakeContent : CompositeDrawable
{
private OsuSpriteText text = null!;
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1),
},
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Fake Content " + (char)('A' + RNG.Next(26)),
},
};
text.FadeOut(500, Easing.OutQuint)
.Then().FadeIn(500, Easing.OutQuint)
.Loop();
}
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallengeScoreBreakdown : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestBasicAppearance()
{
DailyChallengeScoreBreakdown breakdown = null!;
AddStep("create content", () => Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
breakdown = new DailyChallengeScoreBreakdown
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{
if (breakdown.IsNotNull())
breakdown.Width = width;
});
AddSliderStep("adjust height", 0.1f, 1, 1, height =>
{
if (breakdown.IsNotNull())
breakdown.Height = height;
});
AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1]));
AddStep("add new score", () =>
{
var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000);
breakdown.AddNewScore(testScore);
});
}
}
}

View File

@ -84,6 +84,8 @@ namespace osu.Game.Tests.Visual.Editing
targetContainer = getTargetContainer();
initialRotation = targetContainer!.Rotation;
base.Begin();
}
public override void Update(float rotation, Vector2? origin = null)
@ -102,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing
targetContainer = null;
initialRotation = null;
base.Commit();
}
}

View File

@ -193,5 +193,20 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
AddAssert("Tags reverted correctly", () => Game.Beatmap.Value.BeatmapInfo.Metadata.Tags == tags_to_save);
}
[Test]
public void TestBeatDivisor()
{
AddStep("Set custom beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().SetArbitraryDivisor(7));
SaveEditor();
AddAssert("Hash updated", () => !string.IsNullOrEmpty(EditorBeatmap.BeatmapInfo.BeatmapSet?.Hash));
AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7));
ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7));
AddAssert("Correct beat divisor actually active", () => Editor.BeatDivisor, () => Is.EqualTo(7));
}
}
}

View File

@ -126,6 +126,24 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("sample playback re-enabled", () => !Editor.SamplePlaybackDisabled.Value);
}
[TestCase(2000)] // chosen to be after last object in the map
[TestCase(22000)] // chosen to be in the middle of the last spinner
public void TestGameplayTestAtEndOfBeatmap(int offsetFromEnd)
{
AddStep($"seek to end minus {offsetFromEnd}ms", () => EditorClock.Seek(importedBeatmapSet.MaxLength - offsetFromEnd));
AddStep("click test gameplay button", () =>
{
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer);
AddUntilStep("current screen is editor", () => Stack.CurrentScreen is Editor);
}
[Test]
public void TestCancelGameplayTestWithUnsavedChanges()
{

View File

@ -20,6 +20,7 @@ namespace osu.Game.Tests.Visual.Menus
{
base.SetUpSteps();
AddStep("don't fetch online content", () => onlineMenuBanner.FetchOnlineContent = false);
AddStep("disable return to top on idle", () => Game.ChildrenOfType<ButtonSystem>().Single().ReturnToTopOnIdle = false);
}
[Test]

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Settings
{
AddStep("create", () =>
{
Cell(0, 0).Children = new Drawable[]
ContentContainer.Children = new Drawable[]
{
new Box
{

View File

@ -6,23 +6,51 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneOsuDropdown : ThemeComparisonTestScene
{
protected override Drawable CreateContent() =>
new OsuEnumDropdown<TestEnum>
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Width = 150
};
protected override Drawable CreateContent() => new OsuEnumDropdown<TestEnum>
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Width = 150
};
[Test]
public void TestBackAction()
{
AddStep("open", () => dropdownMenu.Open());
AddStep("press back", () => InputManager.Key(Key.Escape));
AddAssert("closed", () => dropdownMenu.State == MenuState.Closed);
AddStep("open", () => dropdownMenu.Open());
AddStep("type something", () => dropdownSearchBar.SearchTerm.Value = "something");
AddAssert("search bar visible", () => dropdownSearchBar.State.Value == Visibility.Visible);
AddStep("press back", () => InputManager.Key(Key.Escape));
AddAssert("text clear", () => dropdownSearchBar.SearchTerm.Value == string.Empty);
AddAssert("search bar hidden", () => dropdownSearchBar.State.Value == Visibility.Hidden);
AddAssert("still open", () => dropdownMenu.State == MenuState.Open);
AddStep("press back", () => InputManager.Key(Key.Escape));
AddAssert("closed", () => dropdownMenu.State == MenuState.Closed);
}
[Test]
public void TestSelectAction()
{
AddStep("open", () => dropdownMenu.Open());
AddStep("press down", () => InputManager.Key(Key.Down));
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("second selected", () => dropdown.Current.Value == TestEnum.ReallyLongOption);
}
private OsuEnumDropdown<TestEnum> dropdown => this.ChildrenOfType<OsuEnumDropdown<TestEnum>>().Last();
private Menu dropdownMenu => dropdown.ChildrenOfType<Menu>().Single();
private DropdownSearchBar dropdownSearchBar => dropdown.ChildrenOfType<DropdownSearchBar>().Single();
private enum TestEnum
{
@ -32,26 +60,5 @@ namespace osu.Game.Tests.Visual.UserInterface
[System.ComponentModel.Description("Really lonnnnnnng option")]
ReallyLongOption,
}
[Test]
// todo: this can be written much better if ThemeComparisonTestScene has a manual input manager
public void TestBackAction()
{
AddStep("open", () => dropdown().ChildrenOfType<Menu>().Single().Open());
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
AddAssert("closed", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Closed);
AddStep("open", () => dropdown().ChildrenOfType<Menu>().Single().Open());
AddStep("type something", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().SearchTerm.Value = "something");
AddAssert("search bar visible", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().State.Value == Visibility.Visible);
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
AddAssert("text clear", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().SearchTerm.Value == string.Empty);
AddAssert("search bar hidden", () => dropdown().ChildrenOfType<DropdownSearchBar>().Single().State.Value == Visibility.Hidden);
AddAssert("still open", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Open);
AddStep("press back", () => dropdown().OnPressed(new KeyBindingPressEvent<GlobalAction>(new InputState(), GlobalAction.Back)));
AddAssert("closed", () => dropdown().ChildrenOfType<Menu>().Single().State == MenuState.Closed);
OsuEnumDropdown<TestEnum> dropdown() => this.ChildrenOfType<OsuEnumDropdown<TestEnum>>().First();
}
}
}

View File

@ -53,8 +53,8 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestBackgroundColour()
{
AddStep("set red scheme", () => CreateThemedContent(OverlayColourScheme.Red));
AddAssert("rounded button has correct colour", () => Cell(0, 1).ChildrenOfType<RoundedButton>().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3);
AddAssert("settings button has correct colour", () => Cell(0, 1).ChildrenOfType<SettingsButton>().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3);
AddAssert("rounded button has correct colour", () => ContentContainer.ChildrenOfType<RoundedButton>().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3);
AddAssert("settings button has correct colour", () => ContentContainer.ChildrenOfType<SettingsButton>().First().BackgroundColour == new OverlayColourProvider(OverlayColourScheme.Red).Colour3);
}
}
}

View File

@ -6,18 +6,21 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public abstract partial class ThemeComparisonTestScene : OsuGridTestScene
public abstract partial class ThemeComparisonTestScene : OsuManualInputManagerTestScene
{
private readonly bool showWithoutColourProvider;
public Container ContentContainer { get; private set; } = null!;
protected ThemeComparisonTestScene(bool showWithoutColourProvider = true)
: base(1, showWithoutColourProvider ? 2 : 1)
{
this.showWithoutColourProvider = showWithoutColourProvider;
}
@ -25,16 +28,32 @@ namespace osu.Game.Tests.Visual.UserInterface
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Child = ContentContainer = new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
};
if (showWithoutColourProvider)
{
Cell(0, 0).AddRange(new[]
ContentContainer.Size = new Vector2(0.5f, 1f);
Add(new Container
{
new Box
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f, 1f),
Children = new[]
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeaFoam
},
CreateContent()
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeaFoam
},
CreateContent()
}
});
}
}
@ -43,10 +62,8 @@ namespace osu.Game.Tests.Visual.UserInterface
{
var colourProvider = new OverlayColourProvider(colourScheme);
int col = showWithoutColourProvider ? 1 : 0;
Cell(0, col).Clear();
Cell(0, col).Add(new DependencyProvidingContainer
ContentContainer.Clear();
ContentContainer.Add(new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]

View File

@ -8,6 +8,7 @@ using System.Text;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Timing;
using osu.Game.IO;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -59,9 +60,13 @@ namespace osu.Game.Database
// Convert beatmap elements to be compatible with legacy format
// So we truncate time and position values to integers, and convert paths with multiple segments to bezier curves
foreach (var controlPoint in playableBeatmap.ControlPointInfo.AllControlPoints)
controlPoint.Time = Math.Floor(controlPoint.Time);
for (int i = 0; i < playableBeatmap.Breaks.Count; i++)
playableBeatmap.Breaks[i] = new BreakPeriod(Math.Floor(playableBeatmap.Breaks[i].StartTime), Math.Floor(playableBeatmap.Breaks[i].EndTime));
foreach (var hitObject in playableBeatmap.HitObjects)
{
// Truncate end time before truncating start time because end time is dependent on start time

View File

@ -418,16 +418,19 @@ namespace osu.Game.Graphics.UserInterface
FontSize = OsuFont.Default.Size,
};
private partial class DropdownSearchTextBox : SearchTextBox
private partial class DropdownSearchTextBox : OsuTextBox
{
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
[BackgroundDependencyLoader]
private void load(OverlayColourProvider? colourProvider)
{
if (e.Action == GlobalAction.Back)
// this method is blocking Dropdown from receiving the back action, despite this text box residing in a separate input manager.
// to fix this properly, a local global action container needs to be added as well, but for simplicity, just don't handle the back action here.
return false;
BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
}
return base.OnPressed(e);
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
BorderThickness = 0;
}
}
}

View File

@ -412,9 +412,6 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))]
EditorToggleRotateControl,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))]
EditorToggleScaleControl,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.IncreaseOffset))]
IncreaseOffset,
@ -432,6 +429,9 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DecreaseModSpeed))]
DecreaseModSpeed,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleScaleControl))]
EditorToggleScaleControl,
}
public enum GlobalActionCategory

View File

@ -26,5 +26,11 @@ namespace osu.Game.Online.Metadata
/// Null value means there is no "daily challenge" currently active.
/// </summary>
Task DailyChallengeUpdated(DailyChallengeInfo? info);
/// <summary>
/// Delivers information that a multiplayer score was set in a watched room.
/// To receive these, the client must call <see cref="IMetadataServer.BeginWatchingMultiplayerRoom"/> for a given room first.
/// </summary>
Task MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent);
}
}

View File

@ -43,5 +43,15 @@ namespace osu.Game.Online.Metadata
/// Signals to the server that the current user would like to stop receiving updates on other users' online presence.
/// </summary>
Task EndWatchingUserPresence();
/// <summary>
/// Signals to the server that the current user would like to begin receiving updates about the state of the multiplayer room with the given <paramref name="id"/>.
/// </summary>
Task<MultiplayerPlaylistItemStats[]> BeginWatchingMultiplayerRoom(long id);
/// <summary>
/// Signals to the server that the current user would like to stop receiving updates about the state of the multiplayer room with the given <paramref name="id"/>.
/// </summary>
Task EndWatchingMultiplayerRoom(long id);
}
}

View File

@ -68,6 +68,24 @@ namespace osu.Game.Online.Metadata
#endregion
#region Multiplayer room watching
public abstract Task<MultiplayerPlaylistItemStats[]> BeginWatchingMultiplayerRoom(long id);
public abstract Task EndWatchingMultiplayerRoom(long id);
public event Action<MultiplayerRoomScoreSetEvent>? MultiplayerRoomScoreSet;
Task IMetadataClient.MultiplayerRoomScoreSet(MultiplayerRoomScoreSetEvent roomScoreSetEvent)
{
if (MultiplayerRoomScoreSet != null)
Schedule(MultiplayerRoomScoreSet, roomScoreSetEvent);
return Task.CompletedTask;
}
#endregion
#region Disconnection handling
public event Action? Disconnecting;

View File

@ -0,0 +1,29 @@
// 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 MessagePack;
namespace osu.Game.Online.Metadata
{
[MessagePackObject]
[Serializable]
public class MultiplayerPlaylistItemStats
{
public const int TOTAL_SCORE_DISTRIBUTION_BINS = 13;
/// <summary>
/// The ID of the playlist item which these stats pertain to.
/// </summary>
[Key(0)]
public long PlaylistItemID { get; set; }
/// <summary>
/// The count of scores with given total ranges in the room.
/// The ranges are bracketed into <see cref="TOTAL_SCORE_DISTRIBUTION_BINS"/> bins, each of 100,000 score width.
/// The last bin will contain count of all scores with total of 1,200,000 or larger.
/// </summary>
[Key(1)]
public long[] TotalScoreDistribution { get; set; } = new long[TOTAL_SCORE_DISTRIBUTION_BINS];
}
}

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using MessagePack;
namespace osu.Game.Online.Metadata
{
[Serializable]
[MessagePackObject]
public class MultiplayerRoomScoreSetEvent
{
/// <summary>
/// The ID of the room in which the score was set.
/// </summary>
[Key(0)]
public long RoomID { get; set; }
/// <summary>
/// The ID of the playlist item on which the score was set.
/// </summary>
[Key(1)]
public long PlaylistItemID { get; set; }
/// <summary>
/// The ID of the score set.
/// </summary>
[Key(2)]
public long ScoreID { get; set; }
/// <summary>
/// The ID of the user who set the score.
/// </summary>
[Key(3)]
public int UserID { get; set; }
/// <summary>
/// The total score set by the player.
/// </summary>
[Key(4)]
public long TotalScore { get; set; }
/// <summary>
/// If the set score is the user's new best on a playlist item, this member will contain the user's new rank in the room overall.
/// Otherwise, it will contain <see langword="null"/>.
/// </summary>
[Key(5)]
public int? NewRank { get; set; }
}
}

View File

@ -62,6 +62,7 @@ namespace osu.Game.Online.Metadata
connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
connection.On<int, UserPresence?>(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated);
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);
};
@ -240,6 +241,24 @@ namespace osu.Game.Online.Metadata
return Task.CompletedTask;
}
public override async Task<MultiplayerPlaylistItemStats[]> BeginWatchingMultiplayerRoom(long id)
{
if (connector?.IsConnected.Value != true)
throw new OperationCanceledException();
Debug.Assert(connection != null);
return await connection.InvokeAsync<MultiplayerPlaylistItemStats[]>(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false);
}
public override async Task EndWatchingMultiplayerRoom(long id)
{
if (connector?.IsConnected.Value != true)
throw new OperationCanceledException();
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom)).ConfigureAwait(false);
}
public override async Task DisconnectRequested()
{
await base.DisconnectRequested().ConfigureAwait(false);

View File

@ -6,6 +6,7 @@ using System.Linq;
using Humanizer;
using Humanizer.Localisation;
using osu.Framework.Bindables;
using osu.Game.Rulesets;
using osu.Game.Utils;
namespace osu.Game.Online.Rooms
@ -42,14 +43,14 @@ namespace osu.Game.Online.Rooms
/// <summary>
/// Returns the total duration from the <see cref="PlaylistItem"/> in playlist order from the supplied <paramref name="playlist"/>,
/// </summary>
public static string GetTotalDuration(this BindableList<PlaylistItem> playlist) =>
public static string GetTotalDuration(this BindableList<PlaylistItem> playlist, RulesetStore rulesetStore) =>
playlist.Select(p =>
{
double rate = 1;
if (p.RequiredMods.Length > 0)
{
var ruleset = p.Beatmap.Ruleset.CreateInstance();
var ruleset = rulesetStore.GetRuleset(p.RulesetID)!.CreateInstance();
rate = ModUtils.CalculateRateWithMods(p.RequiredMods.Select(mod => mod.ToMod(ruleset)));
}

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Humanizer;
@ -871,6 +872,9 @@ namespace osu.Game
{
base.LoadComplete();
if (RuntimeInfo.EntryAssembly.GetCustomAttribute<OfficialBuildAttribute>() == null)
Logger.Log(NotificationsStrings.NotOfficialBuild.ToString());
var languages = Enum.GetValues<Language>();
var mappings = languages.Select(language =>

View File

@ -56,8 +56,6 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
protected override Color4 GetStateColour() => colours.Orange1;
protected override void LoadComplete()
{
base.LoadComplete();
@ -65,6 +63,9 @@ namespace osu.Game.Overlays.BeatmapListing
disclaimerShown = sessionStatics.GetBindable<bool>(Static.FeaturedArtistDisclaimerShownOnce);
}
protected override Color4 ColourNormal => colours.Orange1;
protected override Color4 ColourActive => colours.Orange2;
protected override bool OnClick(ClickEvent e)
{
if (!disclaimerShown.Value && dialogOverlay != null)

View File

@ -1,21 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
using FontWeight = osu.Game.Graphics.FontWeight;
namespace osu.Game.Overlays.BeatmapListing
{
@ -24,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapListing
{
public new readonly BindableList<T> Current = new BindableList<T>();
private MultipleSelectionFilter filter;
private MultipleSelectionFilter filter = null!;
public BeatmapSearchMultipleSelectionFilterRow(LocalisableString header)
: base(header)
@ -42,7 +44,6 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary>
/// Creates a filter control that can be used to simultaneously select multiple values of type <typeparamref name="T"/>.
/// </summary>
[NotNull]
protected virtual MultipleSelectionFilter CreateMultipleSelectionFilter() => new MultipleSelectionFilter();
protected partial class MultipleSelectionFilter : FillFlowContainer<MultipleSelectionFilterTabItem>
@ -54,7 +55,7 @@ namespace osu.Game.Overlays.BeatmapListing
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Spacing = new Vector2(10, 0);
Spacing = new Vector2(10, 5);
AddRange(GetValues().Select(CreateTabItem));
}
@ -69,7 +70,7 @@ namespace osu.Game.Overlays.BeatmapListing
Current.BindCollectionChanged(currentChanged, true);
}
private void currentChanged(object sender, NotifyCollectionChangedEventArgs e)
private void currentChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
foreach (var c in Children)
c.Active.Value = Current.Contains(c.Value);
@ -99,30 +100,91 @@ namespace osu.Game.Overlays.BeatmapListing
protected partial class MultipleSelectionFilterTabItem : FilterTabItem<T>
{
private readonly Box selectedUnderline;
protected override bool HighlightOnHoverWhenActive => true;
private Drawable activeContent = null!;
private Circle background = null!;
public MultipleSelectionFilterTabItem(T value)
: base(value)
{
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeDuration = 100;
AutoSizeEasing = Easing.OutQuint;
// This doesn't match any actual design, but should make it easier for the user to understand
// that filters are applied until we settle on a final design.
AddInternal(selectedUnderline = new Box
AddInternal(activeContent = new Container
{
Depth = float.MaxValue,
RelativeSizeAxes = Axes.X,
Height = 1.5f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Padding = new MarginPadding
{
Left = -16,
Right = -4,
Vertical = -2
},
Children = new Drawable[]
{
background = new Circle
{
Colour = Color4.White,
RelativeSizeAxes = Axes.Both,
},
new SpriteIcon
{
Icon = FontAwesome.Solid.TimesCircle,
Size = new Vector2(10),
Colour = ColourProvider.Background4,
Position = new Vector2(3, 0.5f),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
}
});
}
protected override Color4 ColourActive => ColourProvider.Light1;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
return Active.Value
? background.ReceivePositionalInputAt(screenSpacePos)
: base.ReceivePositionalInputAt(screenSpacePos);
}
protected override void UpdateState()
{
base.UpdateState();
selectedUnderline.FadeTo(Active.Value ? 1 : 0, 200, Easing.OutQuint);
selectedUnderline.FadeColour(IsHovered ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
Color4 colour = Active.Value ? ColourActive : ColourNormal;
if (IsHovered)
colour = Active.Value ? colour.Darken(0.2f) : colour.Lighten(0.2f);
if (Active.Value)
{
// This just allows enough spacing for adjacent tab items to show the "x".
Padding = new MarginPadding { Left = 12 };
activeContent.FadeIn(200, Easing.OutQuint);
background.FadeColour(colour, 200, Easing.OutQuint);
// flipping colours
Text.FadeColour(ColourProvider.Background4, 200, Easing.OutQuint);
Text.Font = Text.Font.With(weight: FontWeight.SemiBold);
}
else
{
Padding = new MarginPadding();
activeContent.FadeOut();
background.FadeColour(colour, 200, Easing.OutQuint);
Text.FadeColour(colour, 200, Easing.OutQuint);
Text.Font = Text.Font.With(weight: FontWeight.Regular);
}
}
protected override bool OnClick(ClickEvent e)

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
@ -24,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapListing
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; }
private OsuSpriteText text;
protected OsuSpriteText Text;
protected Sample SelectSample { get; private set; } = null!;
@ -39,7 +40,7 @@ namespace osu.Game.Overlays.BeatmapListing
AutoSizeAxes = Axes.Both;
AddRangeInternal(new Drawable[]
{
text = new OsuSpriteText
Text = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular),
Text = LabelFor(Value)
@ -84,16 +85,18 @@ namespace osu.Game.Overlays.BeatmapListing
/// </summary>
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
protected virtual bool HighlightOnHoverWhenActive => false;
protected virtual Color4 ColourActive => ColourProvider.Content1;
protected virtual Color4 ColourNormal => ColourProvider.Light2;
protected virtual void UpdateState()
{
bool highlightHover = IsHovered && (!Active.Value || HighlightOnHoverWhenActive);
Color4 colour = Active.Value ? ColourActive : ColourNormal;
text.FadeColour(highlightHover ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular);
if (IsHovered)
colour = colour.Lighten(0.2f);
Text.FadeColour(colour, 200, Easing.OutQuint);
Text.Font = Text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular);
}
protected virtual Color4 GetStateColour() => Active.Value ? ColourProvider.Content1 : ColourProvider.Light2;
}
}

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -71,7 +72,7 @@ namespace osu.Game.Overlays.Dialog
protected override void LoadComplete()
{
base.LoadComplete();
Progress.BindValueChanged(progressChanged);
Progress.BindValueChanged(progressChanged, true);
}
protected override void AbortConfirm()
@ -122,11 +123,13 @@ namespace osu.Game.Overlays.Dialog
private void progressChanged(ValueChangedEvent<double> progress)
{
if (progress.NewValue < progress.OldValue) return;
lowPassFilter.Cutoff = Math.Max(1, (int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5));
if (Clock.CurrentTime - lastTickPlaybackTime < 30) return;
if (progress.NewValue < progress.OldValue)
return;
lowPassFilter.CutoffTo((int)(progress.NewValue * AudioFilter.MAX_LOWPASS_CUTOFF * 0.5));
if (Clock.CurrentTime - lastTickPlaybackTime < 30)
return;
var channel = tickSample.GetChannel();

View File

@ -16,6 +16,9 @@ namespace osu.Game.Overlays.Mods
{
private readonly BindableBool incompatible = new BindableBool();
[Resolved]
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
[Resolved]
private Bindable<IReadOnlyList<Mod>> selectedMods { get; set; } = null!;
@ -55,7 +58,7 @@ namespace osu.Game.Overlays.Mods
#region IHasCustomTooltip
public ITooltip<Mod> GetCustomTooltip() => new IncompatibilityDisplayingTooltip();
public ITooltip<Mod> GetCustomTooltip() => new IncompatibilityDisplayingTooltip(overlayColourProvider);
public Mod TooltipContent => Mod;

View File

@ -24,13 +24,15 @@ namespace osu.Game.Overlays.Mods
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
public IncompatibilityDisplayingTooltip()
public IncompatibilityDisplayingTooltip(OverlayColourProvider colourProvider)
: base(colourProvider)
{
AddRange(new Drawable[]
{
incompatibleText = new OsuSpriteText
{
Margin = new MarginPadding { Top = 5 },
Colour = colourProvider.Content2,
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = "Incompatible with:"
},
@ -43,12 +45,6 @@ namespace osu.Game.Overlays.Mods
});
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
incompatibleText.Colour = colours.BlueLight;
}
protected override void UpdateDisplay(Mod mod)
{
base.UpdateDisplay(mod);

View File

@ -1,9 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -18,11 +15,10 @@ namespace osu.Game.Overlays.Mods
public partial class ModButtonTooltip : VisibilityContainer, ITooltip<Mod>
{
private readonly OsuSpriteText descriptionText;
private readonly Box background;
protected override Container<Drawable> Content { get; }
public ModButtonTooltip()
public ModButtonTooltip(OverlayColourProvider colourProvider)
{
AutoSizeAxes = Axes.Both;
Masking = true;
@ -30,9 +26,10 @@ namespace osu.Game.Overlays.Mods
InternalChildren = new Drawable[]
{
background = new Box
new Box
{
RelativeSizeAxes = Axes.Both
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
Content = new FillFlowContainer
{
@ -43,6 +40,7 @@ namespace osu.Game.Overlays.Mods
{
descriptionText = new OsuSpriteText
{
Colour = colourProvider.Content1,
Font = OsuFont.GetFont(weight: FontWeight.Regular),
},
}
@ -50,17 +48,10 @@ namespace osu.Game.Overlays.Mods
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
background.Colour = colours.Gray3;
descriptionText.Colour = colours.BlueLighter;
}
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
private Mod lastMod;
private Mod? lastMod;
public void SetContent(Mod mod)
{

View File

@ -6,6 +6,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osuTK;
@ -17,6 +19,8 @@ namespace osu.Game.Overlays.Mods
private const double transition_duration = 200;
private readonly OsuSpriteText descriptionText;
public ModPresetTooltip(OverlayColourProvider colourProvider)
{
Width = 250;
@ -36,8 +40,16 @@ namespace osu.Game.Overlays.Mods
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(7),
Spacing = new Vector2(7)
Padding = new MarginPadding { Left = 10, Right = 10, Top = 5, Bottom = 5 },
Spacing = new Vector2(7),
Children = new[]
{
descriptionText = new OsuSpriteText
{
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Colour = colourProvider.Content1,
},
}
}
};
}
@ -49,8 +61,12 @@ namespace osu.Game.Overlays.Mods
if (ReferenceEquals(preset, lastPreset))
return;
descriptionText.Text = preset.Description;
lastPreset = preset;
Content.ChildrenEnumerable = preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod));
Content.RemoveAll(d => d is ModPresetRow, true);
Content.AddRange(preset.Mods.AsOrdered().Select(mod => new ModPresetRow(mod)));
}
protected override void PopIn() => this.FadeIn(transition_duration, Easing.OutQuint);

View File

@ -61,6 +61,8 @@ namespace osu.Game.Overlays.SkinEditor
originalRotations = objectsInRotation.ToDictionary(d => d, d => d.Rotation);
originalPositions = objectsInRotation.ToDictionary(d => d, d => d.ToScreenSpace(d.OriginPosition));
defaultOrigin = GeometryUtils.GetSurroundingQuad(objectsInRotation.SelectMany(d => d.ScreenSpaceDrawQuad.GetVertices().ToArray())).Centre;
base.Begin();
}
public override void Update(float rotation, Vector2? origin = null)
@ -99,6 +101,8 @@ namespace osu.Game.Overlays.SkinEditor
originalPositions = null;
originalRotations = null;
defaultOrigin = null;
base.Commit();
}
}
}

View File

@ -110,26 +110,24 @@ namespace osu.Game.Rulesets.Difficulty
var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap);
var difficultyObjects = getDifficultyHitObjects().ToArray();
foreach (var obj in difficultyObjects)
int currentIndex = 0;
foreach (var obj in Beatmap.HitObjects)
{
// Implementations expect the progressive beatmap to only contain top-level objects from the original beatmap.
// At the same time, we also need to consider the possibility DHOs may not be generated for any given object,
// so we'll add all remaining objects up to the current point in time to the progressive beatmap.
for (int i = progressiveBeatmap.HitObjects.Count; i < Beatmap.HitObjects.Count; i++)
{
if (obj != difficultyObjects[^1] && Beatmap.HitObjects[i].StartTime > obj.BaseObject.StartTime)
break;
progressiveBeatmap.HitObjects.Add(obj);
progressiveBeatmap.HitObjects.Add(Beatmap.HitObjects[i]);
while (currentIndex < difficultyObjects.Length && difficultyObjects[currentIndex].BaseObject.GetEndTime() <= obj.GetEndTime())
{
foreach (var skill in skills)
{
cancellationToken.ThrowIfCancellationRequested();
skill.Process(difficultyObjects[currentIndex]);
}
currentIndex++;
}
foreach (var skill in skills)
{
cancellationToken.ThrowIfCancellationRequested();
skill.Process(obj);
}
attribs.Add(new TimedDifficultyAttributes(obj.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
attribs.Add(new TimedDifficultyAttributes(obj.GetEndTime(), CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
}
return attribs;

View File

@ -207,7 +207,7 @@ namespace osu.Game.Rulesets.Edit
{
Child = new EditorToolboxGroup("inspector")
{
Child = new HitObjectInspector()
Child = CreateHitObjectInspector()
},
}
}
@ -329,6 +329,8 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
protected virtual ComposeBlueprintContainer CreateBlueprintContainer() => new ComposeBlueprintContainer(this);
protected virtual Drawable CreateHitObjectInspector() => new HitObjectInspector();
/// <summary>
/// Construct a drawable ruleset for the provided ruleset.
/// </summary>

View File

@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
public void TriggerFailure()
{
if (HasFailed)
return;
if (Failed?.Invoke() != false)
HasFailed = true;
}

View File

@ -381,9 +381,12 @@ namespace osu.Game.Rulesets.Scoring
if (rank.Value == ScoreRank.F)
return;
rank.Value = RankFromScore(Accuracy.Value, ScoreResultCounts);
ScoreRank newRank = RankFromScore(Accuracy.Value, ScoreResultCounts);
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value);
newRank = mod.AdjustRank(newRank, Accuracy.Value);
rank.Value = newRank;
}
protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)

View File

@ -10,7 +10,7 @@ using osu.Game.Overlays;
namespace osu.Game.Screens.Edit.Compose.Components
{
internal partial class EditorInspector : CompositeDrawable
public partial class EditorInspector : CompositeDrawable
{
protected OsuTextFlowContainer InspectorText = null!;

View File

@ -9,7 +9,7 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Screens.Edit.Compose.Components
{
internal partial class HitObjectInspector : EditorInspector
public partial class HitObjectInspector : EditorInspector
{
protected override void LoadComplete()
{
@ -29,6 +29,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
rollingTextUpdate?.Cancel();
rollingTextUpdate = null;
AddInspectorValues();
// I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes.
// This is a good middle-ground for the time being.
if (EditorBeatmap.SelectedHitObjects.Count > 0)
rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
}
protected virtual void AddInspectorValues()
{
switch (EditorBeatmap.SelectedHitObjects.Count)
{
case 0:
@ -90,9 +100,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
AddValue($"{duration.Duration:#,0.##}ms");
}
// I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes.
// This is a good middle-ground for the time being.
rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
break;
default:

View File

@ -67,6 +67,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (rotationHandler == null) return false;
if (rotationHandler.OperationInProgress.Value)
return false;
rotationHandler.Begin();
return true;
}

View File

@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (scaleHandler == null) return false;
if (scaleHandler.OperationInProgress.Value)
return false;
originalAnchor = Anchor;
scaleHandler.Begin();

View File

@ -12,6 +12,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
public partial class SelectionRotationHandler : Component
{
/// <summary>
/// Whether there is any ongoing scale operation right now.
/// </summary>
public Bindable<bool> OperationInProgress { get; private set; } = new BindableBool();
/// <summary>
/// Whether rotation anchored by the selection origin can currently be performed.
/// </summary>
@ -50,6 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </remarks>
public virtual void Begin()
{
OperationInProgress.Value = true;
}
/// <summary>
@ -85,6 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </remarks>
public virtual void Commit()
{
OperationInProgress.Value = false;
}
}
}

View File

@ -13,6 +13,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
public partial class SelectionScaleHandler : Component
{
/// <summary>
/// Whether there is any ongoing scale operation right now.
/// </summary>
public Bindable<bool> OperationInProgress { get; private set; } = new BindableBool();
/// <summary>
/// Whether horizontal scaling (from the left or right edge) support should be enabled.
/// </summary>
@ -63,6 +68,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </remarks>
public virtual void Begin()
{
OperationInProgress.Value = true;
}
/// <summary>
@ -99,6 +105,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </remarks>
public virtual void Commit()
{
OperationInProgress.Value = false;
}
}
}

View File

@ -170,8 +170,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
changeHandler?.BeginChange();
updateState();
double min = beatmap.HitObjects.Last(ho => ho.GetEndTime() <= Break.Value.StartTime).GetEndTime();
double max = beatmap.HitObjects.First(ho => ho.StartTime >= Break.Value.EndTime).StartTime;
double min = beatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() <= Break.Value.StartTime)?.GetEndTime() ?? double.NegativeInfinity;
double max = beatmap.HitObjects.FirstOrDefault(ho => ho.StartTime >= Break.Value.EndTime)?.StartTime ?? double.PositiveInfinity;
if (isStartHandle)
max = Math.Min(max, Break.Value.EndTime - BreakPeriod.MIN_BREAK_DURATION);

View File

@ -292,7 +292,7 @@ namespace osu.Game.Screens.Edit
dependencies.CacheAs<IEditorChangeHandler>(changeHandler);
}
beatDivisor.Value = editorBeatmap.BeatmapInfo.BeatDivisor;
beatDivisor.SetArbitraryDivisor(editorBeatmap.BeatmapInfo.BeatDivisor);
beatDivisor.BindValueChanged(divisor => editorBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue);
updateLastSavedHash();

View File

@ -40,19 +40,30 @@ namespace osu.Game.Screens.Edit
foreach (var manualBreak in Beatmap.Breaks.ToList())
{
if (Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime))
if (manualBreak.EndTime <= Beatmap.HitObjects.FirstOrDefault()?.StartTime
|| manualBreak.StartTime >= Beatmap.GetLastObjectTime()
|| Beatmap.HitObjects.Any(ho => ho.StartTime <= manualBreak.EndTime && ho.GetEndTime() >= manualBreak.StartTime))
{
Beatmap.Breaks.Remove(manualBreak);
}
}
double currentMaxEndTime = double.MinValue;
for (int i = 1; i < Beatmap.HitObjects.Count; ++i)
{
double previousObjectEndTime = Beatmap.HitObjects[i - 1].GetEndTime();
// Keep track of the maximum end time encountered thus far.
// This handles cases like osu!mania's hold notes, which could have concurrent other objects after their start time.
// Note that we're relying on the implicit assumption that objects are sorted by start time,
// which is why similar tracking is not done for start time.
currentMaxEndTime = Math.Max(currentMaxEndTime, Beatmap.HitObjects[i - 1].GetEndTime());
double nextObjectStartTime = Beatmap.HitObjects[i].StartTime;
if (nextObjectStartTime - previousObjectEndTime < BreakPeriod.MIN_GAP_DURATION)
if (nextObjectStartTime - currentMaxEndTime < BreakPeriod.MIN_GAP_DURATION)
continue;
double breakStartTime = previousObjectEndTime + BreakPeriod.GAP_BEFORE_BREAK;
double breakStartTime = currentMaxEndTime + BreakPeriod.GAP_BEFORE_BREAK;
double breakEndTime = nextObjectStartTime - Math.Max(BreakPeriod.GAP_AFTER_BREAK, Beatmap.ControlPointInfo.TimingPointAt(nextObjectStartTime).BeatLength * 2);
if (breakEndTime - breakStartTime < BreakPeriod.MIN_BREAK_DURATION)

View File

@ -1,189 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK.Graphics;
namespace osu.Game.Screens.Edit
{
public abstract partial class EditorTable : TableContainer
{
public event Action<Drawable>? OnRowSelected;
private const float horizontal_inset = 20;
protected const float ROW_HEIGHT = 25;
public const int TEXT_SIZE = 14;
protected readonly FillFlowContainer<RowBackground> BackgroundFlow;
// We can avoid potentially thousands of objects being added to the input sub-tree since item selection is being handled by the BackgroundFlow
// and no items in the underlying table are clickable.
protected override bool ShouldBeConsideredForInput(Drawable child) => child == BackgroundFlow && base.ShouldBeConsideredForInput(child);
protected EditorTable()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Horizontal = horizontal_inset };
RowSize = new Dimension(GridSizeMode.Absolute, ROW_HEIGHT);
AddInternal(BackgroundFlow = new FillFlowContainer<RowBackground>
{
RelativeSizeAxes = Axes.Both,
Depth = 1f,
Padding = new MarginPadding { Horizontal = -horizontal_inset },
Margin = new MarginPadding { Top = ROW_HEIGHT }
});
}
protected int GetIndexForObject(object? item)
{
for (int i = 0; i < BackgroundFlow.Count; i++)
{
if (BackgroundFlow[i].Item == item)
return i;
}
return -1;
}
protected virtual bool SetSelectedRow(object? item)
{
bool foundSelection = false;
foreach (var b in BackgroundFlow)
{
b.Selected = ReferenceEquals(b.Item, item);
if (b.Selected)
{
Debug.Assert(!foundSelection);
OnRowSelected?.Invoke(b);
foundSelection = true;
}
}
return foundSelection;
}
protected object? GetObjectAtIndex(int index)
{
if (index < 0 || index > BackgroundFlow.Count - 1)
return null;
return BackgroundFlow[index].Item;
}
protected override Drawable CreateHeader(int index, TableColumn? column) => new HeaderText(column?.Header ?? default);
private partial class HeaderText : OsuSpriteText
{
public HeaderText(LocalisableString text)
{
Text = text.ToUpper();
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);
}
}
public partial class RowBackground : OsuClickableContainer
{
public readonly object Item;
private const int fade_duration = 100;
private readonly Box hoveredBackground;
public RowBackground(object item)
{
Item = item;
RelativeSizeAxes = Axes.X;
Height = 25;
AlwaysPresent = true;
CornerRadius = 3;
Masking = true;
Children = new Drawable[]
{
hoveredBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
};
}
private Color4 colourHover;
private Color4 colourSelected;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
colourHover = colours.Background1;
colourSelected = colours.Colour3;
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
FinishTransforms(true);
}
private bool selected;
public bool Selected
{
get => selected;
set
{
if (value == selected)
return;
selected = value;
updateState();
}
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
if (selected || IsHovered)
hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
else
hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
}
}
}
}

View File

@ -1,11 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Screens.Play;
using osu.Game.Users;
@ -43,6 +46,10 @@ namespace osu.Game.Screens.Edit.GameplayTest
protected override void LoadComplete()
{
base.LoadComplete();
markPreviousObjectsHit();
markVisibleDrawableObjectsHit();
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
{
if (completed.NewValue)
@ -56,6 +63,69 @@ namespace osu.Game.Screens.Edit.GameplayTest
});
}
private void markPreviousObjectsHit()
{
foreach (var hitObject in enumerateHitObjects(DrawableRuleset.Objects, editorState.Time))
{
var judgement = hitObject.Judgement;
var result = new JudgementResult(hitObject, judgement) { Type = judgement.MaxResult };
HealthProcessor.ApplyResult(result);
ScoreProcessor.ApplyResult(result);
}
static IEnumerable<HitObject> enumerateHitObjects(IEnumerable<HitObject> hitObjects, double cutoffTime)
{
foreach (var hitObject in hitObjects)
{
foreach (var nested in enumerateHitObjects(hitObject.NestedHitObjects, cutoffTime))
{
if (nested.GetEndTime() < cutoffTime)
yield return nested;
}
if (hitObject.GetEndTime() < cutoffTime)
yield return hitObject;
}
}
}
private void markVisibleDrawableObjectsHit()
{
if (!DrawableRuleset.Playfield.IsLoaded)
{
Schedule(markVisibleDrawableObjectsHit);
return;
}
foreach (var drawableObjectEntry in enumerateDrawableEntries(
DrawableRuleset.Playfield.AllHitObjects
.Select(ho => ho.Entry)
.Where(e => e != null)
.Cast<HitObjectLifetimeEntry>(), editorState.Time))
{
drawableObjectEntry.Result = new JudgementResult(drawableObjectEntry.HitObject, drawableObjectEntry.HitObject.Judgement)
{
Type = drawableObjectEntry.HitObject.Judgement.MaxResult
};
}
static IEnumerable<HitObjectLifetimeEntry> enumerateDrawableEntries(IEnumerable<HitObjectLifetimeEntry> entries, double cutoffTime)
{
foreach (var entry in entries)
{
foreach (var nested in enumerateDrawableEntries(entry.NestedEntries, cutoffTime))
{
if (nested.HitObject.GetEndTime() < cutoffTime)
yield return nested;
}
if (entry.HitObject.GetEndTime() < cutoffTime)
yield return entry;
}
}
}
protected override void PrepareReplay()
{
// don't record replays.

View File

@ -0,0 +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 osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Screens.Edit
{
public partial class TableHeaderText : OsuSpriteText
{
public TableHeaderText(LocalisableString text)
{
Text = text.ToUpper();
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);
}
}
}

View File

@ -7,10 +7,8 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
@ -21,12 +19,8 @@ namespace osu.Game.Screens.Edit.Timing
public partial class ControlPointList : CompositeDrawable
{
private OsuButton deleteButton = null!;
private ControlPointTable table = null!;
private OsuScrollContainer scroll = null!;
private RoundedButton addButton = null!;
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
[Resolved]
private EditorClock clock { get; set; } = null!;
@ -36,9 +30,6 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
@ -47,21 +38,10 @@ namespace osu.Game.Screens.Edit.Timing
const float margins = 10;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Background4,
RelativeSizeAxes = Axes.Both,
},
new Box
{
Colour = colours.Background3,
RelativeSizeAxes = Axes.Y,
Width = ControlPointTable.TIMING_COLUMN_WIDTH + margins,
},
scroll = new OsuScrollContainer
new ControlPointTable
{
RelativeSizeAxes = Axes.Both,
Child = table = new ControlPointTable(),
Groups = { BindTarget = Beatmap.ControlPointInfo.Groups, },
},
new FillFlowContainer
{
@ -105,20 +85,6 @@ namespace osu.Game.Screens.Edit.Timing
? "+ Clone to current time"
: "+ Add at current time";
}, true);
controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((_, _) =>
{
// This callback can happen many times in a change operation. It gets expensive.
// We really should be handling the `CollectionChanged` event properly.
Scheduler.AddOnce(() =>
{
table.ControlGroups = controlPointGroups;
changeHandler?.SaveState();
});
}, true);
table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable);
}
protected override bool OnClick(ClickEvent e)

View File

@ -2,149 +2,281 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Timing.RowAttributes;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
public partial class ControlPointTable : EditorTable
public partial class ControlPointTable : CompositeDrawable
{
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
public BindableList<ControlPointGroup> Groups { get; } = new BindableList<ControlPointGroup>();
[Resolved]
private EditorClock clock { get; set; } = null!;
private const float timing_column_width = 300;
private const float row_height = 25;
private const float row_horizontal_padding = 20;
public const float TIMING_COLUMN_WIDTH = 300;
public IEnumerable<ControlPointGroup> ControlGroups
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
set
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
int selectedIndex = GetIndexForObject(selectedGroup.Value);
Content = null;
BackgroundFlow.Clear();
if (!value.Any())
return;
foreach (var group in value)
new Box
{
BackgroundFlow.Add(new RowBackground(group)
Colour = colours.Background4,
RelativeSizeAxes = Axes.Both,
},
new Box
{
Colour = colours.Background3,
RelativeSizeAxes = Axes.Y,
Width = timing_column_width + 10,
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = row_height,
Padding = new MarginPadding { Horizontal = row_horizontal_padding },
Children = new Drawable[]
{
// schedule to give time for any modified focused text box to lose focus and commit changes (e.g. BPM / time signature textboxes) before switching to new point.
Action = () => Schedule(() =>
new TableHeaderText("Time")
{
SetSelectedRow(group);
clock.SeekSmoothlyTo(group.Time);
})
});
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new TableHeaderText("Attributes")
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = ControlPointTable.timing_column_width }
},
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = row_height },
Child = new ControlPointRowList
{
RelativeSizeAxes = Axes.Both,
RowData = { BindTarget = Groups, },
},
},
};
}
Columns = createHeaders();
Content = value.Select(createContent).ToArray().ToRectangular();
private partial class ControlPointRowList : VirtualisedListContainer<ControlPointGroup, DrawableControlGroup>
{
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
// Attempt to retain selection.
if (SetSelectedRow(selectedGroup.Value))
return;
public ControlPointRowList()
: base(row_height, 50)
{
}
// Some operations completely obliterate references, so best-effort reselect based on index.
if (SetSelectedRow(GetObjectAtIndex(selectedIndex)))
return;
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
// Selection could not be retained.
selectedGroup.Value = null;
protected override void LoadComplete()
{
base.LoadComplete();
selectedGroup.BindValueChanged(val =>
{
// can't use `.ScrollIntoView()` here because of the list virtualisation not giving
// child items valid coordinates from the start, so ballpark something similar
// using estimated row height.
var row = Items.FlowingChildren.SingleOrDefault(item => item.Row.Equals(val.NewValue));
if (row == null)
return;
float minPos = Items.GetLayoutPosition(row) * row_height;
float maxPos = minPos + row_height;
if (minPos < Scroll.Current)
Scroll.ScrollTo(minPos);
else if (maxPos > Scroll.Current + Scroll.DisplayableContent)
Scroll.ScrollTo(maxPos - Scroll.DisplayableContent);
});
}
}
protected override void LoadComplete()
public partial class DrawableControlGroup : PoolableDrawable, IHasCurrentValue<ControlPointGroup>
{
base.LoadComplete();
// Handle external selections.
selectedGroup.BindValueChanged(g => SetSelectedRow(g.NewValue), true);
}
protected override bool SetSelectedRow(object? item)
{
if (!base.SetSelectedRow(item))
return false;
selectedGroup.Value = item as ControlPointGroup;
return true;
}
private TableColumn[] createHeaders()
{
var columns = new List<TableColumn>
public Bindable<ControlPointGroup> Current
{
new TableColumn("Time", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, TIMING_COLUMN_WIDTH)),
new TableColumn("Attributes", Anchor.CentreLeft),
};
get => current.Current;
set => current.Current = value;
}
return columns.ToArray();
}
private readonly BindableWithCurrent<ControlPointGroup> current = new BindableWithCurrent<ControlPointGroup>();
private Drawable[] createContent(ControlPointGroup group)
{
return new Drawable[]
private Box background = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
[Resolved]
private EditorClock editorClock { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
new ControlGroupTiming(group),
new ControlGroupAttributes(group, c => c is not TimingControlPoint)
};
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background1,
Alpha = 0,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = row_horizontal_padding, },
Children = new Drawable[]
{
new ControlGroupTiming { Group = { BindTarget = current }, },
new ControlGroupAttributes(point => point is not TimingControlPoint)
{
Group = { BindTarget = current },
Margin = new MarginPadding { Left = timing_column_width }
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedGroup.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
protected override void PrepareForUse()
{
base.PrepareForUse();
updateState();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
protected override bool OnClick(ClickEvent e)
{
// schedule to give time for any modified focused text box to lose focus and commit changes (e.g. BPM / time signature textboxes) before switching to new point.
var currentGroup = Current.Value;
Schedule(() =>
{
selectedGroup.Value = currentGroup;
editorClock.SeekSmoothlyTo(currentGroup.Time);
});
return true;
}
private void updateState()
{
bool isSelected = selectedGroup.Value?.Equals(current.Value) == true;
if (IsHovered || isSelected)
background.FadeIn(100, Easing.OutQuint);
else
background.FadeOut(100, Easing.OutQuint);
background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1;
}
}
private partial class ControlGroupTiming : FillFlowContainer
{
public ControlGroupTiming(ControlPointGroup group)
public Bindable<ControlPointGroup> Group { get; } = new Bindable<ControlPointGroup>();
private OsuSpriteText timeText = null!;
[BackgroundDependencyLoader]
private void load()
{
Name = @"ControlGroupTiming";
RelativeSizeAxes = Axes.Y;
Width = TIMING_COLUMN_WIDTH;
Width = timing_column_width;
Spacing = new Vector2(5);
Children = new Drawable[]
{
new OsuSpriteText
timeText = new OsuSpriteText
{
Text = group.Time.ToEditorFormattedString(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold),
Width = 70,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new ControlGroupAttributes(group, c => c is TimingControlPoint)
new ControlGroupAttributes(c => c is TimingControlPoint)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Group = { BindTarget = Group },
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Group.BindValueChanged(_ => timeText.Text = Group.Value?.Time.ToEditorFormattedString() ?? default(LocalisableString), true);
}
}
private partial class ControlGroupAttributes : CompositeDrawable
{
public Bindable<ControlPointGroup> Group { get; } = new Bindable<ControlPointGroup>();
private BindableList<ControlPoint> controlPoints { get; } = new BindableList<ControlPoint>();
private readonly Func<ControlPoint, bool> matchFunction;
private readonly IBindableList<ControlPoint> controlPoints = new BindableList<ControlPoint>();
private FillFlowContainer fill = null!;
private readonly FillFlowContainer fill;
public ControlGroupAttributes(ControlPointGroup group, Func<ControlPoint, bool> matchFunction)
public ControlGroupAttributes(Func<ControlPoint, bool> matchFunction)
{
this.matchFunction = matchFunction;
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
Name = @"ControlGroupAttributes";
@ -156,20 +288,21 @@ namespace osu.Game.Screens.Edit.Timing
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2)
};
controlPoints.BindTo(group.ControlPoints);
}
[BackgroundDependencyLoader]
private void load()
{
createChildren();
}
protected override void LoadComplete()
{
base.LoadComplete();
controlPoints.CollectionChanged += (_, _) => createChildren();
Group.BindValueChanged(_ =>
{
controlPoints.UnbindBindings();
controlPoints.Clear();
if (Group.Value != null)
((IBindableList<ControlPoint>)controlPoints).BindTo(Group.Value.ControlPoints);
}, true);
controlPoints.BindCollectionChanged((_, _) => createChildren(), true);
}
private void createChildren()

View File

@ -36,6 +36,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
BackgroundColour = overlayColours.Background6;
FillColour = controlPoint.GetRepresentingColour(colours);
FinishTransforms(true);
}
}
}

View File

@ -11,7 +11,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit;
@ -56,10 +55,9 @@ namespace osu.Game.Screens.Edit.Verify
Colour = colours.Background3,
RelativeSizeAxes = Axes.Both,
},
new OsuScrollContainer
table = new IssueTable
{
RelativeSizeAxes = Axes.Both,
Child = table = new IssueTable(),
},
new FillFlowContainer
{
@ -101,9 +99,10 @@ namespace osu.Game.Screens.Edit.Verify
issues = filter(issues);
table.Issues = issues
.OrderBy(issue => issue.Template.Type)
.ThenBy(issue => issue.Check.Metadata.Category);
table.Issues.Clear();
table.Issues.AddRange(issues
.OrderBy(issue => issue.Template.Type)
.ThenBy(issue => issue.Check.Metadata.Category));
}
private IEnumerable<Issue> filter(IEnumerable<Issue> issues)

View File

@ -1,132 +1,239 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Rulesets.Edit.Checks.Components;
namespace osu.Game.Screens.Edit.Verify
{
public partial class IssueTable : EditorTable
public partial class IssueTable : CompositeDrawable
{
private Bindable<Issue> selectedIssue = null!;
public BindableList<Issue> Issues { get; } = new BindableList<Issue>();
[Resolved]
private VerifyScreen verify { get; set; } = null!;
public const float COLUMN_WIDTH = 70;
public const float COLUMN_GAP = 10;
public const float ROW_HEIGHT = 25;
public const float ROW_HORIZONTAL_PADDING = 20;
public const int TEXT_SIZE = 14;
[Resolved]
private EditorClock clock { get; set; } = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private Editor editor { get; set; } = null!;
public IEnumerable<Issue> Issues
[BackgroundDependencyLoader]
private void load()
{
set
InternalChildren = new Drawable[]
{
Content = null;
BackgroundFlow.Clear();
if (!value.Any())
return;
foreach (var issue in value)
new Container
{
BackgroundFlow.Add(new RowBackground(issue)
RelativeSizeAxes = Axes.X,
Height = ROW_HEIGHT,
Padding = new MarginPadding { Horizontal = ROW_HORIZONTAL_PADDING, },
Children = new[]
{
Action = () =>
new TableHeaderText("Type")
{
selectedIssue.Value = issue;
if (issue.Time != null)
{
clock.Seek(issue.Time.Value);
editor.OnPressed(new KeyBindingPressEvent<GlobalAction>(GetContainingInputManager()!.CurrentState, GlobalAction.EditorComposeMode));
}
if (!issue.HitObjects.Any())
return;
editorBeatmap.SelectedHitObjects.Clear();
editorBeatmap.SelectedHitObjects.AddRange(issue.HitObjects);
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
});
new TableHeaderText("Time")
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = COLUMN_WIDTH + COLUMN_GAP },
},
new TableHeaderText("Message")
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 2 * (COLUMN_WIDTH + COLUMN_GAP) },
},
new TableHeaderText("Category")
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = ROW_HEIGHT, },
Child = new IssueRowList
{
RelativeSizeAxes = Axes.Both,
RowData = { BindTarget = Issues }
}
}
};
}
private partial class IssueRowList : VirtualisedListContainer<Issue, DrawableIssue>
{
public IssueRowList()
: base(ROW_HEIGHT, 50)
{
}
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
}
public partial class DrawableIssue : PoolableDrawable, IHasCurrentValue<Issue>
{
private readonly BindableWithCurrent<Issue> current = new BindableWithCurrent<Issue>();
private readonly Bindable<Issue> selectedIssue = new Bindable<Issue>();
private Box background = null!;
private OsuSpriteText issueTypeText = null!;
private OsuSpriteText issueTimestampText = null!;
private OsuSpriteText issueDetailText = null!;
private OsuSpriteText issueCategoryText = null!;
[Resolved]
private EditorClock clock { get; set; } = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private Editor editor { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public Bindable<Issue> Current
{
get => current.Current;
set => current.Current = value;
}
[BackgroundDependencyLoader]
private void load(VerifyScreen verify)
{
RelativeSizeAxes = Axes.X;
Height = ROW_HEIGHT;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 20, },
Children = new Drawable[]
{
issueTypeText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
},
issueTimestampText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Margin = new MarginPadding { Left = COLUMN_WIDTH + COLUMN_GAP },
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Left = 2 * (COLUMN_GAP + COLUMN_WIDTH),
Right = COLUMN_GAP + COLUMN_WIDTH,
},
Child = issueDetailText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium)
},
},
issueCategoryText = new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
}
}
}
};
selectedIssue.BindTo(verify.SelectedIssue);
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedIssue.BindValueChanged(_ => updateState());
Current.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
protected override bool OnClick(ClickEvent e)
{
selectedIssue.Value = current.Value;
if (current.Value.Time != null)
{
clock.Seek(current.Value.Time.Value);
editor.OnPressed(new KeyBindingPressEvent<GlobalAction>(GetContainingInputManager()!.CurrentState, GlobalAction.EditorComposeMode));
}
Columns = createHeaders();
Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular();
if (current.Value.HitObjects.Any())
{
editorBeatmap.SelectedHitObjects.Clear();
editorBeatmap.SelectedHitObjects.AddRange(current.Value.HitObjects);
}
return true;
}
private void updateState()
{
issueTypeText.Text = Current.Value.Template.Type.ToString();
issueTypeText.Colour = Current.Value.Template.Colour;
issueTimestampText.Text = Current.Value.GetEditorTimestamp();
issueDetailText.Text = Current.Value.ToString();
issueCategoryText.Text = Current.Value.Check.Metadata.Category.ToString();
bool isSelected = selectedIssue.Value == current.Value;
if (IsHovered || isSelected)
background.FadeIn(100, Easing.OutQuint);
else
background.FadeOut(100, Easing.OutQuint);
background.Colour = isSelected ? colourProvider.Colour3 : colourProvider.Background1;
}
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedIssue = verify.SelectedIssue.GetBoundCopy();
selectedIssue.BindValueChanged(issue =>
{
SetSelectedRow(issue.NewValue);
}, true);
}
private TableColumn[] createHeaders()
{
var columns = new List<TableColumn>
{
new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)),
new TableColumn("Type", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),
new TableColumn("Time", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),
new TableColumn("Message", Anchor.CentreLeft),
new TableColumn("Category", Anchor.CentreRight, new Dimension(GridSizeMode.AutoSize)),
};
return columns.ToArray();
}
private Drawable[] createContent(int index, Issue issue) => new Drawable[]
{
new OsuSpriteText
{
Text = $"#{index + 1}",
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),
Margin = new MarginPadding { Right = 10 }
},
new OsuSpriteText
{
Text = issue.Template.Type.ToString(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Margin = new MarginPadding { Right = 10 },
Colour = issue.Template.Colour
},
new OsuSpriteText
{
Text = issue.GetEditorTimestamp(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Margin = new MarginPadding { Right = 10 },
},
new OsuSpriteText
{
Text = issue.ToString(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium)
},
new OsuSpriteText
{
Text = issue.Check.Metadata.Category.ToString(),
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
Margin = new MarginPadding(10)
}
};
}
}

View File

@ -1,12 +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 osu.Framework.Allocation;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
namespace osu.Game.Screens.OnlinePlay.Components
{
public partial class OverlinedPlaylistHeader : OverlinedHeader
{
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
public OverlinedPlaylistHeader()
: base("Playlist")
{
@ -16,7 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
base.LoadComplete();
Playlist.BindCollectionChanged((_, _) => Details.Value = Playlist.GetTotalDuration(), true);
Playlist.BindCollectionChanged((_, _) => Details.Value = Playlist.GetTotalDuration(rulesets), true);
}
}
}

View File

@ -0,0 +1,234 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
public partial class DailyChallengeCarousel : Container
{
private const int switch_interval = 20_500;
private readonly Container content;
private readonly FillFlowContainer<NavigationDot> navigationFlow;
protected override Container<Drawable> Content => content;
private double clockStartTime;
private int lastDisplayed = -1;
public DailyChallengeCarousel()
{
InternalChildren = new Drawable[]
{
content = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 40 },
},
navigationFlow = new FillFlowContainer<NavigationDot>
{
AutoSizeAxes = Axes.X,
Height = 15,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Spacing = new Vector2(10),
}
};
}
public override void Add(Drawable drawable)
{
drawable.RelativeSizeAxes = Axes.Both;
drawable.Size = Vector2.One;
drawable.AlwaysPresent = true;
drawable.Alpha = 0;
base.Add(drawable);
navigationFlow.Add(new NavigationDot { Clicked = onManualNavigation });
}
public override bool Remove(Drawable drawable, bool disposeImmediately)
{
int index = content.IndexOf(drawable);
if (index > 0)
navigationFlow.Remove(navigationFlow[index], true);
return base.Remove(drawable, disposeImmediately);
}
protected override void LoadComplete()
{
base.LoadComplete();
clockStartTime = Clock.CurrentTime;
}
protected override void Update()
{
base.Update();
if (content.Count == 0)
{
lastDisplayed = -1;
return;
}
double elapsed = Clock.CurrentTime - clockStartTime;
int currentDisplay = (int)(elapsed / switch_interval) % content.Count;
double displayProgress = (elapsed % switch_interval) / switch_interval;
navigationFlow[currentDisplay].Active.Value = true;
if (content.Count > 1)
navigationFlow[currentDisplay].Progress = (float)displayProgress;
if (currentDisplay == lastDisplayed)
return;
if (lastDisplayed >= 0)
{
content[lastDisplayed].FadeOutFromOne(250, Easing.OutQuint);
navigationFlow[lastDisplayed].Active.Value = false;
}
content[currentDisplay].Delay(250).Then().FadeInFromZero(250, Easing.OutQuint);
lastDisplayed = currentDisplay;
}
private void onManualNavigation(NavigationDot dot)
{
int index = navigationFlow.IndexOf(dot);
if (index < 0)
return;
clockStartTime = Clock.CurrentTime - index * switch_interval;
}
private partial class NavigationDot : CompositeDrawable
{
public required Action<NavigationDot> Clicked { get; init; }
public BindableBool Active { get; } = new BindableBool();
private double progress;
public float Progress
{
set
{
if (progress == value)
return;
progress = value;
progressLayer.Width = value;
}
}
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Box background = null!;
private Box progressLayer = null!;
private Box hoverLayer = null!;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(15);
InternalChildren = new Drawable[]
{
new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Light4,
},
progressLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Width = 0,
Colour = colourProvider.Highlight1,
Blending = BlendingParameters.Additive,
Alpha = 0,
},
hoverLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.White,
Blending = BlendingParameters.Additive,
Alpha = 0,
}
}
},
new HoverClickSounds()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(val =>
{
if (val.NewValue)
{
background.FadeColour(colourProvider.Highlight1, 250, Easing.OutQuint);
this.ResizeWidthTo(30, 250, Easing.OutQuint);
progressLayer.Width = 0;
progressLayer.Alpha = 0.5f;
}
else
{
background.FadeColour(colourProvider.Light4, 250, Easing.OutQuint);
this.ResizeWidthTo(15, 250, Easing.OutQuint);
progressLayer.FadeOut(250, Easing.OutQuint);
}
}, true);
}
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
hoverLayer.FadeTo(0.2f, 250, Easing.OutQuint);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverLayer.FadeOut(250, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override bool OnClick(ClickEvent e)
{
Clicked(this);
hoverLayer.FadeTo(1)
.Then().FadeTo(IsHovered ? 0.2f : 0, 250, Easing.OutQuint);
return true;
}
}
}
}

View File

@ -0,0 +1,194 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Metadata;
using osu.Game.Overlays;
using osu.Game.Scoring;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
public partial class DailyChallengeScoreBreakdown : CompositeDrawable
{
private FillFlowContainer<Bar> barsContainer = null!;
private const int bin_count = MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS;
private long[] bins = new long[bin_count];
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new SectionHeader("Score breakdown"),
barsContainer = new FillFlowContainer<Bar>
{
Direction = FillDirection.Horizontal,
RelativeSizeAxes = Axes.Both,
Height = 0.9f,
Padding = new MarginPadding { Top = 35 },
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
}
};
for (int i = 0; i < bin_count; ++i)
{
LocalisableString? label = null;
switch (i)
{
case 2:
case 4:
case 6:
case 8:
label = @$"{100 * i}k";
break;
case 10:
label = @"1M";
break;
}
barsContainer.Add(new Bar(label)
{
Width = 1f / bin_count,
});
}
}
public void AddNewScore(IScoreInfo scoreInfo)
{
int targetBin = (int)Math.Clamp(Math.Floor((float)scoreInfo.TotalScore / 100000), 0, bin_count - 1);
bins[targetBin] += 1;
updateCounts();
var text = new OsuSpriteText
{
Text = scoreInfo.TotalScore.ToString(@"N0"),
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Default.With(size: 30),
RelativePositionAxes = Axes.X,
X = (targetBin + 0.5f) / bin_count - 0.5f,
Alpha = 0,
};
AddInternal(text);
Scheduler.AddDelayed(() =>
{
float startY = ToLocalSpace(barsContainer[targetBin].CircularBar.ScreenSpaceDrawQuad.TopLeft).Y;
text.FadeInFromZero()
.ScaleTo(new Vector2(0.8f), 500, Easing.OutElasticHalf)
.MoveToY(startY)
.MoveToOffset(new Vector2(0, -50), 2500, Easing.OutQuint)
.FadeOut(2500, Easing.OutQuint)
.Expire();
}, 150);
}
public void SetInitialCounts(long[] counts)
{
if (counts.Length != bin_count)
throw new ArgumentException(@"Incorrect number of bins.", nameof(counts));
bins = counts;
updateCounts();
}
private void updateCounts()
{
long max = bins.Max();
for (int i = 0; i < bin_count; ++i)
barsContainer[i].UpdateCounts(bins[i], max);
}
private partial class Bar : CompositeDrawable
{
private readonly LocalisableString? label;
private long count;
private long max;
public Container CircularBar { get; private set; } = null!;
public Bar(LocalisableString? label = null)
{
this.label = label;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Both;
AddInternal(new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Bottom = 20,
Horizontal = 3,
},
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Masking = true,
Child = CircularBar = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Height = 0.01f,
Masking = true,
CornerRadius = 10,
Colour = colourProvider.Highlight1,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
}
}
});
if (label != null)
{
AddInternal(new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomCentre,
Text = label.Value,
Colour = colourProvider.Content2,
});
}
}
protected override void Update()
{
base.Update();
CircularBar.CornerRadius = Math.Min(CircularBar.DrawHeight / 2, CircularBar.DrawWidth / 4);
}
public void UpdateCounts(long newCount, long newMax)
{
bool isIncrement = newCount > count;
count = newCount;
max = newMax;
CircularBar.ResizeHeightTo(0.01f + 0.99f * count / max, 300, Easing.OutQuint);
if (isIncrement)
CircularBar.FlashColour(Colour4.White, 600, Easing.OutQuint);
}
}
}
}

View File

@ -24,6 +24,7 @@ using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osuTK;
using osu.Game.Localisation;
using osu.Game.Rulesets;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
@ -78,6 +79,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
private IBindable<APIUser> localUser = null!;
private readonly Room room;
@ -366,7 +370,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
public void SelectBeatmap() => editPlaylistButton.TriggerClick();
private void onPlaylistChanged(object? sender, NotifyCollectionChangedEventArgs e) =>
playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}";
playlistLength.Text = $"Length: {Playlist.GetTotalDuration(rulesets)}";
private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0
&& hasValidDuration;

View File

@ -895,7 +895,6 @@ namespace osu.Game.Screens.Select
{
var panel = setPool.Get(p => p.Item = item);
panel.Depth = item.CarouselYPosition;
panel.Y = item.CarouselYPosition;
Scroll.Add(panel);
@ -915,6 +914,8 @@ namespace osu.Game.Screens.Select
{
bool isSelected = item.Item.State.Value == CarouselItemState.Selected;
bool hasPassedSelection = item.Item.CarouselYPosition < selectedBeatmapSet?.CarouselYPosition;
// Cheap way of doing animations when entering / exiting song select.
const double half_time = 50;
const float panel_x_offset_when_inactive = 200;
@ -929,6 +930,8 @@ namespace osu.Game.Screens.Select
item.Alpha = (float)Interpolation.DampContinuously(item.Alpha, 0, half_time, Clock.ElapsedFrameTime);
item.X = (float)Interpolation.DampContinuously(item.X, panel_x_offset_when_inactive, half_time, Clock.ElapsedFrameTime);
}
Scroll.ChangeChildDepth(item, hasPassedSelection ? -item.Item.CarouselYPosition : item.Item.CarouselYPosition);
}
if (item is DrawableCarouselBeatmapSet set)
@ -1000,8 +1003,6 @@ namespace osu.Game.Screens.Select
return set;
}
private const float panel_padding = 5;
/// <summary>
/// Computes the target Y positions for every item in the carousel.
/// </summary>
@ -1023,10 +1024,18 @@ namespace osu.Game.Screens.Select
{
case CarouselBeatmapSet set:
{
bool isSelected = item.State.Value == CarouselItemState.Selected;
float padding = isSelected ? 5 : -5;
if (isSelected)
// double padding because we want to cancel the negative padding from the last item.
currentY += padding * 2;
visibleItems.Add(set);
set.CarouselYPosition = currentY;
if (item.State.Value == CarouselItemState.Selected)
if (isSelected)
{
// scroll position at currentY makes the set panel appear at the very top of the carousel's screen space
// move down by half of visible height (height of the carousel's visible extent, including semi-transparent areas)
@ -1048,7 +1057,7 @@ namespace osu.Game.Screens.Select
}
}
currentY += set.TotalHeight + panel_padding;
currentY += set.TotalHeight + padding;
break;
}
}

View File

@ -144,9 +144,9 @@ namespace osu.Game.Screens.Select.Carousel
}
if (!Item.Visible)
this.FadeOut(300, Easing.OutQuint);
this.FadeOut(100, Easing.OutQuint);
else
this.FadeIn(250);
this.FadeIn(400, Easing.OutQuint);
}
protected virtual void Selected()

View File

@ -59,8 +59,8 @@ namespace osu.Game.Screens.Select
{
SelectedColour = colours.Yellow;
DeselectedColour = SelectedColour.Opacity(0.5f);
lowMultiplierColour = colours.Red;
highMultiplierColour = colours.Green;
lowMultiplierColour = colours.Green;
highMultiplierColour = colours.Red;
Text = @"mods";
Hotkey = GlobalAction.ToggleModSelection;

View File

@ -86,5 +86,10 @@ namespace osu.Game.Tests.Visual.Metadata
dailyChallengeInfo.Value = info;
return Task.CompletedTask;
}
public override Task<MultiplayerPlaylistItemStats[]> BeginWatchingMultiplayerRoom(long id)
=> Task.FromResult(new MultiplayerPlaylistItemStats[MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS]);
public override Task EndWatchingMultiplayerRoom(long id) => Task.CompletedTask;
}
}

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.618.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.627.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.622.0" />
<PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.618.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.627.0" />
</ItemGroup>
</Project>