1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 11:35:35 +08:00

Merge branch 'master' into abstract-hub-connection

This commit is contained in:
Dan Balasescu 2021-02-15 13:53:54 +09:00 committed by GitHub
commit 34e600464b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 1769 additions and 272 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.128.0" />
</ItemGroup>
</Project>

View File

@ -18,6 +18,7 @@ using osu.Framework.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Updater;
using osu.Desktop.Windows;
using osu.Game.IO;
namespace osu.Desktop
{
@ -32,7 +33,7 @@ namespace osu.Desktop
noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false;
}
public override Storage GetStorageForStableInstall()
public override StableStorage GetStorageForStableInstall()
{
try
{
@ -40,7 +41,7 @@ namespace osu.Desktop
{
string stablePath = getStableInstallPath();
if (!string.IsNullOrEmpty(stablePath))
return new DesktopStorage(stablePath, desktopHost);
return new StableStorage(stablePath, desktopHost);
}
}
catch (Exception)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay<CatchHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap) => new Score
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModCinema : ModCinema<CatchHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap) => new Score
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloat
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloat
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
CircleSize.MaxValue = extended ? 11 : 10;
ApproachRate.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModAutoplay : ModAutoplay<ManiaHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap) => new Score
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModCinema : ModCinema<ManiaHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap) => new Score
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),

View File

@ -0,0 +1,65 @@
// 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.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAutoplay : OsuModTestScene
{
[Test]
public void TestSpmUnaffectedByRateAdjust()
=> runSpmTest(new OsuModDaycore
{
SpeedChange = { Value = 0.88 }
});
[Test]
public void TestSpmUnaffectedByTimeRamp()
=> runSpmTest(new ModWindUp
{
InitialRate = { Value = 0.7 },
FinalRate = { Value = 1.3 }
});
private void runSpmTest(Mod mod)
{
SpinnerSpmCounter spmCounter = null;
CreateModTest(new ModTestData
{
Autoplay = true,
Mod = mod,
Beatmap = new Beatmap
{
HitObjects =
{
new Spinner
{
Duration = 2000,
Position = OsuPlayfield.BASE_SIZE / 2
}
}
},
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
});
AddUntilStep("fetch SPM counter", () =>
{
spmCounter = this.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
return spmCounter != null;
});
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
}
}
}

View File

@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
@ -65,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestAutoMod : OsuModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap) => new Score
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
Replay = new MissingAutoGenerator(beatmap).Generate()
Replay = new MissingAutoGenerator(beatmap, mods).Generate()
};
}
@ -76,8 +78,8 @@ namespace osu.Game.Rulesets.Osu.Tests
{
public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap;
public MissingAutoGenerator(IBeatmap beatmap)
: base(beatmap)
public MissingAutoGenerator(IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(beatmap, mods)
{
}

View File

@ -0,0 +1,491 @@
// 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 NUnit.Framework;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
{
private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
private const double late_miss_window = 500; // time after +500 is considered a miss
/// <summary>
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
/// </summary>
[Test]
public void TestClickSecondCircleBeforeFirstCircleTime()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
}
/// <summary>
/// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged.
/// </summary>
[Test]
public void TestClickSecondCircleAtFirstCircleTime()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
}
/// <summary>
/// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged.
/// </summary>
[Test]
public void TestClickSecondCircleAfterFirstCircleTime()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
}
/// <summary>
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged.
/// </summary>
[Test]
public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
}
/// <summary>
/// Tests clicking a future circle after the first circle's start time, while the first circle HAS been judged.
/// </summary>
[Test]
public void TestClickSecondCircleAfterFirstCircleTimeWithFirstCircleJudged()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
}
/// <summary>
/// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
/// </summary>
[Test]
public void TestMissSliderHeadAndHitAllSliderTicks()
{
const double time_slider = 1500;
const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
new TestSlider
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
}
/// <summary>
/// Tests clicking hitting future slider ticks before a circle.
/// </summary>
[Test]
public void TestHitSliderTicksBeforeCircle()
{
const double time_slider = 1500;
const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(30);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
new TestSlider
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
}
/// <summary>
/// Tests clicking a future circle before a spinner.
/// </summary>
[Test]
public void TestHitCircleBeforeSpinner()
{
const double time_spinner = 1500;
const double time_circle = 1800;
Vector2 positionCircle = Vector2.Zero;
var hitObjects = new List<OsuHitObject>
{
new TestSpinner
{
StartTime = time_spinner,
Position = new Vector2(256, 192),
EndTime = time_spinner + 1000,
},
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
}
[Test]
public void TestHitSliderHeadBeforeHitCircle()
{
const double time_circle = 1000;
const double time_slider = 1200;
Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
new TestSlider
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
}
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
}
private void addJudgementAssert(string name, Func<OsuHitObject> hitObject, HitResult result)
{
AddAssert($"{name} judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
}
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
}
private ScoreAccessibleReplayPlayer currentPlayer;
private List<JudgementResult> judgementResults;
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{
HitObjects = hitObjects,
BeatmapInfo =
{
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
Ruleset = new OsuRuleset().RulesetInfo
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
SelectedMods.Value = new[] { new OsuModClassic() };
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private class TestHitCircle : HitCircle
{
protected override HitWindows CreateHitWindows() => new TestHitWindows();
}
private class TestSlider : Slider
{
public TestSlider()
{
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows();
HeadCircle.HitWindows.SetDifficulty(0);
TailCircle.HitWindows.SetDifficulty(0);
};
}
}
private class TestSpinner : Spinner
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = 1;
}
}
private class TestHitWindows : HitWindows
{
private static readonly DifficultyRange[] ranges =
{
new DifficultyRange(HitResult.Great, 500, 500, 500),
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
};
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
protected override DifficultyRange[] GetRanges() => ranges;
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.Update();
CirclePiece.UpdateFrom(position == SliderPosition.Start ? HitObject.HeadCircle : HitObject.TailCircle);
CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)HitObject.HeadCircle : HitObject.TailCircle);
}
// Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input.

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Judgements
{
public class SliderTickJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
}

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
inputManager.AllowUserCursorMovement = false;
// Generate the replay frames the cursor should follow
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap).Generate().Frames.Cast<OsuReplayFrame>().ToList();
replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast<OsuReplayFrame>().ToList();
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -16,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap) => new Score
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap).Generate()
Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -16,10 +17,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap) => new Score
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap).Generate()
Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
}
}

View File

@ -0,0 +1,86 @@
// 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.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
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.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Classic";
public override string Acronym => "CL";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.History;
public override string Description => "Feeling nostalgic?";
public override bool Ranked => false;
public override ModType Type => ModType.Conversion;
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
[SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")]
public Bindable<bool> NoSliderHeadMovement { get; } = new BindableBool(true);
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
[SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")]
public Bindable<bool> FixedFollowCircleHitArea { get; } = new BindableBool(true);
public void ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Slider slider:
slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value;
foreach (var head in slider.NestedHitObjects.OfType<SliderHeadCircle>())
head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value;
break;
}
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
if (ClassicNoteLock.Value)
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
}
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var obj in drawables)
{
switch (obj)
{
case DrawableSlider slider:
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
break;
case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
break;
}
}
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloat
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloat
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Osu.Mods
Value = 5,
};
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
CircleSize.MaxValue = extended ? 11 : 10;
ApproachRate.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get

View File

@ -110,8 +110,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
double startTime = start.GetEndTime();
double duration = end.StartTime - startTime;
// Preempt time can go below 800ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear preempt function (see: OsuHitObject).
// Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good.
double preempt = PREEMPT * Math.Min(1, start.TimePreempt / OsuHitObject.PREEMPT_MIN);
fadeOutTime = startTime + fraction * duration;
fadeInTime = fadeOutTime - PREEMPT;
fadeInTime = fadeOutTime - preempt;
}
}
}

View File

@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
}
var result = HitObject.HitWindows.ResultFor(timeOffset);
var result = ResultFor(timeOffset);
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
@ -146,6 +146,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
/// <summary>
/// Retrieves the <see cref="HitResult"/> for a time offset.
/// </summary>
/// <param name="timeOffset">The time offset.</param>
/// <returns>The hit result, or <see cref="HitResult.None"/> if <paramref name="timeOffset"/> doesn't result in a judgement.</returns>
protected virtual HitResult ResultFor(double timeOffset) => HitObject.HitWindows.ResultFor(timeOffset);
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (JudgedObject?.HitObject is OsuHitObject osuObject)
{
Position = osuObject.StackedPosition;
Position = osuObject.StackedEndPosition;
Scale = new Vector2(osuObject.Scale);
}
}

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning;
@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SliderBall Ball { get; private set; }
public SkinnableDrawable Body { get; private set; }
public override bool DisplayResult => false;
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody;
@ -249,7 +250,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (userTriggered || Time.Current < HitObject.EndTime)
return;
ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
// If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
// But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
if (HitObject.OnlyJudgeNestedObjects)
{
ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
return;
}
// Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
ApplyResult(r =>
{
int totalTicks = NestedHitObjects.Count;
int hitTicks = NestedHitObjects.Count(h => h.IsHit);
if (hitTicks == totalTicks)
r.Type = HitResult.Great;
else if (hitTicks == 0)
r.Type = HitResult.Miss;
else
{
double hitFraction = (double)hitTicks / totalTicks;
r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh;
}
});
}
public override void PlaySamples()

View File

@ -7,16 +7,27 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderHead : DrawableHitCircle
{
public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject;
[CanBeNull]
public Slider Slider => DrawableSlider?.HitObject;
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
/// <summary>
/// Makes this <see cref="DrawableSliderHead"/> track the follow circle when the start time is reached.
/// If <c>false</c>, this <see cref="DrawableSliderHead"/> will be pinned to its initial position in the slider.
/// </summary>
public bool TrackFollowCircle = true;
private readonly IBindable<int> pathVersion = new Bindable<int>();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
@ -59,12 +70,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.Update();
Debug.Assert(Slider != null);
Debug.Assert(HitObject != null);
double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
if (TrackFollowCircle)
{
double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
if (!IsHit)
Position = Slider.CurvePositionAt(completionProgress);
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
if (!IsHit)
Position = Slider.CurvePositionAt(completionProgress);
}
}
protected override HitResult ResultFor(double timeOffset)
{
Debug.Assert(HitObject != null);
if (HitObject.JudgeAsNormalHitCircle)
return base.ResultFor(timeOffset);
// If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring.
var result = base.ResultFor(timeOffset);
return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
}
public Action<double> OnShake;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@ -25,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
internal const float BASE_SCORING_DISTANCE = 100;
/// <summary>
/// Minimum preempt time at AR=10.
/// </summary>
public const double PREEMPT_MIN = 450;
public double TimePreempt = 600;
public double TimeFadeIn = 400;
@ -112,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
TimeFadeIn = 400; // as per osu-stable
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.
// Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good.
// This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in.
TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN);
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}

View File

@ -114,8 +114,14 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public double TickDistanceMultiplier = 1;
/// <summary>
/// Whether this <see cref="Slider"/>'s judgement is fully handled by its nested <see cref="HitObject"/>s.
/// If <c>false</c>, this <see cref="Slider"/> will be judged proportionally to the number of nested <see cref="HitObject"/>s hit.
/// </summary>
public bool OnlyJudgeNestedObjects = true;
[JsonIgnore]
public HitCircle HeadCircle { get; protected set; }
public SliderHeadCircle HeadCircle { get; protected set; }
[JsonIgnore]
public SliderTailCircle TailCircle { get; protected set; }
@ -140,7 +146,8 @@ namespace osu.Game.Rulesets.Osu.Objects
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
// For now, the samples are attached to and played by the slider itself at the correct end time.
Samples = this.GetNodeSamples(repeatCount + 1);
// ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live).
Samples = this.GetNodeSamples(repeatCount + 1).ToArray();
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@ -233,7 +240,7 @@ namespace osu.Game.Rulesets.Osu.Objects
HeadCircle.Samples = this.GetNodeSamples(0);
}
public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}

View File

@ -1,9 +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.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderHeadCircle : HitCircle
{
/// <summary>
/// Whether to treat this <see cref="SliderHeadCircle"/> as a normal <see cref="HitCircle"/> for judgement purposes.
/// If <c>false</c>, this <see cref="SliderHeadCircle"/> will be judged as a <see cref="SliderTick"/> instead.
/// </summary>
public bool JudgeAsNormalHitCircle = true;
public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement();
}
}

View File

@ -33,10 +33,5 @@ namespace osu.Game.Rulesets.Osu.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderTickJudgement();
public class SliderTickJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}

View File

@ -163,6 +163,7 @@ namespace osu.Game.Rulesets.Osu
{
new OsuModTarget(),
new OsuModDifficultyAdjust(),
new OsuModClassic()
};
case ModType.Automation:

View File

@ -6,10 +6,12 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Scoring;
@ -33,11 +35,6 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Constants
/// <summary>
/// The "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
/// </summary>
private readonly double reactionTime;
private readonly HitWindows defaultHitWindows;
/// <summary>
@ -49,12 +46,9 @@ namespace osu.Game.Rulesets.Osu.Replays
#region Construction / Initialisation
public OsuAutoGenerator(IBeatmap beatmap)
: base(beatmap)
public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(beatmap, mods)
{
// Already superhuman, but still somewhat realistic
reactionTime = ApplyModsToRate(100);
defaultHitWindows = new OsuHitWindows();
defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
}
@ -240,7 +234,7 @@ namespace osu.Game.Rulesets.Osu.Replays
OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1];
// Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime);
double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt));
if (waitTime > lastFrame.Time)
{
@ -250,7 +244,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Vector2 lastPosition = lastFrame.Position;
double timeDifference = ApplyModsToTime(h.StartTime - lastFrame.Time);
double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime);
// Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up.
if (timeDifference > 0 && // Sanity checks
@ -258,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Replays
timeDifference >= 266)) // ... or the beats are slow enough to tap anyway.
{
// Perform eased movement
for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay)
for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time))
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing);
AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions });
@ -272,6 +266,14 @@ namespace osu.Game.Rulesets.Osu.Replays
}
}
/// <summary>
/// Calculates the "reaction time" in ms between "seeing" a new hit object and moving to "react" to it.
/// </summary>
/// <remarks>
/// Already superhuman, but still somewhat realistic.
/// </remarks>
private double getReactionTime(double timeInstant) => ApplyModsToRate(timeInstant, 100);
// Add frames to click the hitobject
private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection)
{
@ -341,17 +343,23 @@ namespace osu.Game.Rulesets.Osu.Replays
float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X);
double t;
double previousFrame = h.StartTime;
for (double j = h.StartTime + FrameDelay; j < spinner.EndTime; j += FrameDelay)
for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame))
{
t = ApplyModsToTime(j - h.StartTime) * spinnerDirection;
t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection;
angle += (float)t / 20;
Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action));
Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action));
previousFrame = nextFrame;
}
t = ApplyModsToTime(spinner.EndTime - h.StartTime) * spinnerDirection;
Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS);
t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection;
angle += (float)t / 20;
Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS);
AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action));
@ -359,7 +367,7 @@ namespace osu.Game.Rulesets.Osu.Replays
break;
case Slider slider:
for (double j = FrameDelay; j < slider.Duration; j += FrameDelay)
for (double j = GetFrameDelay(slider.StartTime); j < slider.Duration; j += GetFrameDelay(slider.StartTime + j))
{
Vector2 pos = slider.StackedPositionAt(j / slider.Duration);
AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action));

View File

@ -5,7 +5,9 @@ using osuTK;
using osu.Game.Beatmaps;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays;
@ -22,33 +24,61 @@ namespace osu.Game.Rulesets.Osu.Replays
public const float SPIN_RADIUS = 50;
/// <summary>
/// The time in ms between each ReplayFrame.
/// </summary>
protected readonly double FrameDelay;
#endregion
#region Construction / Initialisation
protected Replay Replay;
protected List<ReplayFrame> Frames => Replay.Frames;
private readonly IReadOnlyList<IApplicableToRate> timeAffectingMods;
protected OsuAutoGeneratorBase(IBeatmap beatmap)
protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(beatmap)
{
Replay = new Replay();
// We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps.
FrameDelay = ApplyModsToRate(1000.0 / 60.0);
timeAffectingMods = mods.OfType<IApplicableToRate>().ToList();
}
#endregion
#region Utilities
protected double ApplyModsToTime(double v) => v;
protected double ApplyModsToRate(double v) => v;
/// <summary>
/// Returns the real duration of time between <paramref name="startTime"/> and <paramref name="endTime"/>
/// after applying rate-affecting mods.
/// </summary>
/// <remarks>
/// This method should only be used when <paramref name="startTime"/> and <paramref name="endTime"/> are very close.
/// That is because the track rate might be changing with time,
/// and the method used here is a rough instantaneous approximation.
/// </remarks>
/// <param name="startTime">The start time of the time delta, in original track time.</param>
/// <param name="endTime">The end time of the time delta, in original track time.</param>
protected double ApplyModsToTimeDelta(double startTime, double endTime)
{
double delta = endTime - startTime;
foreach (var mod in timeAffectingMods)
delta /= mod.ApplyToRate(startTime);
return delta;
}
protected double ApplyModsToRate(double time, double rate)
{
foreach (var mod in timeAffectingMods)
rate = mod.ApplyToRate(time, rate);
return rate;
}
/// <summary>
/// Calculates the interval after which the next <see cref="ReplayFrame"/> should be generated,
/// in milliseconds.
/// </summary>
/// <param name="time">The time of the previous frame.</param>
protected double GetFrameDelay(double time)
=> ApplyModsToRate(time, 1000.0 / 60);
private class ReplayFrameComparer : IComparer<ReplayFrame>
{

View File

@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
private readonly Bindable<bool> configSnakingOut = new Bindable<bool>();
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableObject)
{
@ -36,10 +38,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, SnakingOut);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
SnakingOut.BindTo(configSnakingOut);
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
drawableObject.HitObjectApplied += onHitObjectApplied;
}
private void onHitObjectApplied(DrawableHitObject obj)
{
var drawableSlider = (DrawableSlider)obj;
if (drawableSlider.HitObject == null)
return;
// When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way.
if (!drawableSlider.HeadCircle.TrackFollowCircle)
{
SnakingOut.UnbindFrom(configSnakingOut);
SnakingOut.Value = false;
}
}
private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour)

View File

@ -31,6 +31,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
set => ball.Colour = value;
}
/// <summary>
/// Whether to track accurately to the visual size of this <see cref="SliderBall"/>.
/// If <c>false</c>, tracking will be performed at the final scale at all times.
/// </summary>
public bool InputTracksVisualSize = true;
private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider;
private readonly Drawable ball;
@ -94,7 +100,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
tracking = value;
followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
if (InputTracksVisualSize)
followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
else
{
// We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration.
followCircle.ScaleTo(tracking ? 2.4f : 1f);
}
followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
}
}

View File

@ -0,0 +1,54 @@
// 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.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
/// <remarks>
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
/// </remarks>
/// </summary>
public class ObjectOrderedHitPolicy : IHitPolicy
{
public IHitObjectContainer HitObjectContainer { get; set; }
public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged);
public void HandleHit(DrawableHitObject hitObject)
{
}
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
{
foreach (var obj in HitObjectContainer.AliveObjects)
{
if (obj.HitObject.StartTime >= targetTime)
yield break;
switch (obj)
{
case DrawableSpinner _:
continue;
case DrawableSlider slider:
yield return slider.HeadCircle;
break;
default:
yield return obj;
break;
}
}
}
}
}

View File

@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
private readonly FollowPointRenderer followPoints;
private readonly StartTimeOrderedHitPolicy hitPolicy;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@ -54,10 +53,9 @@ namespace osu.Game.Rulesets.Osu.UI
approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
};
hitPolicy = new StartTimeOrderedHitPolicy { HitObjectContainer = HitObjectContainer };
HitPolicy = new StartTimeOrderedHitPolicy();
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded));
@ -66,6 +64,18 @@ namespace osu.Game.Rulesets.Osu.UI
NewResult += onNewResult;
}
private IHitPolicy hitPolicy;
public IHitPolicy HitPolicy
{
get => hitPolicy;
set
{
hitPolicy = value ?? throw new ArgumentNullException(nameof(value));
hitPolicy.HitObjectContainer = HitObjectContainer;
}
}
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
{
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModAutoplay : ModAutoplay<TaikoHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap) => new Score
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(),

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModCinema : ModCinema<TaikoHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap) => new Score
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(),

View File

@ -21,6 +21,27 @@ namespace osu.Game.Tests.Chat
Assert.AreEqual(36, result.Links[0].Length);
}
[TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456")]
[TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456?whatever")]
[TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123/456")]
[TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc/def")]
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")]
[TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")]
[TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc")]
public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link)
{
MessageFormatter.WebsiteRootUrl = "dev.ppy.sh";
Message result = MessageFormatter.FormatMessage(new Message { Content = link });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual(expectedAction, result.Links[0].Action);
Assert.AreEqual(expectedArg, result.Links[0].Argument);
if (expectedAction == LinkAction.External)
Assert.AreEqual(link, result.Links[0].Url);
}
[Test]
public void TestMultipleComplexLinks()
{

View File

@ -68,12 +68,29 @@ namespace osu.Game.Tests.Online
Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
}
[Test]
public void TestDeserialiseDifficultyAdjustModWithExtendedLimits()
{
var apiMod = new APIMod(new TestModDifficultyAdjust
{
OverallDifficulty = { Value = 11 },
ExtendedLimits = { Value = true }
});
var deserialised = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
var converted = (TestModDifficultyAdjust)deserialised.ToMod(new TestRuleset());
Assert.That(converted.ExtendedLimits.Value, Is.True);
Assert.That(converted.OverallDifficulty.Value, Is.EqualTo(11));
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
{
new TestMod(),
new TestModTimeRamp(),
new TestModDifficultyAdjust()
};
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new System.NotImplementedException();
@ -135,5 +152,9 @@ namespace osu.Game.Tests.Online
Value = true
};
}
private class TestModDifficultyAdjust : ModDifficultyAdjust
{
}
}
}

View File

@ -68,6 +68,16 @@ namespace osu.Game.Tests.Online
Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
}
[Test]
public void TestDeserialiseEnumMod()
{
var apiMod = new APIMod(new TestModEnum { TestSetting = { Value = TestEnum.Value2 } });
var deserialized = MessagePackSerializer.Deserialize<APIMod>(MessagePackSerializer.Serialize(apiMod));
Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(1));
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
@ -135,5 +145,22 @@ namespace osu.Game.Tests.Online
Value = true
};
}
private class TestModEnum : Mod
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
public Bindable<TestEnum> TestSetting { get; } = new Bindable<TestEnum>();
}
private enum TestEnum
{
Value1 = 0,
Value2 = 1,
Value3 = 2
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty<Mod>());
return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap));
return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap, Array.Empty<Mod>()));
}
protected override void AddCheckSteps()

View File

@ -103,26 +103,26 @@ namespace osu.Game.Tests.Visual.Online
private void testLinksGeneral()
{
addMessageWithChecks("test!");
addMessageWithChecks("osu.ppy.sh!");
addMessageWithChecks("https://osu.ppy.sh!", 1, expectedActions: LinkAction.External);
addMessageWithChecks("dev.ppy.sh!");
addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External);
addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp);
addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.External);
addMessageWithChecks("(osu forums)[https://osu.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[https://osu.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[osu forums](https://osu.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[https://osu.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External);
addMessageWithChecks("is now listening to [https://osu.ppy.sh/s/93523 IMAGE -MATERIAL- <Version 0>]", 1, true, expectedActions: LinkAction.OpenBeatmapSet);
addMessageWithChecks("is now playing [https://osu.ppy.sh/b/252238 IMAGE -MATERIAL- <Version 0>]", 1, true, expectedActions: LinkAction.OpenBeatmap);
addMessageWithChecks("Let's (try)[https://osu.ppy.sh/home] [https://osu.ppy.sh/b/252238 multiple links] https://osu.ppy.sh/home", 3,
addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External);
addMessageWithChecks("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- <Version 0>]", 1, true, expectedActions: LinkAction.OpenBeatmapSet);
addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- <Version 0>]", 1, true, expectedActions: LinkAction.OpenBeatmap);
addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", 3,
expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External });
addMessageWithChecks("[https://osu.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://osu.ppy.sh/home)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://osu.ppy.sh/home] and [[also a rogue wiki link]]", 2, expectedActions: new[] { LinkAction.External, LinkAction.External });
addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", 2, expectedActions: new[] { LinkAction.External, LinkAction.External });
// note that there's 0 links here (they get removed if a channel is not found)
addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).");
addMessageWithChecks("I am important!", 0, false, true);
addMessageWithChecks("feels important", 0, true, true);
addMessageWithChecks("likes to post this [https://osu.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
addMessageWithChecks("Join my [#english](osu://chan/#english).", 1, expectedActions: LinkAction.OpenChannel);
@ -136,9 +136,9 @@ namespace osu.Game.Tests.Visual.Online
int echoCounter = 0;
addEchoWithWait("sent!", "received!");
addEchoWithWait("https://osu.ppy.sh/home", null, 500);
addEchoWithWait("[https://osu.ppy.sh/forum let's try multiple words too!]");
addEchoWithWait("(long loading times! clickable while loading?)[https://osu.ppy.sh/home]", null, 5000);
addEchoWithWait("https://dev.ppy.sh/home", null, 500);
addEchoWithWait("[https://dev.ppy.sh/forum let's try multiple words too!]");
addEchoWithWait("(long loading times! clickable while loading?)[https://dev.ppy.sh/home]", null, 5000);
void addEchoWithWait(string text, string completeText = null, double delay = 250)
{

View File

@ -105,6 +105,15 @@ namespace osu.Game.Tests.Visual.UserInterface
checkDisplayedCount(3);
}
[Test]
public void TestError()
{
setState(Visibility.Visible);
AddStep(@"error #1", sendErrorNotification);
AddAssert("Is visible", () => notificationOverlay.State.Value == Visibility.Visible);
checkDisplayedCount(1);
}
[Test]
public void TestSpam()
{
@ -179,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void sendBarrage()
{
switch (RNG.Next(0, 4))
switch (RNG.Next(0, 5))
{
case 0:
sendHelloNotification();
@ -196,6 +205,10 @@ namespace osu.Game.Tests.Visual.UserInterface
case 3:
sendDownloadProgress();
break;
case 4:
sendErrorNotification();
break;
}
}
@ -214,6 +227,11 @@ namespace osu.Game.Tests.Visual.UserInterface
notificationOverlay.Post(new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" });
}
private void sendErrorNotification()
{
notificationOverlay.Post(new SimpleErrorNotification { Text = @"Rut roh!. Something went wrong!" });
}
private void sendManyNotifications()
{
for (int i = 0; i < 10; i++)

View File

@ -13,15 +13,18 @@ namespace osu.Game.Tournament.Tests
{
base.LoadComplete();
LoadComponentAsync(new Background("Menu/menu-background-0")
BracketLoadTask.ContinueWith(_ => Schedule(() =>
{
Colour = OsuColour.Gray(0.5f),
Depth = 10
}, AddInternal);
LoadComponentAsync(new Background("Menu/menu-background-0")
{
Colour = OsuColour.Gray(0.5f),
Depth = 10
}, AddInternal);
// Have to construct this here, rather than in the constructor, because
// we depend on some dependencies to be loaded within OsuGameBase.load().
Add(new TestBrowser());
// Have to construct this here, rather than in the constructor, because
// we depend on some dependencies to be loaded within OsuGameBase.load().
Add(new TestBrowser());
}));
}
}
}

View File

@ -154,10 +154,13 @@ namespace osu.Game.Tournament.Tests
protected override void LoadAsyncComplete()
{
// this has to be run here rather than LoadComplete because
// TestScene.cs is checking the IsLoaded state (on another thread) and expects
// the runner to be loaded at that point.
Add(runner = new TestSceneTestRunner.TestRunner());
BracketLoadTask.ContinueWith(_ => Schedule(() =>
{
// this has to be run here rather than LoadComplete because
// TestScene.cs is checking the IsLoaded state (on another thread) and expects
// the runner to be loaded at that point.
Add(runner = new TestSceneTestRunner.TestRunner());
}));
}
public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test);

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Colour;
using osu.Game.Graphics.Cursor;
using osu.Game.Tournament.Models;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@ -32,25 +33,24 @@ namespace osu.Game.Tournament
private Drawable heightWarning;
private Bindable<Size> windowSize;
private Bindable<WindowMode> windowMode;
private LoadingSpinner loadingSpinner;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager frameworkConfig)
{
windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>
{
var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1;
heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
}), true);
windowMode = frameworkConfig.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
{
windowMode.Value = WindowMode.Windowed;
}), true);
AddRange(new[]
Add(loadingSpinner = new LoadingSpinner(true, true)
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding(40),
});
loadingSpinner.Show();
BracketLoadTask.ContinueWith(_ => LoadComponentsAsync(new[]
{
new Container
{
@ -93,7 +93,24 @@ namespace osu.Game.Tournament
RelativeSizeAxes = Axes.Both,
Child = new TournamentSceneManager()
}
});
}, drawables =>
{
loadingSpinner.Hide();
loadingSpinner.Expire();
AddRange(drawables);
windowSize.BindValueChanged(size => ScheduleAfterChildren(() =>
{
var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1;
heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0;
}), true);
windowMode.BindValueChanged(mode => ScheduleAfterChildren(() =>
{
windowMode.Value = WindowMode.Windowed;
}), true);
}));
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
@ -29,6 +30,10 @@ namespace osu.Game.Tournament
private DependencyContainer dependencies;
private FileBasedIPC ipc;
protected Task BracketLoadTask => taskCompletionSource.Task;
private readonly TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
return dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@ -46,14 +51,9 @@ namespace osu.Game.Tournament
Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
readBracket();
ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value);
dependencies.CacheAs(new StableInfo(storage));
dependencies.CacheAs<MatchIPCInfo>(ipc = new FileBasedIPC());
Add(ipc);
Task.Run(readBracket);
}
private void readBracket()
@ -68,10 +68,6 @@ namespace osu.Game.Tournament
ladder ??= new LadderInfo();
ladder.Ruleset.Value ??= RulesetStore.AvailableRulesets.First();
Ruleset.BindTo(ladder.Ruleset);
dependencies.Cache(ladder);
bool addedInfo = false;
// assign teams
@ -127,6 +123,19 @@ namespace osu.Game.Tournament
if (addedInfo)
SaveChanges();
ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value);
Schedule(() =>
{
Ruleset.BindTo(ladder.Ruleset);
dependencies.Cache(ladder);
dependencies.CacheAs<MatchIPCInfo>(ipc = new FileBasedIPC());
Add(ipc);
taskCompletionSource.SetResult(true);
});
}
/// <summary>

View File

@ -64,7 +64,9 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
protected override string ImportFromStablePath => "Songs";
protected override string ImportFromStablePath => ".";
protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage();
private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps;

View File

@ -34,8 +34,8 @@ namespace osu.Game.Beatmaps.Drawables
/// </summary>
protected virtual double UnloadDelay => 10000;
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay);
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad) =>
new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay) { RelativeSizeAxes = Axes.Both };
protected override double TransformDuration => 400;

View File

@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Configuration
{
/// <summary>
/// A helper class for tracking changes to the settings of a set of <see cref="Mod"/>s.
/// </summary>
/// <remarks>
/// Ensure to dispose when usage is finished.
/// </remarks>
public class ModSettingChangeTracker : IDisposable
{
/// <summary>
/// Notifies that the setting of a <see cref="Mod"/> has changed.
/// </summary>
public Action<Mod> SettingChanged;
private readonly List<ISettingsItem> settings = new List<ISettingsItem>();
/// <summary>
/// Creates a new <see cref="ModSettingChangeTracker"/> for a set of <see cref="Mod"/>s.
/// </summary>
/// <param name="mods">The set of <see cref="Mod"/>s whose settings need to be tracked.</param>
public ModSettingChangeTracker(IEnumerable<Mod> mods)
{
foreach (var mod in mods)
{
foreach (var setting in mod.CreateSettingsControls().OfType<ISettingsItem>())
{
setting.SettingChanged += () => SettingChanged?.Invoke(mod);
settings.Add(setting);
}
}
}
public void Dispose()
{
SettingChanged = null;
foreach (var r in settings)
r.Dispose();
settings.Clear();
}
}
}

View File

@ -57,6 +57,7 @@ namespace osu.Game.Configuration
yield return new SettingsSlider<float>
{
LabelText = attr.Label,
TooltipText = attr.Description,
Current = bNumber,
KeyboardStep = 0.1f,
};
@ -67,6 +68,7 @@ namespace osu.Game.Configuration
yield return new SettingsSlider<double>
{
LabelText = attr.Label,
TooltipText = attr.Description,
Current = bNumber,
KeyboardStep = 0.1f,
};
@ -77,6 +79,7 @@ namespace osu.Game.Configuration
yield return new SettingsSlider<int>
{
LabelText = attr.Label,
TooltipText = attr.Description,
Current = bNumber
};
@ -86,6 +89,7 @@ namespace osu.Game.Configuration
yield return new SettingsCheckbox
{
LabelText = attr.Label,
TooltipText = attr.Description,
Current = bBool
};
@ -95,6 +99,7 @@ namespace osu.Game.Configuration
yield return new SettingsTextBox
{
LabelText = attr.Label,
TooltipText = attr.Description,
Current = bString
};
@ -105,6 +110,7 @@ namespace osu.Game.Configuration
var dropdown = (Drawable)Activator.CreateInstance(dropdownType);
dropdownType.GetProperty(nameof(SettingsDropdown<object>.LabelText))?.SetValue(dropdown, attr.Label);
dropdownType.GetProperty(nameof(SettingsDropdown<object>.TooltipText))?.SetValue(dropdown, attr.Description);
dropdownType.GetProperty(nameof(SettingsDropdown<object>.Current))?.SetValue(dropdown, bindable);
yield return dropdown;

View File

@ -625,7 +625,7 @@ namespace osu.Game.Database
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
public Func<StableStorage> GetStableStorage { private get; set; }
/// <summary>
/// Denotes whether an osu-stable installation is present to perform automated imports from.
@ -638,9 +638,10 @@ namespace osu.Game.Database
protected virtual string ImportFromStablePath => null;
/// <summary>
/// Select paths to import from stable. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>.
/// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in <see cref="ImportFromStablePath"/>.
/// </summary>
protected virtual IEnumerable<string> GetStableImportPaths(Storage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath);
protected virtual IEnumerable<string> GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath)
.Select(path => storage.GetFullPath(path));
/// <summary>
/// Whether this specified path should be removed after successful import.
@ -654,24 +655,33 @@ namespace osu.Game.Database
/// </summary>
public Task ImportFromStableAsync()
{
var stable = GetStableStorage?.Invoke();
var stableStorage = GetStableStorage?.Invoke();
if (stable == null)
if (stableStorage == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
if (!stable.ExistsDirectory(ImportFromStablePath))
var storage = PrepareStableStorage(stableStorage);
if (!storage.ExistsDirectory(ImportFromStablePath))
{
// This handles situations like when the user does not have a Skins folder
Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray()));
return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()));
}
/// <summary>
/// Run any required traversal operations on the stable storage location before performing operations.
/// </summary>
/// <param name="stableStorage">The stable storage.</param>
/// <returns>The usable storage. Return the unchanged <paramref name="stableStorage"/> if no traversal is required.</returns>
protected virtual Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage;
#endregion
/// <summary>

View File

@ -38,7 +38,12 @@ namespace osu.Game.Graphics.Containers
foreach (var link in links)
{
AddText(text[previousLinkEnd..link.Index]);
AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url);
string displayText = text.Substring(link.Index, link.Length);
string linkArgument = link.Argument ?? link.Url;
string tooltip = displayText == link.Url ? null : link.Url;
AddLink(displayText, link.Action, linkArgument, tooltip);
previousLinkEnd = link.Index + link.Length;
}
@ -52,7 +57,7 @@ namespace osu.Game.Graphics.Containers
=> createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action);
public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action<SpriteText> creationParameters = null)
=> createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null);
=> createLink(AddText(text, creationParameters), new LinkDetails(action, argument), tooltipText);
public void AddLink(IEnumerable<SpriteText> text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null)
{

View File

@ -20,6 +20,8 @@ namespace osu.Game.Graphics.Containers
{
private SampleChannel samplePopIn;
private SampleChannel samplePopOut;
protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
protected override bool BlockNonPositionalInput => true;
@ -40,8 +42,8 @@ namespace osu.Game.Graphics.Containers
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio)
{
samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in");
samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out");
samplePopIn = audio.Samples.Get(PopInSampleName);
samplePopOut = audio.Samples.Get(PopOutSampleName);
}
protected override void LoadComplete()

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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// Handles debouncing hover sounds at a global level to ensure the effects are not overwhelming.
/// </summary>
public abstract class HoverSampleDebounceComponent : CompositeDrawable
{
/// <summary>
/// Length of debounce for hover sound playback, in milliseconds.
/// </summary>
public double HoverDebounceTime { get; } = 20;
private Bindable<double?> lastPlaybackTime;
[BackgroundDependencyLoader]
private void load(AudioManager audio, SessionStatics statics)
{
lastPlaybackTime = statics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime);
}
protected override bool OnHover(HoverEvent e)
{
// hover sounds shouldn't be played during scroll operations.
if (e.HasAnyButtonPressed)
return false;
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime;
if (enoughTimePassedSinceLastPlayback)
{
PlayHoverSample();
lastPlaybackTime.Value = Time.Current;
}
return false;
}
public abstract void PlayHoverSample();
}
}

View File

@ -5,12 +5,10 @@ using System.ComponentModel;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Framework.Utils;
namespace osu.Game.Graphics.UserInterface
{
@ -18,19 +16,12 @@ namespace osu.Game.Graphics.UserInterface
/// Adds hover sounds to a drawable.
/// Does not draw anything.
/// </summary>
public class HoverSounds : CompositeDrawable
public class HoverSounds : HoverSampleDebounceComponent
{
private SampleChannel sampleHover;
/// <summary>
/// Length of debounce for hover sound playback, in milliseconds.
/// </summary>
public double HoverDebounceTime { get; } = 20;
protected readonly HoverSampleSet SampleSet;
private Bindable<double?> lastPlaybackTime;
public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal)
{
SampleSet = sampleSet;
@ -40,22 +31,13 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio, SessionStatics statics)
{
lastPlaybackTime = statics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime);
sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}");
}
protected override bool OnHover(HoverEvent e)
public override void PlayHoverSample()
{
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime;
if (enoughTimePassedSinceLastPlayback)
{
sampleHover?.Play();
lastPlaybackTime.Value = Time.Current;
}
return base.OnHover(e);
sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08);
sampleHover.Play();
}
}
@ -68,6 +50,9 @@ namespace osu.Game.Graphics.UserInterface
Normal,
[Description("-softer")]
Soft
Soft,
[Description("-toolbar")]
Toolbar
}
}

View File

@ -64,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.X,
},
Nub = new Nub(),
new HoverClickSounds()
new HoverSounds()
};
if (nubOnRight)

View File

@ -0,0 +1,62 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.IO;
using System.Linq;
using osu.Framework.Platform;
namespace osu.Game.IO
{
/// <summary>
/// A storage pointing to an osu-stable installation.
/// Provides methods for handling installations with a custom Song folder location.
/// </summary>
public class StableStorage : DesktopStorage
{
private const string stable_default_songs_path = "Songs";
private readonly DesktopGameHost host;
private readonly Lazy<string> songsPath;
public StableStorage(string path, DesktopGameHost host)
: base(path, host)
{
this.host = host;
songsPath = new Lazy<string>(locateSongsDirectory);
}
/// <summary>
/// Returns a <see cref="Storage"/> pointing to the osu-stable Songs directory.
/// </summary>
public Storage GetSongStorage() => new DesktopStorage(songsPath.Value, host);
private string locateSongsDirectory()
{
var configFile = GetFiles(".", $"osu!.{Environment.UserName}.cfg").SingleOrDefault();
if (configFile != null)
{
using (var stream = GetStream(configFile))
using (var textReader = new StreamReader(stream))
{
string line;
while ((line = textReader.ReadLine()) != null)
{
if (!line.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase)) continue;
var customDirectory = line.Split('=').LastOrDefault()?.Trim();
if (customDirectory != null && Path.IsPathFullyQualified(customDirectory))
return customDirectory;
break;
}
}
}
return GetFullPath(stable_default_songs_path);
}
}
}

View File

@ -3,6 +3,7 @@
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using MessagePack;
using MessagePack.Formatters;
@ -41,6 +42,13 @@ namespace osu.Game.Online.API
primitiveFormatter.Serialize(ref writer, b.Value, options);
break;
case IBindable u:
// A mod with unknown (e.g. enum) generic type.
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
Debug.Assert(valueMethod != null);
primitiveFormatter.Serialize(ref writer, valueMethod.GetValue(u), options);
break;
default:
// fall back for non-bindable cases.
primitiveFormatter.Serialize(ref writer, kvp.Value, options);

View File

@ -49,6 +49,18 @@ namespace osu.Game.Online.Chat
// Unicode emojis
private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])");
/// <summary>
/// The root URL for the website, used for chat link matching.
/// </summary>
public static string WebsiteRootUrl
{
set => websiteRootUrl = value
.Trim('/') // trim potential trailing slash/
.Split('/').Last(); // only keep domain name, ignoring protocol.
}
private static string websiteRootUrl = "osu.ppy.sh";
private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null, char[] escapeChars = null)
{
int captureOffset = 0;
@ -119,22 +131,42 @@ namespace osu.Game.Online.Chat
case "http":
case "https":
// length > 3 since all these links need another argument to work
if (args.Length > 3 && args[1] == "osu.ppy.sh")
if (args.Length > 3 && args[1].EndsWith(websiteRootUrl, StringComparison.OrdinalIgnoreCase))
{
var mainArg = args[3];
switch (args[2])
{
// old site only
case "b":
case "beatmaps":
return new LinkDetails(LinkAction.OpenBeatmap, args[3]);
{
string trimmed = mainArg.Split('?').First();
if (int.TryParse(trimmed, out var id))
return new LinkDetails(LinkAction.OpenBeatmap, id.ToString());
break;
}
case "s":
case "beatmapsets":
case "d":
return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]);
{
if (args.Length > 4 && int.TryParse(args[4], out var id))
// https://osu.ppy.sh/beatmapsets/1154158#osu/2768184
return new LinkDetails(LinkAction.OpenBeatmap, id.ToString());
// https://osu.ppy.sh/beatmapsets/1154158#whatever
string trimmed = mainArg.Split('#').First();
if (int.TryParse(trimmed, out id))
return new LinkDetails(LinkAction.OpenBeatmapSet, id.ToString());
break;
}
case "u":
case "users":
return new LinkDetails(LinkAction.OpenUserProfile, args[3]);
return new LinkDetails(LinkAction.OpenUserProfile, mainArg);
}
}
@ -183,10 +215,9 @@ namespace osu.Game.Online.Chat
case "osump":
return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]);
default:
return new LinkDetails(LinkAction.External, null);
}
return new LinkDetails(LinkAction.External, null);
}
private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3)
@ -259,8 +290,9 @@ namespace osu.Game.Online.Chat
public class LinkDetails
{
public LinkAction Action;
public string Argument;
public readonly LinkAction Action;
public readonly string Argument;
public LinkDetails(LinkAction action, string argument)
{

View File

@ -28,7 +28,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Collections;
@ -52,6 +51,7 @@ using osu.Game.Updater;
using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Database;
using osu.Game.IO;
namespace osu.Game
{
@ -88,7 +88,7 @@ namespace osu.Game
protected SentryLogger SentryLogger;
public virtual Storage GetStorageForStableInstall() => null;
public virtual StableStorage GetStorageForStableInstall() => null;
public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0);
@ -778,7 +778,7 @@ namespace osu.Game
if (recentLogCount < short_term_display_limit)
{
Schedule(() => notifications.Post(new SimpleNotification
Schedule(() => notifications.Post(new SimpleErrorNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),

View File

@ -31,6 +31,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.IO;
using osu.Game.Online;
using osu.Game.Online.Chat;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
@ -155,6 +156,8 @@ namespace osu.Game
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(0.5f);
[BackgroundDependencyLoader]
private void load()
{
@ -223,6 +226,8 @@ namespace osu.Game
EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;
dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints));
dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints));
@ -278,9 +283,10 @@ namespace osu.Game
RegisterImportHandler(ScoreManager);
RegisterImportHandler(SkinManager);
// tracks play so loud our samples can't keep up.
// this adds a global reduction of track volume for the time being.
Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8));
// drop track volume game-wide to leave some head-room for UI effects / samples.
// this means that for the time being, gameplay sample playback is louder relative to the audio track, compared to stable.
// we may want to revisit this if users notice or complain about the difference (consider this a bit of a trial).
Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust);
Beatmap = new NonNullableBindable<WorkingBeatmap>(defaultBeatmap);

View File

@ -14,6 +14,9 @@ namespace osu.Game.Overlays
{
private readonly Container dialogContainer;
protected override string PopInSampleName => "UI/dialog-pop-in";
protected override string PopOutSampleName => "UI/dialog-pop-out";
public PopupDialog CurrentDialog { get; private set; }
public DialogOverlay()

View File

@ -51,6 +51,11 @@ namespace osu.Game.Overlays.Mods
/// </summary>
protected virtual bool Stacked => true;
/// <summary>
/// Whether configurable <see cref="Mod"/>s can be configured by the local user.
/// </summary>
protected virtual bool AllowConfiguration => true;
[NotNull]
private Func<Mod, bool> isValidMod = m => true;
@ -300,6 +305,7 @@ namespace osu.Game.Overlays.Mods
Text = "Customisation",
Action = () => ModSettingsContainer.ToggleVisibility(),
Enabled = { Value = false },
Alpha = AllowConfiguration ? 1 : 0,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
@ -512,7 +518,8 @@ namespace osu.Game.Overlays.Mods
OnModSelected(selectedMod);
if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show();
if (selectedMod.RequiresConfiguration && AllowConfiguration)
ModSettingsContainer.Show();
}
else
{

View File

@ -52,6 +52,7 @@ namespace osu.Game.Overlays.Mods
new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = modSettingsContent = new FillFlowContainer<ModControlSection>
{
Anchor = Anchor.TopCentre,

View File

@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@ -40,6 +42,11 @@ namespace osu.Game.Overlays.Notifications
/// </summary>
public virtual bool DisplayOnTop => true;
private SampleChannel samplePopIn;
private SampleChannel samplePopOut;
protected virtual string PopInSampleName => "UI/notification-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out"; // TODO: replace with a unique sample?
protected NotificationLight Light;
private readonly CloseButton closeButton;
protected Container IconContent;
@ -107,7 +114,7 @@ namespace osu.Game.Overlays.Notifications
closeButton = new CloseButton
{
Alpha = 0,
Action = Close,
Action = () => Close(),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding
@ -120,6 +127,13 @@ namespace osu.Game.Overlays.Notifications
});
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
samplePopIn = audio.Samples.Get(PopInSampleName);
samplePopOut = audio.Samples.Get(PopOutSampleName);
}
protected override bool OnHover(HoverEvent e)
{
closeButton.FadeIn(75);
@ -143,6 +157,9 @@ namespace osu.Game.Overlays.Notifications
protected override void LoadComplete()
{
base.LoadComplete();
samplePopIn?.Play();
this.FadeInFromZero(200);
NotificationContent.MoveToX(DrawSize.X);
NotificationContent.MoveToX(0, 500, Easing.OutQuint);
@ -150,12 +167,15 @@ namespace osu.Game.Overlays.Notifications
public bool WasClosed;
public virtual void Close()
public virtual void Close(bool playSound = true)
{
if (WasClosed) return;
WasClosed = true;
if (playSound)
samplePopOut?.Play();
Closed?.Invoke();
this.FadeOut(100);
Expire();

View File

@ -109,7 +109,12 @@ namespace osu.Game.Overlays.Notifications
private void clearAll()
{
notifications.Children.ForEach(c => c.Close());
bool first = true;
notifications.Children.ForEach(c =>
{
c.Close(first);
first = false;
});
}
protected override void Update()

View File

@ -150,12 +150,12 @@ namespace osu.Game.Overlays.Notifications
colourCancelled = colours.Red;
}
public override void Close()
public override void Close(bool playSound = true)
{
switch (State)
{
case ProgressNotificationState.Cancelled:
base.Close();
base.Close(playSound);
break;
case ProgressNotificationState.Active:

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.Notifications
{
public class SimpleErrorNotification : SimpleNotification
{
protected override string PopInSampleName => "UI/error-notification-pop-in";
public SimpleErrorNotification()
{
Icon = FontAwesome.Solid.Bomb;
}
}
}

View File

@ -51,6 +51,9 @@ namespace osu.Game.Overlays
private Container dragContainer;
private Container playerContainer;
protected override string PopInSampleName => "UI/now-playing-pop-in";
protected override string PopOutSampleName => "UI/now-playing-pop-out";
/// <summary>
/// Provide a source for the toolbar height.
/// </summary>

View File

@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -19,6 +21,13 @@ namespace osu.Game.Overlays.OSD
{
private const int lights_bottom_margin = 40;
private readonly int optionCount;
private readonly int selectedOption = -1;
private SampleChannel sampleOn;
private SampleChannel sampleOff;
private SampleChannel sampleChange;
public TrackedSettingToast(SettingDescription description)
: base(description.Name, description.Value, description.Shortcut)
{
@ -46,9 +55,6 @@ namespace osu.Game.Overlays.OSD
}
};
int optionCount = 0;
int selectedOption = -1;
switch (description.RawValue)
{
case bool val:
@ -69,6 +75,34 @@ namespace osu.Game.Overlays.OSD
optionLights.Add(new OptionLight { Glowing = i == selectedOption });
}
protected override void LoadComplete()
{
base.LoadComplete();
if (optionCount == 1)
{
if (selectedOption == 0)
sampleOn?.Play();
else
sampleOff?.Play();
}
else
{
if (sampleChange == null) return;
sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f;
sampleChange.Play();
}
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleOn = audio.Samples.Get("UI/osd-on");
sampleOff = audio.Samples.Get("UI/osd-off");
sampleChange = audio.Samples.Get("UI/osd-change");
}
private class OptionLight : Container
{
private Color4 glowingColour, idleColour;

View File

@ -21,7 +21,7 @@ using osuTK;
namespace osu.Game.Overlays.Settings
{
public abstract class SettingsItem<T> : Container, IFilterable, ISettingsItem, IHasCurrentValue<T>
public abstract class SettingsItem<T> : Container, IFilterable, ISettingsItem, IHasCurrentValue<T>, IHasTooltip
{
protected abstract Drawable CreateControl();
@ -37,6 +37,8 @@ namespace osu.Game.Overlays.Settings
public bool ShowsDefaultIndicator = true;
public string TooltipText { get; set; }
public virtual string LabelText
{
get => labelText?.Text ?? string.Empty;

View File

@ -40,6 +40,8 @@ namespace osu.Game.Overlays
private SeekLimitedSearchTextBox searchTextBox;
protected override string PopInSampleName => "UI/settings-pop-in";
/// <summary>
/// Provide a source for the toolbar height.
/// </summary>

View File

@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Toolbar
private KeyBindingStore keyBindings { get; set; }
protected ToolbarButton()
: base(HoverSampleSet.Loud)
: base(HoverSampleSet.Toolbar)
{
Width = Toolbar.HEIGHT;
RelativeSizeAxes = Axes.Y;

View File

@ -18,6 +18,8 @@ namespace osu.Game.Overlays
protected override bool StartHidden => true;
protected override string PopInSampleName => "UI/wave-pop-in";
protected WaveOverlayContainer()
{
AddInternal(Waves = new WaveContainer

View File

@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
@ -45,6 +46,9 @@ namespace osu.Game.Rulesets.Edit
{
HitObject = hitObject;
// adding the default hit sample should be the case regardless of the ruleset.
HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
RelativeSizeAxes = Axes.Both;
// This is required to allow the blueprint's position to be updated via OnMouseMove/Handle

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// Interface that should be implemented by mods that affect the track playback speed,
/// and in turn, values of the track rate.
/// </summary>
public interface IApplicableToRate : IApplicableToAudio
{
/// <summary>
/// Returns the playback rate at <paramref name="time"/> after this mod is applied.
/// </summary>
/// <param name="time">The time instant at which the playback rate is queried.</param>
/// <param name="rate">The playback rate before applying this mod.</param>
/// <returns>The playback rate after applying this mod.</returns>
double ApplyToRate(double time, double rate = 1);
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@ -15,7 +16,10 @@ namespace osu.Game.Rulesets.Mods
public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToDrawableRuleset<T>
where T : HitObject
{
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset) => drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap));
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
}
}
public abstract class ModAutoplay : Mod, IApplicableFailOverride
@ -35,6 +39,11 @@ namespace osu.Game.Rulesets.Mods
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
[Obsolete("Use the mod-supporting override")] // can be removed 20210731
public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() };
#pragma warning disable 618
public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => CreateReplayScore(beatmap);
#pragma warning restore 618
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
{
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap));
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
// AlwaysPresent required for hitsounds
drawableRuleset.Playfield.AlwaysPresent = true;

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mods
protected const int LAST_SETTING_ORDER = 2;
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)]
public BindableNumber<float> DrainRate { get; } = new BindableFloat
public BindableNumber<float> DrainRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mods
};
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)]
public BindableNumber<float> OverallDifficulty { get; } = new BindableFloat
public BindableNumber<float> OverallDifficulty { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@ -53,6 +53,24 @@ namespace osu.Game.Rulesets.Mods
Value = 5,
};
[SettingSource("Extended Limits", "Adjust difficulty beyond sane limits.")]
public BindableBool ExtendedLimits { get; } = new BindableBool();
protected ModDifficultyAdjust()
{
ExtendedLimits.BindValueChanged(extend => ApplyLimits(extend.NewValue));
}
/// <summary>
/// Changes the difficulty adjustment limits. Occurs when the value of <see cref="ExtendedLimits"/> is changed.
/// </summary>
/// <param name="extended">Whether limits should extend beyond sane ranges.</param>
protected virtual void ApplyLimits(bool extended)
{
DrainRate.MaxValue = extended ? 11 : 10;
OverallDifficulty.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get
@ -152,5 +170,62 @@ namespace osu.Game.Rulesets.Mods
TransferSettings(difficulty);
}
}
/// <summary>
/// A <see cref="BindableDouble"/> that extends its min/max values to support any assigned value.
/// </summary>
protected class BindableDoubleWithLimitExtension : BindableDouble
{
public override double Value
{
get => base.Value;
set
{
if (value < MinValue)
MinValue = value;
if (value > MaxValue)
MaxValue = value;
base.Value = value;
}
}
}
/// <summary>
/// A <see cref="BindableFloat"/> that extends its min/max values to support any assigned value.
/// </summary>
protected class BindableFloatWithLimitExtension : BindableFloat
{
public override float Value
{
get => base.Value;
set
{
if (value < MinValue)
MinValue = value;
if (value > MaxValue)
MaxValue = value;
base.Value = value;
}
}
}
/// <summary>
/// A <see cref="BindableInt"/> that extends its min/max values to support any assigned value.
/// </summary>
protected class BindableIntWithLimitExtension : BindableInt
{
public override int Value
{
get => base.Value;
set
{
if (value < MinValue)
MinValue = value;
if (value > MaxValue)
MaxValue = value;
base.Value = value;
}
}
}
}
}

View File

@ -8,7 +8,7 @@ using osu.Framework.Graphics.Audio;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModRateAdjust : Mod, IApplicableToAudio
public abstract class ModRateAdjust : Mod, IApplicableToRate
{
public abstract BindableNumber<double> SpeedChange { get; }
@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mods
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value;
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
}
}

View File

@ -14,7 +14,7 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToAudio
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToRate
{
/// <summary>
/// The point in the beatmap at which the final ramping rate should be reached.
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mods
protected ModTimeRamp()
{
// for preview purpose at song select. eventually we'll want to be able to update every frame.
FinalRate.BindValueChanged(val => applyRateAdjustment(1), true);
FinalRate.BindValueChanged(val => applyRateAdjustment(double.PositiveInfinity), true);
AdjustPitch.BindValueChanged(applyPitchAdjustment);
}
@ -75,17 +75,24 @@ namespace osu.Game.Rulesets.Mods
finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart);
}
public double ApplyToRate(double time, double rate = 1)
{
double amount = (time - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime);
double ramp = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1);
// round the end result to match the bindable SpeedChange's precision, in case this is called externally.
return rate * Math.Round(ramp, 2);
}
public virtual void Update(Playfield playfield)
{
applyRateAdjustment((track.CurrentTime - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime));
applyRateAdjustment(track.CurrentTime);
}
/// <summary>
/// Adjust the rate along the specified ramp
/// Adjust the rate along the specified ramp.
/// </summary>
/// <param name="amount">The amount of adjustment to apply (from 0..1).</param>
private void applyRateAdjustment(double amount) =>
SpeedChange.Value = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1);
private void applyRateAdjustment(double time) => SpeedChange.Value = ApplyToRate(time);
private void applyPitchAdjustment(ValueChangedEvent<bool> adjustPitchSetting)
{

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.UI
protected IRulesetConfigManager Config { get; private set; }
[Cached(typeof(IReadOnlyList<Mod>))]
protected override IReadOnlyList<Mod> Mods { get; }
public sealed override IReadOnlyList<Mod> Mods { get; }
private FrameStabilityContainer frameStabilityContainer;
@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.UI
/// <summary>
/// The mods which are to be applied.
/// </summary>
protected abstract IReadOnlyList<Mod> Mods { get; }
public abstract IReadOnlyList<Mod> Mods { get; }
/// <summary>~
/// The associated ruleset.

View File

@ -71,8 +71,9 @@ namespace osu.Game.Scoring
}
}
protected override IEnumerable<string> GetStableImportPaths(Storage stableStorage)
=> stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false));
protected override IEnumerable<string> GetStableImportPaths(Storage storage)
=> storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false))
.Select(path => storage.GetFullPath(path));
public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store);

View File

@ -211,9 +211,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (blueprint != null)
{
// doing this post-creations as adding the default hit sample should be the case regardless of the ruleset.
blueprint.HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL));
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame

View File

@ -129,7 +129,7 @@ namespace osu.Game.Screens.Menu
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
buttonsTopLevel.Add(new Button(@"edit", @"button-generic-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(@"edit", @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(@"browse", @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
if (host.CanExit)

View File

@ -67,6 +67,10 @@ namespace osu.Game.Screens.Menu
{
StartTrack();
// this classic intro loops forever.
if (UsingThemedIntro)
Track.Looping = true;
const float fade_in_time = 200;
logo.ScaleTo(1);

View File

@ -45,6 +45,7 @@ namespace osu.Game.Screens.Menu
private SampleChannel sampleClick;
private SampleChannel sampleBeat;
private SampleChannel sampleDownbeat;
private readonly Container colourAndTriangles;
private readonly Triangles triangles;
@ -259,6 +260,7 @@ namespace osu.Game.Screens.Menu
{
sampleClick = audio.Samples.Get(@"Menu/osu-logo-select");
sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat");
logo.Texture = textures.Get(@"Menu/logo");
ripple.Texture = textures.Get(@"Menu/logo");
@ -281,7 +283,15 @@ namespace osu.Game.Screens.Menu
if (beatIndex < 0) return;
if (IsHovered)
this.Delay(early_activation).Schedule(() => sampleBeat.Play());
{
this.Delay(early_activation).Schedule(() =>
{
if (beatIndex % (int)timingPoint.TimeSignature == 0)
sampleDownbeat.Play();
else
sampleBeat.Play();
});
}
logoBeatContainer
.ScaleTo(1 - 0.02f * amplitudeAdjust, early_activation, Easing.Out).Then()

View File

@ -20,17 +20,18 @@ namespace osu.Game.Screens.OnlinePlay
{
protected override bool Stacked => false;
protected override bool AllowConfiguration => false;
public new Func<Mod, bool> IsValidMod
{
get => base.IsValidMod;
set => base.IsValidMod = m => m.HasImplementation && !m.RequiresConfiguration && !(m is ModAutoplay) && value(m);
set => base.IsValidMod = m => m.HasImplementation && !(m is ModAutoplay) && value(m);
}
public FreeModSelectOverlay()
{
IsValidMod = m => true;
CustomiseButton.Alpha = 0;
MultiplierSection.Alpha = 0;
DeselectAllButton.Alpha = 0;

View File

@ -59,6 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust) && !mod.RequiresConfiguration;
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust);
}
}

View File

@ -12,6 +12,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
@ -75,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 105,
Horizontal = HORIZONTAL_OVERFLOW_PADDING + 55,
Vertical = 20
},
Child = new GridContainer
@ -235,6 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING },
Child = userModsSelectOverlay = new UserModSelectOverlay
{
SelectedMods = { BindTarget = UserMods },
@ -314,12 +317,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
}
private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedModSettingsUpdate;
private void onUserModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
modSettingChangeTracker?.Dispose();
if (client.Room == null)
return;
client.ChangeUserMods(mods.NewValue);
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += onModSettingsChanged;
}
private void onModSettingsChanged(Mod mod)
{
// Debounce changes to mod settings so as to not thrash the network.
debouncedModSettingsUpdate?.Cancel();
debouncedModSettingsUpdate = Scheduler.AddDelayed(() =>
{
if (client.Room == null)
return;
client.ChangeUserMods(UserMods.Value);
}, 500);
}
private void updateBeatmapAvailability(ValueChangedEvent<BeatmapAvailability> availability)
@ -385,14 +409,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client != null)
client.LoadRequested -= onLoadRequested;
modSettingChangeTracker?.Dispose();
}
private class UserModSelectOverlay : LocalPlayerModSelectOverlay
{
public UserModSelectOverlay()
{
CustomiseButton.Alpha = 0;
}
}
}
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@ -20,10 +21,6 @@ namespace osu.Game.Screens.Select.Carousel
{
public class CarouselHeader : Container
{
private SampleChannel sampleHover;
private readonly Box hoverLayer;
public Container BorderContainer;
public readonly Bindable<CarouselItemState> State = new Bindable<CarouselItemState>(CarouselItemState.NotSelected);
@ -44,23 +41,11 @@ namespace osu.Game.Screens.Select.Carousel
Children = new Drawable[]
{
Content,
hoverLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Blending = BlendingParameters.Additive,
},
new HoverLayer()
}
};
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours)
{
sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
hoverLayer.Colour = colours.Blue.Opacity(0.1f);
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -97,18 +82,50 @@ namespace osu.Game.Screens.Select.Carousel
}
}
protected override bool OnHover(HoverEvent e)
public class HoverLayer : HoverSampleDebounceComponent
{
sampleHover?.Play();
private SampleChannel sampleHover;
hoverLayer.FadeIn(100, Easing.OutQuint);
return base.OnHover(e);
}
private Box box;
protected override void OnHoverLost(HoverLostEvent e)
{
hoverLayer.FadeOut(1000, Easing.OutQuint);
base.OnHoverLost(e);
public HoverLayer()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours)
{
InternalChild = box = new Box
{
Colour = colours.Blue.Opacity(0.1f),
Alpha = 0,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
};
sampleHover = audio.Samples.Get("SongSelect/song-ping");
}
protected override bool OnHover(HoverEvent e)
{
box.FadeIn(100, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
box.FadeOut(1000, Easing.OutQuint);
base.OnHoverLost(e);
}
public override void PlayHoverSample()
{
if (sampleHover == null) return;
sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2);
sampleHover.Play();
}
}
}
}

View File

@ -18,7 +18,6 @@ using System.Threading;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
namespace osu.Game.Screens.Select.Details
@ -83,32 +82,22 @@ namespace osu.Game.Screens.Select.Details
mods.BindValueChanged(modsChanged, true);
}
private readonly List<ISettingsItem> references = new List<ISettingsItem>();
private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedStatisticsUpdate;
private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
// TODO: find a more permanent solution for this if/when it is needed in other components.
// this is generating drawables for the only purpose of storing bindable references.
foreach (var r in references)
r.Dispose();
modSettingChangeTracker?.Dispose();
references.Clear();
ScheduledDelegate debounce = null;
foreach (var mod in mods.NewValue.OfType<IApplicableToDifficulty>())
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += m =>
{
foreach (var setting in mod.CreateSettingsControls().OfType<ISettingsItem>())
{
setting.SettingChanged += () =>
{
debounce?.Cancel();
debounce = Scheduler.AddDelayed(updateStatistics, 100);
};
if (!(m is IApplicableToDifficulty))
return;
references.Add(setting);
}
}
debouncedStatisticsUpdate?.Cancel();
debouncedStatisticsUpdate = Scheduler.AddDelayed(updateStatistics, 100);
};
updateStatistics();
}
@ -173,6 +162,7 @@ namespace osu.Game.Screens.Select.Details
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
modSettingChangeTracker?.Dispose();
starDifficultyCancellationSource?.Cancel();
}

View File

@ -30,7 +30,7 @@
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
<PackageReference Include="Sentry" Version="3.0.1" />
<PackageReference Include="SharpCompress" Version="0.27.1" />
<PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -71,7 +71,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.1" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>