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

Merge github.com:ppy/osu into patch/wiki-locale

This commit is contained in:
CloneWith 2024-08-13 11:51:14 +08:00
commit 64468bce6d
No known key found for this signature in database
GPG Key ID: F4FC0D1E91D7FFD5
124 changed files with 3163 additions and 987 deletions

View File

@ -21,7 +21,7 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2024.517.0", "version": "2024.802.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]

View File

@ -121,9 +121,7 @@ jobs:
build-only-ios: build-only-ios:
name: Build only (iOS) name: Build only (iOS)
# `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3. runs-on: macos-latest
# TODO: can be changed to `macos-latest` once `macos-13` becomes latest (currently in beta: https://github.com/actions/runner-images/tree/main#available-images)
runs-on: macos-13
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
@ -137,8 +135,5 @@ jobs:
- name: Install .NET Workloads - name: Install .NET Workloads
run: dotnet workload install maui-ios run: dotnet workload install maui-ios
- name: Select Xcode 15.2
run: sudo xcode-select -s /Applications/Xcode_15.2.app/Contents/Developer
- name: Build - name: Build
run: dotnet build -c Debug osu.iOS run: dotnet build -c Debug osu.iOS

View File

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

View File

@ -4,7 +4,6 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Tests
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test] [Test]
public void TestLegacyHUDComboCounterHidden([Values] bool withModifiedSkin) public void TestLegacyHUDComboCounterNotExistent([Values] bool withModifiedSkin)
{ {
if (withModifiedSkin) if (withModifiedSkin)
{ {
@ -29,10 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests
CreateTest(); CreateTest();
} }
AddAssert("legacy HUD combo counter hidden", () => AddAssert("legacy HUD combo counter not added", () => !Player.ChildrenOfType<LegacyComboCounter>().Any());
{
return Player.ChildrenOfType<LegacyComboCounter>().All(c => c.ChildrenOfType<Container>().Single().Alpha == 0f);
});
} }
} }
} }

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy namespace osu.Game.Rulesets.Catch.Skinning.Legacy
@ -28,76 +29,93 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{ {
if (lookup is SkinComponentsContainerLookup containerLookup) switch (lookup)
{ {
switch (containerLookup.Target) case SkinComponentsContainerLookup containerLookup:
{ if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents)
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents: return base.GetDrawableComponent(lookup);
var components = base.GetDrawableComponent(lookup) as Container;
if (providesComboCounter && components != null) // Modifications for global components.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup) as Container;
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
// Our own ruleset components default.
// todo: remove CatchSkinComponents.CatchComboCounter and refactor LegacyCatchComboCounter to be added here instead.
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
if (keyCounter != null)
{ {
// catch may provide its own combo counter; hide the default. // set the anchor to top right so that it won't squash to the return button to the top
// todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed. keyCounter.Anchor = Anchor.CentreRight;
foreach (var legacyComboCounter in components.OfType<LegacyComboCounter>()) keyCounter.Origin = Anchor.CentreRight;
legacyComboCounter.HiddenByRulesetImplementation = false; keyCounter.X = 0;
// 340px is the default height inherit from stable
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
} }
})
return components; {
} Children = new Drawable[]
}
if (lookup is CatchSkinComponentLookup catchSkinComponent)
{
switch (catchSkinComponent.Component)
{
case CatchSkinComponents.Fruit:
if (hasPear)
return new LegacyFruitPiece();
return null;
case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece();
return null;
case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null)
return new LegacyDropletPiece();
return null;
case CatchSkinComponents.Catcher:
decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
if (version < 2.3m)
{ {
if (hasOldStyleCatcherSprite()) new LegacyKeyCounterDisplay(),
return new LegacyCatcherOld();
} }
};
if (hasNewStyleCatcherSprite()) case CatchSkinComponentLookup catchSkinComponent:
return new LegacyCatcherNew(); switch (catchSkinComponent.Component)
{
case CatchSkinComponents.Fruit:
if (hasPear)
return new LegacyFruitPiece();
return null; return null;
case CatchSkinComponents.CatchComboCounter: case CatchSkinComponents.Banana:
if (providesComboCounter) if (GetTexture("fruit-bananas") != null)
return new LegacyCatchComboCounter(); return new LegacyBananaPiece();
return null; return null;
case CatchSkinComponents.HitExplosion: case CatchSkinComponents.Droplet:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite()) if (GetTexture("fruit-drop") != null)
return new LegacyHitExplosion(); return new LegacyDropletPiece();
return null; return null;
default: case CatchSkinComponents.Catcher:
throw new UnsupportedSkinComponentException(lookup); decimal version = GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value ?? 1;
}
if (version < 2.3m)
{
if (hasOldStyleCatcherSprite())
return new LegacyCatcherOld();
}
if (hasNewStyleCatcherSprite())
return new LegacyCatcherNew();
return null;
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
return new LegacyCatchComboCounter();
return null;
case CatchSkinComponents.HitExplosion:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
return new LegacyHitExplosion();
return null;
default:
throw new UnsupportedSkinComponentException(lookup);
}
} }
return base.GetDrawableComponent(lookup); return base.GetDrawableComponent(lookup);

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}); });
moveMouseToHitObject(1); moveMouseToHitObject(1);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true); AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
mergeSelection(); mergeSelection();
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}); });
moveMouseToHitObject(1); moveMouseToHitObject(1);
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems?.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection")); AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
mergeSelection(); mergeSelection();
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2)); && EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}); });
moveMouseToHitObject(1); moveMouseToHitObject(1);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true); AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
mergeSelection(); mergeSelection();

View File

@ -42,7 +42,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
base.PostProcess(); base.PostProcess();
var hitObjects = Beatmap.HitObjects as List<OsuHitObject> ?? Beatmap.HitObjects.OfType<OsuHitObject>().ToList(); ApplyStacking(Beatmap);
}
internal static void ApplyStacking(IBeatmap beatmap)
{
var hitObjects = beatmap.HitObjects as List<OsuHitObject> ?? beatmap.HitObjects.OfType<OsuHitObject>().ToList();
if (hitObjects.Count > 0) if (hitObjects.Count > 0)
{ {
@ -50,14 +55,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
foreach (var h in hitObjects) foreach (var h in hitObjects)
h.StackHeight = 0; h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6) if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
else else
applyStackingOld(Beatmap.BeatmapInfo, hitObjects); applyStackingOld(beatmap.BeatmapInfo, hitObjects);
} }
} }
private void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex) private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
{ {
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
@ -209,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
} }
} }
private void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects) private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
{ {
for (int i = 0; i < hitObjects.Count; i++) for (int i = 0; i < hitObjects.Count; i++)
{ {

View File

@ -61,12 +61,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
flashlightRating *= 0.7; flashlightRating *= 0.7;
} }
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000; double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000; double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
double baseFlashlightPerformance = 0.0; double baseFlashlightPerformance = 0.0;
if (mods.Any(h => h is OsuModFlashlight)) if (mods.Any(h => h is OsuModFlashlight))
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0; baseFlashlightPerformance = Flashlight.DifficultyToPerformance(flashlightRating);
double basePerformance = double basePerformance =
Math.Pow( Math.Pow(

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@ -139,7 +140,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (score.Mods.Any(h => h is OsuModRelax)) if (score.Mods.Any(h => h is OsuModRelax))
return 0.0; return 0.0;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0; double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@ -226,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
if (!score.Mods.Any(h => h is OsuModFlashlight)) if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0; return 0.0;
double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0; double flashlightValue = Flashlight.DifficultyToPerformance(attributes.FlashlightDifficulty);
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)

View File

@ -42,5 +42,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
} }
public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER; public override double DifficultyValue() => GetCurrentStrainPeaks().Sum() * OsuStrainSkill.DEFAULT_DIFFICULTY_MULTIPLIER;
public static double DifficultyToPerformance(double difficulty) => 25 * Math.Pow(difficulty, 2);
} }
} }

View File

@ -67,5 +67,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return difficulty * DifficultyMultiplier; return difficulty * DifficultyMultiplier;
} }
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
} }
} }

View File

@ -295,6 +295,12 @@ namespace osu.Game.Rulesets.Osu.Edit
if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius) if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius)
{ {
// if the snap target is a stacked object, snap to its unstacked position rather than its stacked position.
// this is intended to make working with stacks easier (because thanks to this, you can drag an object to any
// of the items on the stack to add an object to it, rather than having to drag to the position of the *first* object on it at all times).
if (b.Item is OsuHitObject osuObject && osuObject.StackOffset != Vector2.Zero)
closestSnapPosition = b.ToScreenSpace(b.ToLocalSpace(closestSnapPosition) - osuObject.StackOffset);
// only return distance portion, since time is not really valid // only return distance portion, since time is not really valid
snapResult = new SnapResult(closestSnapPosition, null, playfield); snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true; return true;

View File

@ -11,14 +11,14 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
public partial class OsuHitObjectInspector : HitObjectInspector public partial class OsuHitObjectInspector : HitObjectInspector
{ {
protected override void AddInspectorValues() protected override void AddInspectorValues(HitObject[] objects)
{ {
base.AddInspectorValues(); base.AddInspectorValues(objects);
if (EditorBeatmap.SelectedHitObjects.Count > 0) if (objects.Length > 0)
{ {
var firstInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MinBy(ho => ho.StartTime)!; var firstInSelection = (OsuHitObject)objects.MinBy(ho => ho.StartTime)!;
var lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!; var lastInSelection = (OsuHitObject)objects.MaxBy(ho => ho.GetEndTime())!;
Debug.Assert(firstInSelection != null && lastInSelection != null); Debug.Assert(firstInSelection != null && lastInSelection != null);

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
@ -50,12 +51,33 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
var hitObjects = selectedMovableObjects; var hitObjects = selectedMovableObjects;
var localDelta = this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
// this conditional is a rather ugly special case for stacks.
// as it turns out, adding the `EditorBeatmap.Update()` call at the end of this would cause stacked objects to jitter when moved around
// (they would stack and then unstack every frame).
// the reason for that is that the selection handling abstractions are not aware of the distinction between "displayed" and "actual" position
// which is unique to osu! due to stacking being applied as a post-processing step.
// therefore, the following loop would occur:
// - on frame 1 the blueprint is snapped to the stack's baseline position. `EditorBeatmap.Update()` applies stacking successfully,
// the blueprint moves up the stack from its original drag position.
// - on frame 2 the blueprint's position is now the *stacked* position, which is interpreted higher up as *manually performing an unstack*
// to the blueprint's unstacked position (as the machinery higher up only cares about differences in screen space position).
if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset)))
return true;
// this will potentially move the selection out of bounds... // this will potentially move the selection out of bounds...
foreach (var h in hitObjects) foreach (var h in hitObjects)
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); h.Position += localDelta;
// but this will be corrected. // but this will be corrected.
moveSelectionInBounds(); moveSelectionInBounds();
// manually update stacking.
// this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons,
// as the entire flow is too expensive to run on every movement.
Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap);
return true; return true;
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -41,139 +42,179 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup) public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{ {
if (lookup is OsuSkinComponentLookup osuComponent) switch (lookup)
{ {
switch (osuComponent.Component) case SkinComponentsContainerLookup containerLookup:
{ // Only handle per ruleset defaults here.
case OsuSkinComponents.FollowPoint: if (containerLookup.Ruleset == null)
return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS)); return base.GetDrawableComponent(lookup);
case OsuSkinComponents.SliderScorePoint: // Skin has configuration.
return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS); if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
case OsuSkinComponents.SliderFollowCircle: // Our own ruleset components default.
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE); switch (containerLookup.Target)
if (followCircleContent != null) {
return new LegacyFollowCircle(followCircleContent); case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
return new DefaultSkinComponentsContainer(container =>
{
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
return null; if (keyCounter != null)
{
// set the anchor to top right so that it won't squash to the return button to the top
keyCounter.Anchor = Anchor.CentreRight;
keyCounter.Origin = Anchor.CentreRight;
keyCounter.X = 0;
// 340px is the default height inherit from stable
keyCounter.Y = container.ToLocalSpace(new Vector2(0, container.ScreenSpaceDrawQuad.Centre.Y - 340f)).Y;
}
})
{
Children = new Drawable[]
{
new LegacyComboCounter(),
new LegacyKeyCounterDisplay(),
}
};
}
case OsuSkinComponents.SliderBall: return null;
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
return new LegacySliderBall(this);
return null; case OsuSkinComponentLookup osuComponent:
switch (osuComponent.Component)
{
case OsuSkinComponents.FollowPoint:
return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false, maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS));
case OsuSkinComponents.SliderBody: case OsuSkinComponents.SliderScorePoint:
if (hasHitCircle.Value) return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS);
return new LegacySliderBody();
return null; case OsuSkinComponents.SliderFollowCircle:
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
if (followCircleContent != null)
return new LegacyFollowCircle(followCircleContent);
case OsuSkinComponents.SliderTailHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderendcircle", false);
return null;
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
return new LegacySliderHeadHitCircle();
return null;
case OsuSkinComponents.ReverseArrow:
if (hasHitCircle.Value)
return new LegacyReverseArrow();
return null;
case OsuSkinComponents.HitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece();
return null;
case OsuSkinComponents.Cursor:
if (GetTexture("cursor") != null)
return new LegacyCursor(this);
return null;
case OsuSkinComponents.CursorTrail:
if (GetTexture("cursortrail") != null)
return new LegacyCursorTrail(this);
return null;
case OsuSkinComponents.CursorRipple:
if (GetTexture("cursor-ripple") != null)
{
var ripple = this.GetAnimation("cursor-ripple", false, false);
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
// If anyone complains about these not being applied, this can be uncommented.
//
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
// so we might be okay.
//
// if (ripple != null)
// {
// ripple.Scale = new Vector2(0.5f);
// ripple.Alpha = 0.2f;
// }
return ripple;
}
return null;
case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null)
return new LegacyCursorParticles();
return null;
case OsuSkinComponents.CursorSmoke:
if (GetTexture("cursor-smoke") != null)
return new LegacySmokeSegment();
return null;
case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle))
return null; return null;
const float hitcircle_text_scale = 0.8f; case OsuSkinComponents.SliderBall:
return new LegacySpriteText(LegacyFont.HitCircle) if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
{ return new LegacySliderBall(this);
// stable applies a blanket 0.8x scale to hitcircle fonts
Scale = new Vector2(hitcircle_text_scale),
MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale,
};
case OsuSkinComponents.SpinnerBody: return null;
bool hasBackground = GetTexture("spinner-background") != null;
if (GetTexture("spinner-top") != null && !hasBackground) case OsuSkinComponents.SliderBody:
return new LegacyNewStyleSpinner(); if (hasHitCircle.Value)
else if (hasBackground) return new LegacySliderBody();
return new LegacyOldStyleSpinner();
return null; return null;
case OsuSkinComponents.ApproachCircle: case OsuSkinComponents.SliderTailHitCircle:
if (GetTexture(@"approachcircle") != null) if (hasHitCircle.Value)
return new LegacyApproachCircle(); return new LegacyMainCirclePiece("sliderendcircle", false);
return null; return null;
default: case OsuSkinComponents.SliderHeadHitCircle:
throw new UnsupportedSkinComponentException(lookup); if (hasHitCircle.Value)
} return new LegacySliderHeadHitCircle();
return null;
case OsuSkinComponents.ReverseArrow:
if (hasHitCircle.Value)
return new LegacyReverseArrow();
return null;
case OsuSkinComponents.HitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece();
return null;
case OsuSkinComponents.Cursor:
if (GetTexture("cursor") != null)
return new LegacyCursor(this);
return null;
case OsuSkinComponents.CursorTrail:
if (GetTexture("cursortrail") != null)
return new LegacyCursorTrail(this);
return null;
case OsuSkinComponents.CursorRipple:
if (GetTexture("cursor-ripple") != null)
{
var ripple = this.GetAnimation("cursor-ripple", false, false);
// In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
// If anyone complains about these not being applied, this can be uncommented.
//
// But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
// so we might be okay.
//
// if (ripple != null)
// {
// ripple.Scale = new Vector2(0.5f);
// ripple.Alpha = 0.2f;
// }
return ripple;
}
return null;
case OsuSkinComponents.CursorParticles:
if (GetTexture("star2") != null)
return new LegacyCursorParticles();
return null;
case OsuSkinComponents.CursorSmoke:
if (GetTexture("cursor-smoke") != null)
return new LegacySmokeSegment();
return null;
case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle))
return null;
const float hitcircle_text_scale = 0.8f;
return new LegacySpriteText(LegacyFont.HitCircle)
{
// stable applies a blanket 0.8x scale to hitcircle fonts
Scale = new Vector2(hitcircle_text_scale),
MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale,
};
case OsuSkinComponents.SpinnerBody:
bool hasBackground = GetTexture("spinner-background") != null;
if (GetTexture("spinner-top") != null && !hasBackground)
return new LegacyNewStyleSpinner();
else if (hasBackground)
return new LegacyOldStyleSpinner();
return null;
case OsuSkinComponents.ApproachCircle:
if (GetTexture(@"approachcircle") != null)
return new LegacyApproachCircle();
return null;
default:
throw new UnsupportedSkinComponentException(lookup);
}
default:
return base.GetDrawableComponent(lookup);
} }
return base.GetDrawableComponent(lookup);
} }
public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)

View File

@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Timing; using osu.Framework.Timing;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -63,8 +62,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
// -1 signals that the part is unusable, and should not be drawn // -1 signals that the part is unusable, and should not be drawn
parts[i].InvalidationID = -1; parts[i].InvalidationID = -1;
} }
AddLayout(partSizeCache);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -95,12 +92,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
} }
private readonly LayoutValue<Vector2> partSizeCache = new LayoutValue<Vector2>(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence);
private Vector2 partSize => partSizeCache.IsValid
? partSizeCache.Value
: (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy);
/// <summary> /// <summary>
/// The amount of time to fade the cursor trail pieces. /// The amount of time to fade the cursor trail pieces.
/// </summary> /// </summary>
@ -156,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected void AddTrail(Vector2 position) protected void AddTrail(Vector2 position)
{ {
position = ToLocalSpace(position);
if (InterpolateMovements) if (InterpolateMovements)
{ {
if (!lastPosition.HasValue) if (!lastPosition.HasValue)
@ -174,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length; float distance = diff.Length;
Vector2 direction = diff / distance; Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier; float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0); float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
for (float d = interval; d < stopAt; d += interval) for (float d = interval; d < stopAt; d += interval)
@ -191,9 +184,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
} }
private void addPart(Vector2 screenSpacePosition) private void addPart(Vector2 localSpacePosition)
{ {
parts[currentIndex].Position = ToLocalSpace(screenSpacePosition); parts[currentIndex].Position = localSpacePosition;
parts[currentIndex].Time = time + 1; parts[currentIndex].Time = time + 1;
++parts[currentIndex].InvalidationID; ++parts[currentIndex].InvalidationID;

View File

@ -206,6 +206,15 @@ namespace osu.Game.Rulesets.Osu.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
private OsuResumeOverlay.OsuResumeOverlayInputBlocker? resumeInputBlocker;
public void AttachResumeOverlayInputBlocker(OsuResumeOverlay.OsuResumeOverlayInputBlocker resumeInputBlocker)
{
Debug.Assert(this.resumeInputBlocker == null);
this.resumeInputBlocker = resumeInputBlocker;
AddInternal(resumeInputBlocker);
}
private partial class ProxyContainer : LifetimeManagementContainer private partial class ProxyContainer : LifetimeManagementContainer
{ {
public void Add(Drawable proxy) => AddInternal(proxy); public void Add(Drawable proxy) => AddInternal(proxy);

View File

@ -33,9 +33,30 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
OsuResumeOverlayInputBlocker? inputBlocker = null;
if (drawableRuleset != null)
{
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
}
Add(cursorScaleContainer = new Container Add(cursorScaleContainer = new Container
{ {
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume } Child = clickToResumeCursor = new OsuClickToResumeCursor
{
ResumeRequested = () =>
{
// since the user had to press a button to tap the resume cursor,
// block that press event from potentially reaching a hit circle that's behind the cursor.
// we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one,
// so we rely on a dedicated input blocking component that's implanted in there to do that for us.
if (inputBlocker != null)
inputBlocker.BlockNextPress = true;
Resume();
}
}
}); });
} }
@ -115,10 +136,7 @@ namespace osu.Game.Rulesets.Osu.UI
return false; return false;
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
// When resuming with a button, we do not want the osu! input manager to see this button press and include it in the score.
// To ensure that this works correctly, schedule the resume operation one frame forward, since the resume operation enables the input manager to see input events.
Schedule(() => ResumeRequested?.Invoke());
return true; return true;
} }
@ -143,5 +161,27 @@ namespace osu.Game.Rulesets.Osu.UI
this.FadeColour(IsHovered ? Color4.White : Color4.Orange, 400, Easing.OutQuint); this.FadeColour(IsHovered ? Color4.White : Color4.Orange, 400, Easing.OutQuint);
} }
} }
public partial class OsuResumeOverlayInputBlocker : Drawable, IKeyBindingHandler<OsuAction>
{
public bool BlockNextPress;
public OsuResumeOverlayInputBlocker()
{
RelativeSizeAxes = Axes.Both;
Depth = float.MinValue;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
bool block = BlockNextPress;
BlockNextPress = false;
return block;
}
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
} }
} }

View File

@ -14,9 +14,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
{ {
} }
public override Drawable? GetDrawableComponent(ISkinComponentLookup component) public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{ {
switch (component) switch (lookup)
{ {
case GameplaySkinComponentLookup<HitResult> resultComponent: case GameplaySkinComponentLookup<HitResult> resultComponent:
// This should eventually be moved to a skin setting, when supported. // This should eventually be moved to a skin setting, when supported.
@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
break; break;
} }
return base.GetDrawableComponent(component); return base.GetDrawableComponent(lookup);
} }
} }
} }

View File

@ -468,6 +468,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestDecodeBeatmapHitObjectCoordinatesLegacy()
{
var decoder = new LegacyBeatmapDecoder();
using (var resStream = TestResources.OpenResource("hitobject-coordinates-legacy.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
var positionData = hitObjects[0] as IHasPosition;
Assert.IsNotNull(positionData);
Assert.AreEqual(new Vector2(256, 256), positionData!.Position);
}
}
[Test]
public void TestDecodeBeatmapHitObjectCoordinatesLazer()
{
var decoder = new LegacyBeatmapDecoder(LegacyBeatmapEncoder.FIRST_LAZER_VERSION);
using (var resStream = TestResources.OpenResource("hitobject-coordinates-lazer.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var hitObjects = decoder.Decode(stream).HitObjects;
var positionData = hitObjects[0] as IHasPosition;
Assert.IsNotNull(positionData);
Assert.AreEqual(new Vector2(256.99853f, 256.001f), positionData!.Position);
}
}
[Test] [Test]
public void TestDecodeBeatmapHitObjects() public void TestDecodeBeatmapHitObjects()
{ {

View File

@ -259,6 +259,44 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestNoChangesAfterDelete()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchive(out string pathOriginalSecond);
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
importBeforeUpdate!.PerformWrite(s => s.DeletePending = true);
var dateBefore = importBeforeUpdate.Value.DateAdded;
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
realm.Run(r => r.Refresh());
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
});
}
[Test] [Test]
public void TestNoChanges() public void TestNoChanges()
{ {
@ -272,21 +310,25 @@ namespace osu.Game.Tests.Database
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal)); var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
var dateBefore = importBeforeUpdate!.Value.DateAdded;
Assert.That(importBeforeUpdate, Is.Not.Null); Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null); Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value); var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
realm.Run(r => r.Refresh());
Assert.That(importAfterUpdate, Is.Not.Null); Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null); Debug.Assert(importAfterUpdate != null);
realm.Run(r => r.Refresh());
checkCount<BeatmapSetInfo>(realm, 1); checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps); checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps); checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1)); Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID)); Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
}); });
} }

View File

@ -52,10 +52,7 @@ namespace osu.Game.Tests.Editing
[SetUp] [SetUp]
public void Setup() => Schedule(() => public void Setup() => Schedule(() =>
{ {
Children = new Drawable[] Child = composer = new TestHitObjectComposer();
{
composer = new TestHitObjectComposer()
};
BeatDivisor.Value = 1; BeatDivisor.Value = 1;

View File

@ -73,7 +73,12 @@ namespace osu.Game.Tests.Resources
private static string getTempFilename() => temp_storage.GetFullPath(Guid.NewGuid() + ".osz"); private static string getTempFilename() => temp_storage.GetFullPath(Guid.NewGuid() + ".osz");
private static int importId; private static int testId = 1;
/// <summary>
/// Get a unique int value which is incremented each call.
/// </summary>
public static int GetNextTestID() => Interlocked.Increment(ref testId);
/// <summary> /// <summary>
/// Create a test beatmap set model. /// Create a test beatmap set model.
@ -88,7 +93,7 @@ namespace osu.Game.Tests.Resources
RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length]; RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length];
int setId = Interlocked.Increment(ref importId); int setId = GetNextTestID();
var metadata = new BeatmapMetadata var metadata = new BeatmapMetadata
{ {

View File

@ -0,0 +1,6 @@
osu file format v128
[HitObjects]
// Coordinates should be preserves in lazer beatmaps.
256.99853,256.001,1000,49,0,0:0:0:0:

View File

@ -0,0 +1,5 @@
osu file format v14
[HitObjects]
// Coordinates should be truncated to int values in legacy beatmaps.
256.99853,256.001,1000,49,0,0:0:0:0:

View File

@ -65,7 +65,9 @@ namespace osu.Game.Tests.Skins
// Covers default rank display // Covers default rank display
"Archives/modified-default-20230809.osk", "Archives/modified-default-20230809.osk",
// Covers legacy rank display // Covers legacy rank display
"Archives/modified-classic-20230809.osk" "Archives/modified-classic-20230809.osk",
// Covers legacy key counter
"Archives/modified-classic-20240724.osk"
}; };
/// <summary> /// <summary>

View File

@ -304,11 +304,6 @@ namespace osu.Game.Tests.Visual.Background
{ {
private bool? lastLoadTriggerCausedChange; private bool? lastLoadTriggerCausedChange;
public TestBackgroundScreenDefault()
: base(false)
{
}
public override bool Next() public override bool Next()
{ {
bool didChange = base.Next(); bool didChange = base.Next();

View File

@ -0,0 +1,89 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Tests.Visual.OnlinePlay;
using CreateRoomRequest = osu.Game.Online.Rooms.CreateRoomRequest;
namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallengeIntro : OnlinePlayTestScene
{
[Cached(typeof(MetadataClient))]
private TestMetadataClient metadataClient = new TestMetadataClient();
[Cached(typeof(INotificationOverlay))]
private NotificationOverlay notificationOverlay = new NotificationOverlay();
[BackgroundDependencyLoader]
private void load()
{
base.Content.Add(notificationOverlay);
base.Content.Add(metadataClient);
}
[Test]
[Solo]
public void TestDailyChallenge()
{
var room = new Room
{
RoomID = { Value = 1234 },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
{
new PlaylistItem(CreateAPIBeatmapSet().Beatmaps.First())
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
Category = { Value = RoomCategory.DailyChallenge }
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallengeIntro(room)));
}
[Test]
public void TestNotifications()
{
var room = new Room
{
RoomID = { Value = 1234 },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
{
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
Category = { Value = RoomCategory.DailyChallenge }
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
}
}
}

View File

@ -28,6 +28,74 @@ namespace osu.Game.Tests.Visual.Editing
private GlobalActionContainer globalActionContainer => this.ChildrenOfType<GlobalActionContainer>().Single(); private GlobalActionContainer globalActionContainer => this.ChildrenOfType<GlobalActionContainer>().Single();
[Test]
public void TestDeleteUsingMiddleMouse()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("delete with middle mouse", () => InputManager.Click(MouseButton.Middle));
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty);
}
[Test]
public void TestDeleteUsingShiftRightClick()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("delete with right mouse", () =>
{
InputManager.PressKey(Key.ShiftLeft);
InputManager.Click(MouseButton.Right);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddAssert("circle removed", () => EditorBeatmap.HitObjects, () => Is.Empty);
}
[Test]
public void TestContextMenu()
{
AddStep("select circle placement tool", () => InputManager.Key(Key.Number2));
AddStep("move mouse to center of playfield", () => InputManager.MoveMouseTo(this.ChildrenOfType<Playfield>().Single()));
AddStep("place circle", () => InputManager.Click(MouseButton.Left));
AddAssert("one circle added", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddStep("delete with right mouse", () =>
{
InputManager.Click(MouseButton.Right);
});
AddAssert("circle not removed", () => EditorBeatmap.HitObjects, () => Has.One.Items);
AddAssert("circle selected", () => EditorBeatmap.SelectedHitObjects, () => Has.One.Items);
}
[Test]
[Solo]
public void TestCommitPlacementViaRightClick()
{
Playfield playfield = null!;
AddStep("select slider placement tool", () => InputManager.Key(Key.Number3));
AddStep("move mouse to top left of playfield", () =>
{
playfield = this.ChildrenOfType<Playfield>().Single();
var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
AddStep("begin placement", () => InputManager.Click(MouseButton.Left));
AddStep("move mouse to bottom right of playfield", () =>
{
var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4;
InputManager.MoveMouseTo(location);
});
AddStep("confirm via right click", () => InputManager.Click(MouseButton.Right));
AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1));
}
[Test] [Test]
public void TestCommitPlacementViaGlobalAction() public void TestCommitPlacementViaGlobalAction()
{ {

View File

@ -9,7 +9,6 @@ using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -45,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing. // best way to check without exposing.
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First(); private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
private Drawable keyCounterFlow => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<FillFlowContainer<KeyCounter>>().Single(); private Drawable keyCounterContent => hudOverlay.ChildrenOfType<KeyCounterDisplay>().First().ChildrenOfType<Drawable>().Skip(1).First();
public TestSceneHUDOverlay() public TestSceneHUDOverlay()
{ {
@ -79,7 +78,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("showhud is set", () => hudOverlay.ShowHud.Value); AddAssert("showhud is set", () => hudOverlay.ShowHud.Value);
AddAssert("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); AddAssert("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0));
AddAssert("key counter flow is visible", () => keyCounterFlow.IsPresent); AddAssert("key counter flow is visible", () => keyCounterContent.IsPresent);
AddAssert("pause button is visible", () => hudOverlay.HoldToQuit.IsPresent); AddAssert("pause button is visible", () => hudOverlay.HoldToQuit.IsPresent);
} }
@ -104,7 +103,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
// Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above. // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above.
AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent); AddAssert("key counter flow not affected", () => keyCounterContent.IsPresent);
} }
[Test] [Test]
@ -150,11 +149,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0)); AddUntilStep("hidetarget is hidden", () => hideTarget.Alpha, () => Is.LessThanOrEqualTo(0));
AddUntilStep("key counters hidden", () => !keyCounterFlow.IsPresent); AddUntilStep("key counters hidden", () => !keyCounterContent.IsPresent);
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
AddUntilStep("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0)); AddUntilStep("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0));
AddUntilStep("key counters still hidden", () => !keyCounterFlow.IsPresent); AddUntilStep("key counters still hidden", () => !keyCounterContent.IsPresent);
} }
[Test] [Test]

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -56,6 +57,11 @@ namespace osu.Game.Tests.Visual.Gameplay
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Scale = new Vector2(1, -1) Scale = new Vector2(1, -1)
}, },
new LegacyKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
},
new FillFlowContainer new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -89,6 +95,12 @@ namespace osu.Game.Tests.Visual.Gameplay
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Rotation = 90, Rotation = 90,
}, },
new LegacyKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Rotation = 90,
},
} }
}, },
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -13,6 +14,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -32,6 +34,23 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } = null!; private AudioManager audioManager { get; set; } = null!;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects =
{
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 0,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 5000,
}
}
};
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
@ -70,18 +89,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single())); AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
// Z key was released before pause, resuming should not trigger it
checkKey(() => counter, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 1, false);
AddStep("press Z", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 2, true); checkKey(() => counter, 2, true);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 2, false); checkKey(() => counter, 2, false);
AddStep("press Z", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 3, true);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 3, false);
} }
[Test] [Test]
@ -90,30 +107,29 @@ namespace osu.Game.Tests.Visual.Gameplay
KeyCounter counter = null!; KeyCounter counter = null!;
loadPlayer(() => new ManiaRuleset()); loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Key1)); AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
checkKey(() => counter, 0, false); checkKey(() => counter, 0, false);
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, true); checkKey(() => counter, 1, true);
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false); checkKey(() => counter, 1, false);
AddStep("pause", () => Player.Pause()); AddStep("pause", () => Player.Pause());
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, false); checkKey(() => counter, 1, false);
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false); checkKey(() => counter, 1, false);
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning); AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
checkKey(() => counter, 1, false);
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 2, true); checkKey(() => counter, 2, true);
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 2, false); checkKey(() => counter, 2, false);
} }
@ -145,8 +161,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single())); AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counterZ, 1, false); checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, false); checkKey(() => counterX, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
} }
[Test] [Test]
@ -155,12 +174,12 @@ namespace osu.Game.Tests.Visual.Gameplay
KeyCounter counter = null!; KeyCounter counter = null!;
loadPlayer(() => new ManiaRuleset()); loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Key1)); AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
AddStep("pause", () => Player.Pause()); AddStep("pause", () => Player.Pause());
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, true); checkKey(() => counter, 1, true);
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
@ -202,12 +221,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single())); AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counterZ, 1, false); checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, true); checkKey(() => counterX, 1, true);
AddStep("release X", () => InputManager.ReleaseKey(Key.X)); AddStep("release X", () => InputManager.ReleaseKey(Key.X));
checkKey(() => counterZ, 1, false);
checkKey(() => counterX, 1, false); checkKey(() => counterX, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
} }
[Test] [Test]
@ -216,24 +237,50 @@ namespace osu.Game.Tests.Visual.Gameplay
KeyCounter counter = null!; KeyCounter counter = null!;
loadPlayer(() => new ManiaRuleset()); loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Key1)); AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, true); checkKey(() => counter, 1, true);
AddStep("pause", () => Player.Pause()); AddStep("pause", () => Player.Pause());
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning); AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
checkKey(() => counter, 1, true); checkKey(() => counter, 1, true);
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false); checkKey(() => counter, 1, false);
} }
[Test]
public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked()
{
KeyCounter counter = null!;
loadPlayer(() => new OsuRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.LeftButton));
AddStep("pause", () => Player.Pause());
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
// ensure the input manager receives the Z button press...
checkKey(() => counter, 1, true);
AddAssert("button is pressed in kbc", () => Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Single() == OsuAction.LeftButton);
// ...but also ensure the hit circle in front of the cursor isn't hit by checking max combo.
AddAssert("circle not hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(0));
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 1, false);
AddAssert("button is released in kbc", () => !Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Any());
}
private void loadPlayer(Func<Ruleset> createRuleset) private void loadPlayer(Func<Ruleset> createRuleset)
{ {
AddStep("set ruleset", () => currentRuleset = createRuleset()); AddStep("set ruleset", () => currentRuleset = createRuleset());
@ -241,9 +288,10 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinComponentsContainer>().All(s => s.ComponentsLoaded)); AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinComponentsContainer>().All(s => s.ComponentsLoaded));
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(20000)); AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(20000).Within(500)); AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500));
AddAssert("not in break", () => !Player.IsBreakTime.Value); AddAssert("not in break", () => !Player.IsBreakTime.Value);
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield));
} }
private void checkKey(Func<KeyCounter> counter, int count, bool active) private void checkKey(Func<KeyCounter> counter, int count, bool active)

View File

@ -7,21 +7,25 @@ using System.Linq;
using System.Text; using System.Text;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor; using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Skinning.Components; using osu.Game.Skinning.Components;
using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -39,6 +43,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached] [Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard(); public readonly EditorClipboard Clipboard = new EditorClipboard();
[Resolved]
private SkinManager skins { get; set; } = null!;
private SkinComponentsContainer targetContainer => Player.ChildrenOfType<SkinComponentsContainer>().First(); private SkinComponentsContainer targetContainer => Player.ChildrenOfType<SkinComponentsContainer>().First();
[SetUpSteps] [SetUpSteps]
@ -46,6 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
base.SetUpSteps(); base.SetUpSteps();
AddStep("reset skin", () => skins.CurrentSkinInfo.SetDefault());
AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded); AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded);
AddStep("reload skin editor", () => AddStep("reload skin editor", () =>
@ -369,6 +377,93 @@ namespace osu.Game.Tests.Visual.Gameplay
() => Is.EqualTo(3)); () => Is.EqualTo(3));
} }
private SkinComponentsContainer globalHUDTarget => Player.ChildrenOfType<SkinComponentsContainer>()
.Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset == null);
private SkinComponentsContainer rulesetHUDTarget => Player.ChildrenOfType<SkinComponentsContainer>()
.Single(c => c.Lookup.Target == SkinComponentsContainerLookup.TargetArea.MainHUDComponents && c.Lookup.Ruleset != null);
[Test]
public void TestMigrationArgon()
{
Live<SkinInfo> importedSkin = null!;
AddStep("import old argon skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"argon-layout-version-0.osk").SkinInfo);
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType<ArgonComboCounter>().Any());
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<ArgonComboCounter>().Count() == 1);
AddStep("add combo to global target", () => globalHUDTarget.Add(new ArgonComboCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2f),
}));
AddStep("save skin", () => skins.Save(skins.CurrentSkin.Value));
AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault());
AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin);
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType<ArgonComboCounter>().Count() == 1);
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<ArgonComboCounter>().Count() == 1);
}
[Test]
public void TestMigrationTriangles()
{
Live<SkinInfo> importedSkin = null!;
AddStep("import old triangles skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"triangles-layout-version-0.osk").SkinInfo);
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType<DefaultComboCounter>().Any());
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<DefaultComboCounter>().Count() == 1);
AddStep("add combo to global target", () => globalHUDTarget.Add(new DefaultComboCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2f),
}));
AddStep("save skin", () => skins.Save(skins.CurrentSkin.Value));
AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault());
AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin);
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType<DefaultComboCounter>().Count() == 1);
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<DefaultComboCounter>().Count() == 1);
}
[Test]
public void TestMigrationLegacy()
{
Live<SkinInfo> importedSkin = null!;
AddStep("import old classic skin", () => skins.CurrentSkinInfo.Value = importedSkin = importSkinFromArchives(@"classic-layout-version-0.osk").SkinInfo);
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
AddAssert("no combo in global target", () => !globalHUDTarget.Components.OfType<LegacyComboCounter>().Any());
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
AddStep("add combo to global target", () => globalHUDTarget.Add(new LegacyComboCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(2f),
}));
AddStep("save skin", () => skins.Save(skins.CurrentSkin.Value));
AddStep("select another skin", () => skins.CurrentSkinInfo.SetDefault());
AddStep("select skin again", () => skins.CurrentSkinInfo.Value = importedSkin);
AddUntilStep("wait for load", () => globalHUDTarget.ComponentsLoaded && rulesetHUDTarget.ComponentsLoaded);
AddAssert("combo placed in global target", () => globalHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyComboCounter>().Count() == 1);
}
private Skin importSkinFromArchives(string filename)
{
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
return imported.PerformRead(skinInfo => skins.GetSkin(skinInfo));
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler

View File

@ -4,7 +4,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -28,17 +27,5 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("reset combo", () => scoreProcessor.Combo.Value = 0); AddStep("reset combo", () => scoreProcessor.Combo.Value = 0);
} }
[Test]
public void TestLegacyComboCounterHiddenByRulesetImplementation()
{
AddToggleStep("toggle legacy hidden by ruleset", visible =>
{
foreach (var legacyCounter in this.ChildrenOfType<LegacyComboCounter>())
legacyCounter.HiddenByRulesetImplementation = visible;
});
AddRepeatStep("increase combo", () => scoreProcessor.Combo.Value++, 10);
}
} }
} }

View File

@ -16,12 +16,13 @@ using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Gameplay; using osu.Game.Tests.Gameplay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -91,10 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
SetContents(_ => SetContents(_ =>
{ {
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>()); hudOverlay = new HUDOverlay(new DrawableOsuRuleset(new OsuRuleset(), new OsuBeatmap()), Array.Empty<Mod>());
// Add any key just to display the key counter visually.
hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space));
action?.Invoke(hudOverlay); action?.Invoke(hudOverlay);

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneSkinnableKeyCounter : SkinnableHUDComponentTestScene
{
[Cached]
private readonly InputCountController controller = new InputCountController();
public override void SetUpSteps()
{
AddStep("create dependencies", () =>
{
Add(controller);
controller.Add(new KeyCounterKeyboardTrigger(Key.Z));
controller.Add(new KeyCounterKeyboardTrigger(Key.X));
controller.Add(new KeyCounterKeyboardTrigger(Key.C));
controller.Add(new KeyCounterKeyboardTrigger(Key.V));
foreach (var trigger in controller.Triggers)
Add(trigger);
});
base.SetUpSteps();
}
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
protected override Drawable CreateDefaultImplementation() => new ArgonKeyCounterDisplay();
protected override Drawable CreateLegacyImplementation() => new LegacyKeyCounterDisplay();
}
}

View File

@ -1,11 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osuTK.Input; using osuTK.Input;
@ -23,6 +27,49 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("disable return to top on idle", () => Game.ChildrenOfType<ButtonSystem>().Single().ReturnToTopOnIdle = false); AddStep("disable return to top on idle", () => Game.ChildrenOfType<ButtonSystem>().Single().ReturnToTopOnIdle = false);
} }
[Test]
public void TestDailyChallenge()
{
AddStep("set up API", () => ((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case GetRoomRequest getRoomRequest:
if (getRoomRequest.RoomId != 1234)
return false;
var beatmap = CreateAPIBeatmap();
beatmap.OnlineID = 1001;
getRoomRequest.TriggerSuccess(new Room
{
RoomID = { Value = 1234 },
Name = { Value = "Aug 8, 2024" },
Playlist =
{
new PlaylistItem(beatmap)
},
StartDate = { Value = DateTimeOffset.Now.AddMinutes(-30) },
EndDate = { Value = DateTimeOffset.Now.AddSeconds(60) }
});
return true;
default:
return false;
}
});
AddStep("beatmap of the day active", () => Game.ChildrenOfType<IMetadataClient>().Single().DailyChallengeUpdated(new DailyChallengeInfo
{
RoomID = 1234,
}));
AddStep("enter menu", () => InputManager.Key(Key.P));
AddStep("enter submenu", () => InputManager.Key(Key.P));
AddStep("enter daily challenge", () => InputManager.Key(Key.D));
AddUntilStep("wait for daily challenge screen", () => Game.ScreenStack.CurrentScreen, Is.TypeOf<Screens.OnlinePlay.DailyChallenge.DailyChallenge>);
}
[Test] [Test]
public void TestOnlineMenuBannerTrusted() public void TestOnlineMenuBannerTrusted()
{ {

View File

@ -61,7 +61,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select difficulty adjust", () => freeModSelectOverlay.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); AddStep("select difficulty adjust", () => freeModSelectOverlay.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() });
AddWaitStep("wait some", 3); AddWaitStep("wait some", 3);
AddAssert("customisation area not expanded", () => !this.ChildrenOfType<ModCustomisationPanel>().Single().Expanded.Value); AddAssert("customisation area not expanded",
() => this.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
} }
[Test] [Test]

View File

@ -139,8 +139,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void addRandomPlayer() private void addRandomPlayer()
{ {
int randomUser = RNG.Next(200000, 500000); int id = TestResources.GetNextTestID();
multiplayerClient.AddUser(new APIUser { Id = randomUser, Username = $"user {randomUser}" }); multiplayerClient.AddUser(new APIUser { Id = id, Username = $"user {id}" });
} }
private void removeLastUser() private void removeLastUser()

View File

@ -9,13 +9,13 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Chat.ChannelList; using osu.Game.Overlays.Chat.ChannelList;
using osu.Game.Overlays.Chat.Listing; using osu.Game.Overlays.Chat.Listing;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual.Online
private Channel createRandomPublicChannel() private Channel createRandomPublicChannel()
{ {
int id = RNG.Next(0, 10000); int id = TestResources.GetNextTestID();
return new Channel return new Channel
{ {
Name = $"#channel-{id}", Name = $"#channel-{id}",
@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.Online
private Channel createRandomPrivateChannel() private Channel createRandomPrivateChannel()
{ {
int id = RNG.Next(0, 10000); int id = TestResources.GetNextTestID();
return new Channel(new APIUser return new Channel(new APIUser
{ {
Id = id, Id = id,
@ -181,7 +181,7 @@ namespace osu.Game.Tests.Visual.Online
private Channel createRandomAnnounceChannel() private Channel createRandomAnnounceChannel()
{ {
int id = RNG.Next(0, 10000); int id = TestResources.GetNextTestID();
return new Channel return new Channel
{ {
Name = $"Announce {id}", Name = $"Announce {id}",

View File

@ -19,7 +19,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -33,6 +32,7 @@ using osu.Game.Overlays.Chat.ChannelList;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Online
return true; return true;
case PostMessageRequest postMessage: case PostMessageRequest postMessage:
postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000)) postMessage.TriggerSuccess(new Message(TestResources.GetNextTestID())
{ {
Content = postMessage.Message.Content, Content = postMessage.Message.Content,
ChannelId = postMessage.Message.ChannelId, ChannelId = postMessage.Message.ChannelId,
@ -719,7 +719,8 @@ namespace osu.Game.Tests.Visual.Online
private Channel createPrivateChannel() private Channel createPrivateChannel()
{ {
int id = RNG.Next(0, DummyAPIAccess.DUMMY_USER_ID - 1); int id = TestResources.GetNextTestID();
return new Channel(new APIUser return new Channel(new APIUser
{ {
Id = id, Id = id,

View File

@ -33,6 +33,34 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
[Test]
public void TestMention()
{
AddStep("add normal message", () => channel.AddNewMessages(
new Message(1)
{
Sender = new APIUser
{
Id = 2,
Username = "TestUser2"
},
Content = "Hello how are you today?",
Timestamp = new DateTimeOffset(2021, 12, 11, 13, 33, 24, TimeSpan.Zero)
}));
AddStep("add mention", () => channel.AddNewMessages(
new Message(2)
{
Sender = new APIUser
{
Id = 2,
Username = "TestUser2"
},
Content = $"Hello {API.LocalUser.Value.Username} how are you today?",
Timestamp = new DateTimeOffset(2021, 12, 11, 13, 33, 25, TimeSpan.Zero)
}));
}
[Test] [Test]
public void TestDaySeparators() public void TestDaySeparators()
{ {

View File

@ -0,0 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Rulesets.Osu;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene
{
[Cached]
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo));
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
protected override void LoadComplete()
{
base.LoadComplete();
DailyChallengeStatsDisplay display = null!;
AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v));
AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v));
AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v));
AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v));
AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v));
AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v));
AddSliderStep("playcount", 0, 999, 0, v => update(s => s.PlayCount = v));
AddStep("create", () =>
{
Clear();
Add(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background2,
});
Add(display = new DailyChallengeStatsDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1f),
User = { BindTarget = User },
});
});
AddStep("hover", () => InputManager.MoveMouseTo(display));
}
private void update(Action<APIUserDailyChallengeStatistics> change)
{
change.Invoke(User.Value!.User.DailyChallengeStatistics);
User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset);
}
}
}

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -24,7 +25,17 @@ namespace osu.Game.Tests.Visual.Online
[SetUpSteps] [SetUpSteps]
public void SetUp() public void SetUp()
{ {
AddStep("create profile overlay", () => Child = profile = new UserProfileOverlay()); AddStep("create profile overlay", () =>
{
profile = new UserProfileOverlay();
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(UserProfileOverlay), profile) },
Child = profile,
};
});
} }
[Test] [Test]
@ -131,6 +142,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = CountryCode.JP, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue, ProfileHue = hue,
PlayMode = "osu",
}); });
return true; return true;
} }
@ -174,6 +186,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = CountryCode.JP, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue, ProfileHue = hue,
PlayMode = "osu",
})); }));
int hue2 = 0; int hue2 = 0;
@ -189,6 +202,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = CountryCode.JP, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue2, ProfileHue = hue2,
PlayMode = "osu",
})); }));
} }
@ -282,6 +296,15 @@ namespace osu.Game.Tests.Visual.Online
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png", ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png",
}, },
}, },
DailyChallengeStatistics = new APIUserDailyChallengeStatistics
{
DailyStreakCurrent = 231,
WeeklyStreakCurrent = 18,
DailyStreakBest = 370,
WeeklyStreakBest = 51,
Top10PercentPlacements = 345,
Top50PercentPlacements = 427,
},
Title = "osu!volunteer", Title = "osu!volunteer",
Colour = "ff0000", Colour = "ff0000",
Achievements = Array.Empty<APIUserAchievement>(), Achievements = Array.Empty<APIUserAchievement>(),

View File

@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
} }
} }
@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible); AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
} }
} }

View File

@ -58,6 +58,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
new PlaylistItem(beatmap) new PlaylistItem(beatmap)
}, },
StartDate = { Value = DateTimeOffset.Now.AddMinutes(-5) },
EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) } EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) }
}); });
return true; return true;
@ -95,8 +96,13 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
}; };
}); });
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
AddStep("clear notifications", () =>
{
foreach (var notification in notificationOverlay.AllNotifications)
notification.Close(runFlingAnimation: false);
});
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null)); AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
@ -105,7 +111,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
RoomID = 1234, RoomID = 1234,
})); }));
AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1)); AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
} }
} }
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -10,6 +11,8 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
@ -19,10 +22,15 @@ namespace osu.Game.Tests.Visual.UserInterface
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private ModCustomisationPanel panel = null!; private ModCustomisationPanel panel = null!;
private ModCustomisationHeader header = null!;
private Container content = null!;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
SelectedMods.Value = Array.Empty<Mod>();
InputManager.MoveMouseTo(Vector2.One);
Child = new Container Child = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -36,6 +44,9 @@ namespace osu.Game.Tests.Visual.UserInterface
SelectedMods = { BindTarget = SelectedMods }, SelectedMods = { BindTarget = SelectedMods },
} }
}; };
header = panel.Children.OfType<ModCustomisationHeader>().First();
content = panel.Children.OfType<Container>().First();
}); });
[Test] [Test]
@ -44,23 +55,112 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set DT", () => AddStep("set DT", () =>
{ {
SelectedMods.Value = new[] { new OsuModDoubleTime() }; SelectedMods.Value = new[] { new OsuModDoubleTime() };
panel.Enabled.Value = panel.Expanded.Value = true; panel.Enabled.Value = true;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded;
}); });
AddStep("set DA", () => AddStep("set DA", () =>
{ {
SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }; SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() };
panel.Enabled.Value = panel.Expanded.Value = true; panel.Enabled.Value = true;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded;
}); });
AddStep("set FL+WU+DA+AD", () => AddStep("set FL+WU+DA+AD", () =>
{ {
SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() }; SelectedMods.Value = new Mod[] { new OsuModFlashlight(), new ModWindUp(), new OsuModDifficultyAdjust(), new OsuModApproachDifferent() };
panel.Enabled.Value = panel.Expanded.Value = true; panel.Enabled.Value = true;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded;
}); });
AddStep("set empty", () => AddStep("set empty", () =>
{ {
SelectedMods.Value = Array.Empty<Mod>(); SelectedMods.Value = Array.Empty<Mod>();
panel.Enabled.Value = panel.Expanded.Value = false; panel.Enabled.Value = false;
panel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Collapsed;
}); });
} }
[Test]
public void TestHoverDoesNotExpandWhenNoCustomisableMods()
{
AddStep("hover header", () => InputManager.MoveMouseTo(header));
checkExpanded(false);
AddStep("hover content", () => InputManager.MoveMouseTo(content));
checkExpanded(false);
AddStep("left from content", () => InputManager.MoveMouseTo(Vector2.One));
}
[Test]
public void TestHoverExpandsWithCustomisableMods()
{
AddStep("add customisable mod", () =>
{
SelectedMods.Value = new[] { new OsuModDoubleTime() };
panel.Enabled.Value = true;
});
AddStep("hover header", () => InputManager.MoveMouseTo(header));
checkExpanded(true);
AddStep("move to content", () => InputManager.MoveMouseTo(content));
checkExpanded(true);
AddStep("move away", () => InputManager.MoveMouseTo(Vector2.One));
checkExpanded(false);
AddStep("hover header", () => InputManager.MoveMouseTo(header));
checkExpanded(true);
AddStep("move away", () => InputManager.MoveMouseTo(Vector2.One));
checkExpanded(false);
}
[Test]
public void TestExpandedStatePersistsWhenClicked()
{
AddStep("add customisable mod", () =>
{
SelectedMods.Value = new[] { new OsuModDoubleTime() };
panel.Enabled.Value = true;
});
AddStep("hover header", () => InputManager.MoveMouseTo(header));
checkExpanded(true);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkExpanded(false);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkExpanded(true);
AddStep("move away", () => InputManager.MoveMouseTo(Vector2.One));
checkExpanded(true);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkExpanded(false);
}
[Test]
public void TestHoverExpandsAndCollapsesWhenHeaderClicked()
{
AddStep("add customisable mod", () =>
{
SelectedMods.Value = new[] { new OsuModDoubleTime() };
panel.Enabled.Value = true;
});
AddStep("hover header", () => InputManager.MoveMouseTo(header));
checkExpanded(true);
AddStep("click", () => InputManager.Click(MouseButton.Left));
checkExpanded(false);
}
private void checkExpanded(bool expanded)
{
AddUntilStep(expanded ? "is expanded" : "not expanded", () => panel.ExpandedState.Value,
() => expanded ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
}
} }
} }

View File

@ -57,6 +57,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0)); AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
AddStep("reset mods", () => SelectedMods.SetDefault()); AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("reset config", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true)); AddStep("reset config", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true));
AddStep("reset mouse", () => InputManager.MoveMouseTo(Vector2.One));
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo)); AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
AddStep("set up presets", () => AddStep("set up presets", () =>
{ {
@ -998,7 +999,9 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left)); AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left));
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus); AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("customisation panel closed by click", () => !this.ChildrenOfType<ModCustomisationPanel>().Single().Expanded.Value); AddAssert("customisation panel closed by click",
() => this.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
if (textSearchStartsActive) if (textSearchStartsActive)
AddAssert("search focused", () => this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus); AddAssert("search focused", () => this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
@ -1021,7 +1024,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private void assertCustomisationToggleState(bool disabled, bool active) private void assertCustomisationToggleState(bool disabled, bool active)
{ {
AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().Enabled.Value == !disabled); AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().Enabled.Value == !disabled);
AddAssert($"customisation panel is {(active ? "" : "not ")}active", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().Expanded.Value == active); AddAssert($"customisation panel is {(active ? "" : "not ")}active",
() => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => active ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
} }
private T getSelectedMod<T>() where T : Mod => SelectedMods.Value.OfType<T>().Single(); private T getSelectedMod<T>() where T : Mod => SelectedMods.Value.OfType<T>().Single();

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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneOsuTooltip : OsuManualInputManagerTestScene
{
private TestTooltipContainer container = null!;
private static readonly string[] test_case_tooltip_string =
[
"Hello!!",
string.Concat(Enumerable.Repeat("Hello ", 100)),
//TODO: o!f issue: https://github.com/ppy/osu-framework/issues/5007
//Enable after o!f fixed
// $"H{new string('e', 500)}llo",
];
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(100),
Children = new Drawable[]
{
new Box
{
Colour = Colour4.Red.Opacity(0.5f),
RelativeSizeAxes = Axes.Both,
},
container = new TestTooltipContainer
{
RelativeSizeAxes = Axes.Both,
Child = new OsuSpriteText
{
Text = "Hover me!",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 50)
}
},
},
};
});
[TestCaseSource(nameof(test_case_tooltip_string))]
public void TestTooltipBasic(string text)
{
AddStep("Set tooltip content", () => container.TooltipText = text);
AddStep("Move mouse to container", () => InputManager.MoveMouseTo(new Vector2(InputManager.ScreenSpaceDrawQuad.Centre.X, InputManager.ScreenSpaceDrawQuad.Centre.Y)));
OsuTooltipContainer.OsuTooltip? tooltip = null!;
AddUntilStep("Wait for the tooltip shown", () =>
{
tooltip = container.FindClosestParent<OsuTooltipContainer>().ChildrenOfType<OsuTooltipContainer.OsuTooltip>().FirstOrDefault();
return tooltip != null && tooltip.Alpha == 1;
});
AddAssert("Check tooltip is under width limit", () => tooltip != null && tooltip.Width <= 500);
}
internal sealed partial class TestTooltipContainer : Container, IHasTooltip
{
public LocalisableString TooltipText { get; set; }
}
}
}

View File

@ -43,6 +43,8 @@ namespace osu.Game.Beatmaps
public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
{ {
var originalDateAdded = original.DateAdded;
Guid originalId = original.ID; Guid originalId = original.ID;
var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false); var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false);
@ -57,8 +59,11 @@ namespace osu.Game.Beatmaps
// If there were no changes, ensure we don't accidentally nuke ourselves. // If there were no changes, ensure we don't accidentally nuke ourselves.
if (first.ID == originalId) if (first.ID == originalId)
{ {
first.PerformRead(s => first.PerformWrite(s =>
{ {
// Transfer local values which should be persisted across a beatmap update.
s.DateAdded = originalDateAdded;
// Re-run processing even in this case. We might have outdated metadata. // Re-run processing even in this case. We might have outdated metadata.
ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst); ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst);
}); });
@ -79,7 +84,7 @@ namespace osu.Game.Beatmaps
original.DeletePending = true; original.DeletePending = true;
// Transfer local values which should be persisted across a beatmap update. // Transfer local values which should be persisted across a beatmap update.
updated.DateAdded = original.DateAdded; updated.DateAdded = originalDateAdded;
transferCollectionReferences(realm, original, updated); transferCollectionReferences(realm, original, updated);
@ -278,6 +283,9 @@ namespace osu.Game.Beatmaps
protected override void UndeleteForReuse(BeatmapSetInfo existing) protected override void UndeleteForReuse(BeatmapSetInfo existing)
{ {
if (!existing.DeletePending)
return;
base.UndeleteForReuse(existing); base.UndeleteForReuse(existing);
existing.DateAdded = DateTimeOffset.UtcNow; existing.DateAdded = DateTimeOffset.UtcNow;
} }

View File

@ -563,7 +563,7 @@ namespace osu.Game.Beatmaps
remove => workingBeatmapCache.OnInvalidated -= value; remove => workingBeatmapCache.OnInvalidated -= value;
} }
public override bool IsAvailableLocally(BeatmapSetInfo model) => Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID)); public override bool IsAvailableLocally(BeatmapSetInfo model) => Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID && !s.DeletePending));
#endregion #endregion

View File

@ -18,6 +18,12 @@ namespace osu.Game.Beatmaps.Formats
{ {
public const int LATEST_VERSION = 14; public const int LATEST_VERSION = 14;
/// <summary>
/// The .osu format (beatmap) version.
///
/// osu!stable's versions end at <see cref="LATEST_VERSION"/>.
/// osu!lazer's versions starts at <see cref="LegacyBeatmapEncoder.FIRST_LAZER_VERSION"/>.
/// </summary>
protected readonly int FormatVersion; protected readonly int FormatVersion;
protected LegacyDecoder(int version) protected LegacyDecoder(int version)
@ -93,14 +99,8 @@ namespace osu.Game.Beatmaps.Formats
return line; return line;
} }
protected void HandleColours<TModel>(TModel output, string line, bool allowAlpha) private Color4 convertSettingStringToColor4(string[] split, bool allowAlpha, KeyValuePair<string, string> pair)
{ {
var pair = SplitKeyVal(line);
bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal);
string[] split = pair.Value.Split(',');
if (split.Length != 3 && split.Length != 4) if (split.Length != 3 && split.Length != 4)
throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B or R,G,B,A): {pair.Value}"); throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B or R,G,B,A): {pair.Value}");
@ -116,6 +116,18 @@ namespace osu.Game.Beatmaps.Formats
throw new InvalidOperationException(@"Color must be specified with 8-bit integer components"); throw new InvalidOperationException(@"Color must be specified with 8-bit integer components");
} }
return colour;
}
protected void HandleColours<TModel>(TModel output, string line, bool allowAlpha)
{
var pair = SplitKeyVal(line);
string[] split = pair.Value.Split(',');
Color4 colour = convertSettingStringToColor4(split, allowAlpha, pair);
bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal);
if (isCombo) if (isCombo)
{ {
if (!(output is IHasComboColours tHasComboColours)) return; if (!(output is IHasComboColours tHasComboColours)) return;

View File

@ -43,6 +43,9 @@ namespace osu.Game.Collections
// //
// if we want to support user sorting (but changes will need to be made to realm to persist). // if we want to support user sorting (but changes will need to be made to realm to persist).
ShowDragHandle.Value = false; ShowDragHandle.Value = false;
Masking = true;
CornerRadius = item_height / 2;
} }
protected override Drawable CreateContent() => new ItemContent(Model); protected override Drawable CreateContent() => new ItemContent(Model);
@ -50,7 +53,7 @@ namespace osu.Game.Collections
/// <summary> /// <summary>
/// The main content of the <see cref="DrawableCollectionListItem"/>. /// The main content of the <see cref="DrawableCollectionListItem"/>.
/// </summary> /// </summary>
private partial class ItemContent : CircularContainer private partial class ItemContent : CompositeDrawable
{ {
private readonly Live<BeatmapCollection> collection; private readonly Live<BeatmapCollection> collection;
@ -65,13 +68,12 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = item_height; Height = item_height;
Masking = true;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Children = new[] InternalChildren = new[]
{ {
collection.IsManaged collection.IsManaged
? new DeleteButton(collection) ? new DeleteButton(collection)
@ -132,7 +134,7 @@ namespace osu.Game.Collections
} }
} }
public partial class DeleteButton : CompositeDrawable public partial class DeleteButton : OsuClickableContainer
{ {
public Func<Vector2, bool> IsTextBoxHovered = null!; public Func<Vector2, bool> IsTextBoxHovered = null!;
@ -155,7 +157,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
InternalChild = fadeContainer = new Container Child = fadeContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0.1f, Alpha = 0.1f,
@ -176,6 +178,14 @@ namespace osu.Game.Collections
} }
} }
}; };
Action = () =>
{
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0)
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
};
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos);
@ -195,12 +205,7 @@ namespace osu.Game.Collections
{ {
background.FlashColour(Color4.White, 150); background.FlashColour(Color4.White, 150);
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0) return base.OnClick(e);
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
return true;
} }
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));

View File

@ -67,13 +67,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Username, string.Empty); SetDefault(OsuSetting.Username, string.Empty);
SetDefault(OsuSetting.Token, string.Empty); SetDefault(OsuSetting.Token, string.Empty);
#pragma warning disable CS0618 // Type or member is obsolete SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, true);
// this default set MUST remain despite the setting being deprecated, because `SetDefault()` calls are implicitly used to declare the type returned for the lookup.
// if this is removed, the setting will be interpreted as a string, and `Migrate()` will fail due to cast failure.
// can be removed 20240618
SetDefault(OsuSetting.AutomaticallyDownloadWhenSpectating, false);
#pragma warning restore CS0618 // Type or member is obsolete
SetDefault(OsuSetting.AutomaticallyDownloadMissingBeatmaps, false);
SetDefault(OsuSetting.SavePassword, true).ValueChanged += enabled => SetDefault(OsuSetting.SavePassword, true).ValueChanged += enabled =>
{ {
@ -244,12 +238,6 @@ namespace osu.Game.Configuration
// migrations can be added here using a condition like: // migrations can be added here using a condition like:
// if (combined < 20220103) { performMigration() } // if (combined < 20220103) { performMigration() }
if (combined < 20230918)
{
#pragma warning disable CS0618 // Type or member is obsolete
SetValue(OsuSetting.AutomaticallyDownloadMissingBeatmaps, Get<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating)); // can be removed 20240618
#pragma warning restore CS0618 // Type or member is obsolete
}
} }
public override TrackedSettings CreateTrackedSettings() public override TrackedSettings CreateTrackedSettings()
@ -424,9 +412,6 @@ namespace osu.Game.Configuration
EditorAutoSeekOnPlacement, EditorAutoSeekOnPlacement,
DiscordRichPresence, DiscordRichPresence,
[Obsolete($"Use {nameof(AutomaticallyDownloadMissingBeatmaps)} instead.")] // can be removed 20240318
AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent, ShowOnlineExplicitContent,
LastProcessedMetadataId, LastProcessedMetadataId,
SafeAreaConsiderations, SafeAreaConsiderations,

View File

@ -64,6 +64,7 @@ namespace osu.Game.Graphics.Containers
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new HoverClickSounds(),
new GridContainer new GridContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -92,7 +93,6 @@ namespace osu.Game.Graphics.Containers
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
}, },
new HoverClickSounds()
}; };
} }

View File

@ -8,16 +8,24 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.Cursor namespace osu.Game.Graphics.Cursor
{ {
[Cached(typeof(OsuContextMenuContainer))]
public partial class OsuContextMenuContainer : ContextMenuContainer public partial class OsuContextMenuContainer : ContextMenuContainer
{ {
[Cached] [Cached]
private OsuContextMenuSamples samples = new OsuContextMenuSamples(); private OsuContextMenuSamples samples = new OsuContextMenuSamples();
private OsuContextMenu menu = null!;
public OsuContextMenuContainer() public OsuContextMenuContainer()
{ {
AddInternal(samples); AddInternal(samples);
} }
protected override Menu CreateMenu() => new OsuContextMenu(true); protected override Menu CreateMenu() => menu = new OsuContextMenu(true);
public void CloseMenu()
{
menu.Close();
}
} }
} }

View File

@ -10,7 +10,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Containers;
namespace osu.Game.Graphics.Cursor namespace osu.Game.Graphics.Cursor
{ {
@ -27,15 +27,20 @@ namespace osu.Game.Graphics.Cursor
public partial class OsuTooltip : Tooltip public partial class OsuTooltip : Tooltip
{ {
private const float max_width = 500;
private readonly Box background; private readonly Box background;
private readonly OsuSpriteText text; private readonly TextFlowContainer text;
private bool instantMovement = true; private bool instantMovement = true;
public override void SetContent(LocalisableString contentString) private LocalisableString lastContent;
{
if (contentString == text.Text) return;
text.Text = contentString; public override void SetContent(LocalisableString content)
{
if (content.Equals(lastContent))
return;
text.Text = content;
if (IsPresent) if (IsPresent)
{ {
@ -44,6 +49,8 @@ namespace osu.Game.Graphics.Cursor
} }
else else
AutoSizeDuration = 0; AutoSizeDuration = 0;
lastContent = content;
} }
public OsuTooltip() public OsuTooltip()
@ -65,10 +72,14 @@ namespace osu.Game.Graphics.Cursor
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0.9f, Alpha = 0.9f,
}, },
text = new OsuSpriteText text = new TextFlowContainer(f =>
{ {
Padding = new MarginPadding(5), f.Font = OsuFont.GetFont(weight: FontWeight.Regular);
Font = OsuFont.GetFont(weight: FontWeight.Regular) })
{
Margin = new MarginPadding(5),
AutoSizeAxes = Axes.Both,
MaximumSize = new Vector2(max_width, float.PositiveInfinity),
} }
}; };
} }

View File

@ -24,6 +24,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString MentionUser => new TranslatableString(getKey(@"mention_user"), @"Mention"); public static LocalisableString MentionUser => new TranslatableString(getKey(@"mention_user"), @"Mention");
private static string getKey(string key) => $"{prefix}:{key}"; /// <summary>
/// "press enter to chat..."
/// </summary>
public static LocalisableString InGameInputPlaceholder => new TranslatableString(getKey(@"in_game_input_placeholder"), @"press enter to chat...");
private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -19,6 +19,16 @@ namespace osu.Game.Localisation.HUD
/// </summary> /// </summary>
public static LocalisableString ShowGraphDescription => new TranslatableString(getKey(@"show_graph_description"), "Whether a graph displaying difficulty throughout the beatmap should be shown"); public static LocalisableString ShowGraphDescription => new TranslatableString(getKey(@"show_graph_description"), "Whether a graph displaying difficulty throughout the beatmap should be shown");
/// <summary>
/// "Show time"
/// </summary>
public static LocalisableString ShowTime => new TranslatableString(getKey(@"show_time"), "Show time");
/// <summary>
/// "Whether the passed and remaining time should be shown"
/// </summary>
public static LocalisableString ShowTimeDescription => new TranslatableString(getKey(@"show_time_description"), "Whether the passed and remaining time should be shown");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -272,6 +272,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("groups")] [JsonProperty("groups")]
public APIUserGroup[] Groups; public APIUserGroup[] Groups;
[JsonProperty("daily_challenge_user_stats")]
public APIUserDailyChallengeStatistics DailyChallengeStatistics = new APIUserDailyChallengeStatistics();
public override string ToString() => Username; public override string ToString() => Username;
/// <summary> /// <summary>

View File

@ -0,0 +1,41 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIUserDailyChallengeStatistics
{
[JsonProperty("user_id")]
public int UserID;
[JsonProperty("daily_streak_best")]
public int DailyStreakBest;
[JsonProperty("daily_streak_current")]
public int DailyStreakCurrent;
[JsonProperty("weekly_streak_best")]
public int WeeklyStreakBest;
[JsonProperty("weekly_streak_current")]
public int WeeklyStreakCurrent;
[JsonProperty("top_10p_placements")]
public int Top10PercentPlacements;
[JsonProperty("top_50p_placements")]
public int Top50PercentPlacements;
[JsonProperty("playcount")]
public int PlayCount;
[JsonProperty("last_update")]
public DateTimeOffset? LastUpdate;
[JsonProperty("last_weekly_streak")]
public DateTimeOffset? LastWeeklyStreak;
}
}

View File

@ -128,6 +128,9 @@ namespace osu.Game.Online.Chat
public partial class ChatTextBox : HistoryTextBox public partial class ChatTextBox : HistoryTextBox
{ {
public Action Focus;
public Action FocusLost;
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
{ {
// Chat text boxes are generally used in places where they retain focus, but shouldn't block interaction with other // Chat text boxes are generally used in places where they retain focus, but shouldn't block interaction with other
@ -153,13 +156,17 @@ namespace osu.Game.Online.Chat
BackgroundFocused = new Color4(10, 10, 10, 255); BackgroundFocused = new Color4(10, 10, 10, 255);
} }
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
Focus?.Invoke();
}
protected override void OnFocusLost(FocusLostEvent e) protected override void OnFocusLost(FocusLostEvent e)
{ {
base.OnFocusLost(e); base.OnFocusLost(e);
FocusLost?.Invoke(); FocusLost?.Invoke();
} }
public Action FocusLost;
} }
public partial class StandAloneDrawableChannel : DrawableChannel public partial class StandAloneDrawableChannel : DrawableChannel

View File

@ -782,7 +782,21 @@ namespace osu.Game.Online.Multiplayer
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new AggregateException($"Item: {JsonConvert.SerializeObject(createPlaylistItem(item))}\n\nRoom:{JsonConvert.SerializeObject(APIRoom)}", ex); // Temporary code to attempt to figure out long-term failing tests.
bool success = true;
int indexOf = -1234;
try
{
indexOf = Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID));
Room.Playlist[indexOf] = item;
}
catch
{
success = false;
}
throw new AggregateException($"Index: {indexOf} Length: {Room.Playlist.Count} Retry success: {success} Item: {JsonConvert.SerializeObject(createPlaylistItem(item))}\n\nRoom:{JsonConvert.SerializeObject(APIRoom)}", ex);
} }
ItemChanged?.Invoke(item); ItemChanged?.Invoke(item);

View File

@ -642,10 +642,10 @@ namespace osu.Game
Live<BeatmapSetInfo> databasedSet = null; Live<BeatmapSetInfo> databasedSet = null;
if (beatmap.OnlineID > 0) if (beatmap.OnlineID > 0)
databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID); databasedSet = BeatmapManager.QueryBeatmapSet(s => s.OnlineID == beatmap.OnlineID && !s.DeletePending);
if (beatmap is BeatmapSetInfo localBeatmap) if (beatmap is BeatmapSetInfo localBeatmap)
databasedSet ??= BeatmapManager.QueryBeatmapSet(s => s.Hash == localBeatmap.Hash); databasedSet ??= BeatmapManager.QueryBeatmapSet(s => s.Hash == localBeatmap.Hash && !s.DeletePending);
if (databasedSet == null) if (databasedSet == null)
{ {

View File

@ -18,6 +18,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osuTK; using osuTK;
@ -104,6 +105,8 @@ namespace osu.Game.Overlays.Chat
} }
} }
private bool isMention;
/// <summary> /// <summary>
/// The colour used to paint the author's username. /// The colour used to paint the author's username.
/// </summary> /// </summary>
@ -255,12 +258,21 @@ namespace osu.Game.Overlays.Chat
private void styleMessageContent(SpriteText text) private void styleMessageContent(SpriteText text)
{ {
text.Shadow = false; text.Shadow = false;
text.Font = text.Font.With(size: FontSize, italics: Message.IsAction); text.Font = text.Font.With(size: FontSize, italics: Message.IsAction, weight: isMention ? FontWeight.SemiBold : FontWeight.Medium);
bool messageHasColour = Message.IsAction && !string.IsNullOrEmpty(message.Sender.Colour); Color4 messageColour = colourProvider?.Content1 ?? Colour4.White;
text.Colour = messageHasColour ? Color4Extensions.FromHex(message.Sender.Colour) : colourProvider?.Content1 ?? Colour4.White;
if (isMention)
messageColour = colourProvider?.Highlight1 ?? Color4.Orange;
else if (Message.IsAction && !string.IsNullOrEmpty(message.Sender.Colour))
messageColour = Color4Extensions.FromHex(message.Sender.Colour);
text.Colour = messageColour;
} }
[Resolved]
private IAPIProvider api { get; set; } = null!;
private void updateMessageContent() private void updateMessageContent()
{ {
this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint); this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint);
@ -280,6 +292,8 @@ namespace osu.Game.Overlays.Chat
// remove non-existent channels from the link list // remove non-existent channels from the link list
message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument.ToString()) != true); message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument.ToString()) != true);
isMention = MessageNotifier.CheckContainsUsername(message.DisplayContent, api.LocalUser.Value.Username);
drawableContentFlow.Clear(); drawableContentFlow.Clear();
drawableContentFlow.AddLinks(message.DisplayContent, message.Links); drawableContentFlow.AddLinks(message.DisplayContent, message.Links);
} }

View File

@ -9,12 +9,14 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Localisation; using osu.Game.Localisation;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using static osu.Game.Overlays.Mods.ModCustomisationPanel;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
@ -29,11 +31,13 @@ namespace osu.Game.Overlays.Mods
protected override IEnumerable<Drawable> EffectTargets => new[] { background }; protected override IEnumerable<Drawable> EffectTargets => new[] { background };
public readonly BindableBool Expanded = new BindableBool(); public readonly Bindable<ModCustomisationPanelState> ExpandedState = new Bindable<ModCustomisationPanelState>(ModCustomisationPanelState.Collapsed);
public ModCustomisationHeader() private readonly ModCustomisationPanel panel;
public ModCustomisationHeader(ModCustomisationPanel panel)
{ {
Action = Expanded.Toggle; this.panel = panel;
Enabled.Value = false; Enabled.Value = false;
} }
@ -54,6 +58,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.White.Opacity(0.4f), Colour = Color4.White.Opacity(0.4f),
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
Alpha = 0,
}, },
new OsuSpriteText new OsuSpriteText
{ {
@ -101,10 +106,48 @@ namespace osu.Game.Overlays.Mods
} }
}, true); }, true);
Expanded.BindValueChanged(v => ExpandedState.BindValueChanged(v =>
{ {
icon.ScaleTo(v.NewValue ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint); icon.ScaleTo(v.NewValue > ModCustomisationPanelState.Collapsed ? new Vector2(1, -1) : Vector2.One, 300, Easing.OutQuint);
}, true); }, true);
} }
protected override bool OnClick(ClickEvent e)
{
if (Enabled.Value)
{
ExpandedState.Value = ExpandedState.Value switch
{
ModCustomisationPanelState.Collapsed => ModCustomisationPanelState.Expanded,
_ => ModCustomisationPanelState.Collapsed
};
}
return base.OnClick(e);
}
private bool touchedThisFrame;
protected override bool OnTouchDown(TouchDownEvent e)
{
if (Enabled.Value)
{
touchedThisFrame = true;
Schedule(() => touchedThisFrame = false);
}
return base.OnTouchDown(e);
}
protected override bool OnHover(HoverEvent e)
{
if (Enabled.Value)
{
if (!touchedThisFrame && panel.ExpandedState.Value == ModCustomisationPanelState.Collapsed)
panel.ExpandedState.Value = ModCustomisationPanelState.ExpandedByHover;
}
return base.OnHover(e);
}
} }
} }

View File

@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Mods
public readonly BindableBool Enabled = new BindableBool(); public readonly BindableBool Enabled = new BindableBool();
public readonly BindableBool Expanded = new BindableBool(); public readonly Bindable<ModCustomisationPanelState> ExpandedState = new Bindable<ModCustomisationPanelState>();
public Bindable<IReadOnlyList<Mod>> SelectedMods { get; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>()); public Bindable<IReadOnlyList<Mod>> SelectedMods { get; } = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
@ -46,9 +46,9 @@ namespace osu.Game.Overlays.Mods
// Handle{Non}PositionalInput controls whether the panel should act as a blocking layer on the screen. only block when the panel is expanded. // Handle{Non}PositionalInput controls whether the panel should act as a blocking layer on the screen. only block when the panel is expanded.
// These properties are used because they correctly handle blocking/unblocking hover when mouse is pointing at a drawable outside // These properties are used because they correctly handle blocking/unblocking hover when mouse is pointing at a drawable outside
// (returning Expanded.Value to OnHover or overriding Block{Non}PositionalInput doesn't work). // (handling OnHover or overriding Block{Non}PositionalInput doesn't work).
public override bool HandlePositionalInput => Expanded.Value; public override bool HandlePositionalInput => ExpandedState.Value != ModCustomisationPanelState.Collapsed;
public override bool HandleNonPositionalInput => Expanded.Value; public override bool HandleNonPositionalInput => ExpandedState.Value != ModCustomisationPanelState.Collapsed;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -57,15 +57,15 @@ namespace osu.Game.Overlays.Mods
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new ModCustomisationHeader new ModCustomisationHeader(this)
{ {
Depth = float.MinValue, Depth = float.MinValue,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = header_height, Height = header_height,
Enabled = { BindTarget = Enabled }, Enabled = { BindTarget = Enabled },
Expanded = { BindTarget = Expanded }, ExpandedState = { BindTarget = ExpandedState },
}, },
content = new FocusGrabbingContainer content = new FocusGrabbingContainer(this)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
BorderColour = colourProvider.Dark3, BorderColour = colourProvider.Dark3,
@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Mods
Roundness = 5f, Roundness = 5f,
Colour = Color4.Black.Opacity(0.25f), Colour = Color4.Black.Opacity(0.25f),
}, },
Expanded = { BindTarget = Expanded }, ExpandedState = { BindTarget = ExpandedState },
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Box
@ -122,7 +122,7 @@ namespace osu.Game.Overlays.Mods
this.FadeColour(OsuColour.Gray(e.NewValue ? 1f : 0.6f), 300, Easing.OutQuint); this.FadeColour(OsuColour.Gray(e.NewValue ? 1f : 0.6f), 300, Easing.OutQuint);
}, true); }, true);
Expanded.BindValueChanged(_ => updateDisplay(), true); ExpandedState.BindValueChanged(_ => updateDisplay(), true);
SelectedMods.BindValueChanged(_ => updateMods(), true); SelectedMods.BindValueChanged(_ => updateMods(), true);
FinishTransforms(true); FinishTransforms(true);
@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Mods
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
Expanded.Value = false; ExpandedState.Value = ModCustomisationPanelState.Collapsed;
return base.OnClick(e); return base.OnClick(e);
} }
@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Mods
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.Back: case GlobalAction.Back:
Expanded.Value = false; ExpandedState.Value = ModCustomisationPanelState.Collapsed;
return true; return true;
} }
@ -162,7 +162,7 @@ namespace osu.Game.Overlays.Mods
{ {
content.ClearTransforms(); content.ClearTransforms();
if (Expanded.Value) if (ExpandedState.Value != ModCustomisationPanelState.Collapsed)
{ {
content.AutoSizeDuration = 400; content.AutoSizeDuration = 400;
content.AutoSizeEasing = Easing.OutQuint; content.AutoSizeEasing = Easing.OutQuint;
@ -179,7 +179,7 @@ namespace osu.Game.Overlays.Mods
private void updateMods() private void updateMods()
{ {
Expanded.Value = false; ExpandedState.Value = ModCustomisationPanelState.Collapsed;
sectionsFlow.Clear(); sectionsFlow.Clear();
// Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels). // Importantly, the selected mods bindable is already ordered by the mod select overlay (following the order of mod columns and panels).
@ -202,10 +202,35 @@ namespace osu.Game.Overlays.Mods
private partial class FocusGrabbingContainer : InputBlockingContainer private partial class FocusGrabbingContainer : InputBlockingContainer
{ {
public IBindable<bool> Expanded { get; } = new BindableBool(); public readonly Bindable<ModCustomisationPanelState> ExpandedState = new Bindable<ModCustomisationPanelState>();
public override bool RequestsFocus => Expanded.Value; public override bool RequestsFocus => panel.ExpandedState.Value != ModCustomisationPanelState.Collapsed;
public override bool AcceptsFocus => Expanded.Value; public override bool AcceptsFocus => panel.ExpandedState.Value != ModCustomisationPanelState.Collapsed;
private readonly ModCustomisationPanel panel;
public FocusGrabbingContainer(ModCustomisationPanel panel)
{
this.panel = panel;
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (ExpandedState.Value is ModCustomisationPanelState.ExpandedByHover
&& !ReceivePositionalInputAt(e.ScreenSpaceMousePosition))
{
ExpandedState.Value = ModCustomisationPanelState.Collapsed;
}
base.OnHoverLost(e);
}
}
public enum ModCustomisationPanelState
{
Collapsed = 0,
ExpandedByHover = 1,
Expanded = 2,
} }
} }
} }

View File

@ -138,6 +138,7 @@ namespace osu.Game.Overlays.Mods
}, },
new GridContainer new GridContainer
{ {
Padding = new MarginPadding { Top = 1, Bottom = 3 },
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RowDimensions = new[] RowDimensions = new[]
{ {

View File

@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Mods
ActiveMods.Value = ComputeActiveMods(); ActiveMods.Value = ComputeActiveMods();
}, true); }, true);
customisationPanel.Expanded.BindValueChanged(_ => updateCustomisationVisualState(), true); customisationPanel.ExpandedState.BindValueChanged(_ => updateCustomisationVisualState(), true);
SearchTextBox.Current.BindValueChanged(query => SearchTextBox.Current.BindValueChanged(query =>
{ {
@ -368,18 +368,18 @@ namespace osu.Game.Overlays.Mods
customisationPanel.Enabled.Value = true; customisationPanel.Enabled.Value = true;
if (anyModPendingConfiguration) if (anyModPendingConfiguration)
customisationPanel.Expanded.Value = true; customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded;
} }
else else
{ {
customisationPanel.Expanded.Value = false; customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Collapsed;
customisationPanel.Enabled.Value = false; customisationPanel.Enabled.Value = false;
} }
} }
private void updateCustomisationVisualState() private void updateCustomisationVisualState()
{ {
if (customisationPanel.Expanded.Value) if (customisationPanel.ExpandedState.Value != ModCustomisationPanel.ModCustomisationPanelState.Collapsed)
{ {
columnScroll.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); columnScroll.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
SearchTextBox.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint); SearchTextBox.FadeColour(OsuColour.Gray(0.5f), 400, Easing.OutQuint);
@ -544,7 +544,7 @@ namespace osu.Game.Overlays.Mods
nonFilteredColumnCount += 1; nonFilteredColumnCount += 1;
} }
customisationPanel.Expanded.Value = false; customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Collapsed;
} }
#endregion #endregion
@ -571,7 +571,7 @@ namespace osu.Game.Overlays.Mods
// wherein activating the binding will both change the contents of the search text box and deselect all mods. // wherein activating the binding will both change the contents of the search text box and deselect all mods.
case GlobalAction.DeselectAllMods: case GlobalAction.DeselectAllMods:
{ {
if (!SearchTextBox.HasFocus && !customisationPanel.Expanded.Value) if (!SearchTextBox.HasFocus && customisationPanel.ExpandedState.Value == ModCustomisationPanel.ModCustomisationPanelState.Collapsed)
{ {
DisplayedFooterContent?.DeselectAllModsButton?.TriggerClick(); DisplayedFooterContent?.DeselectAllModsButton?.TriggerClick();
return true; return true;
@ -637,7 +637,7 @@ namespace osu.Game.Overlays.Mods
if (e.Repeat || e.Key != Key.Tab) if (e.Repeat || e.Key != Key.Tab)
return false; return false;
if (customisationPanel.Expanded.Value) if (customisationPanel.ExpandedState.Value != ModCustomisationPanel.ModCustomisationPanelState.Collapsed)
return true; return true;
// TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`) // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`)
@ -668,6 +668,8 @@ namespace osu.Game.Overlays.Mods
[Cached] [Cached]
internal partial class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer> internal partial class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
{ {
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public ColumnScrollContainer() public ColumnScrollContainer()
: base(Direction.Horizontal) : base(Direction.Horizontal)
{ {

View File

@ -0,0 +1,121 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class DailyChallengeStatsDisplay : CompositeDrawable, IHasCustomTooltip<DailyChallengeTooltipData>
{
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public DailyChallengeTooltipData? TooltipContent { get; private set; }
private OsuSpriteText dailyPlayCount = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
CornerRadius = 5;
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(5f),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
AutoSizeAxes = Axes.Both,
// can't use this because osu-web does weird stuff with \\n.
// Text = UsersStrings.ShowDailyChallengeTitle.,
Text = "Daily\nChallenge",
Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f },
},
new Container
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
CornerRadius = 5f,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
dailyPlayCount = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
UseFullGlyphHeight = false,
Colour = colourProvider.Content2,
Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f },
},
}
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
User.BindValueChanged(_ => updateDisplay(), true);
}
private void updateDisplay()
{
if (User.Value == null || User.Value.Ruleset.OnlineID != 0)
{
Hide();
return;
}
APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics;
dailyPlayCount.Text = UsersStrings.ShowDailyChallengeUnitDay(stats.PlayCount.ToLocalisableString("N0"));
dailyPlayCount.Colour = colours.ForRankingTier(tierForPlayCount(stats.PlayCount));
TooltipContent = new DailyChallengeTooltipData(colourProvider, stats);
Show();
static RankingTier tierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily(playCount / 3);
}
public ITooltip<DailyChallengeTooltipData> GetCustomTooltip() => new DailyChallengeStatsTooltip();
}
}

View File

@ -0,0 +1,241 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osuTK;
using Box = osu.Framework.Graphics.Shapes.Box;
using Color4 = osuTK.Graphics.Color4;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class DailyChallengeStatsTooltip : VisibilityContainer, ITooltip<DailyChallengeTooltipData>
{
private StreakPiece currentDaily = null!;
private StreakPiece currentWeekly = null!;
private StatisticsPiece bestDaily = null!;
private StatisticsPiece bestWeekly = null!;
private StatisticsPiece topTen = null!;
private StatisticsPiece topFifty = null!;
private Box topBackground = null!;
private Box background = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
CornerRadius = 20f;
Masking = true;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.25f),
Radius = 30f,
};
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
topBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding(15f),
Spacing = new Vector2(30f),
Children = new[]
{
currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent),
currentWeekly = new StreakPiece(UsersStrings.ShowDailyChallengeWeeklyStreakCurrent),
}
},
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(15f),
Spacing = new Vector2(10f),
Children = new[]
{
bestDaily = new StatisticsPiece(UsersStrings.ShowDailyChallengeDailyStreakBest),
bestWeekly = new StatisticsPiece(UsersStrings.ShowDailyChallengeWeeklyStreakBest),
topTen = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop10pPlacements),
topFifty = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop50pPlacements),
}
},
}
}
};
}
public void SetContent(DailyChallengeTooltipData content)
{
var statistics = content.Statistics;
var colourProvider = content.ColourProvider;
background.Colour = colourProvider.Background4;
topBackground.Colour = colourProvider.Background5;
currentDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0"));
currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent));
currentWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0"));
currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent));
bestDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0"));
bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakBest));
bestWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0"));
bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest));
topTen.Value = statistics.Top10PercentPlacements.ToLocalisableString(@"N0");
topTen.ValueColour = colourProvider.Content2;
topFifty.Value = statistics.Top50PercentPlacements.ToLocalisableString(@"N0");
topFifty.ValueColour = colourProvider.Content2;
}
// reference: https://github.com/ppy/osu-web/blob/8206e0e91eeea80ccf92f0586561346dd40e085e/resources/js/profile-page/daily-challenge.tsx#L13-L43
public static RankingTier TierForDaily(int daily)
{
if (daily > 360)
return RankingTier.Lustrous;
if (daily > 240)
return RankingTier.Radiant;
if (daily > 120)
return RankingTier.Rhodium;
if (daily > 60)
return RankingTier.Platinum;
if (daily > 30)
return RankingTier.Gold;
if (daily > 10)
return RankingTier.Silver;
if (daily > 5)
return RankingTier.Bronze;
return RankingTier.Iron;
}
public static RankingTier TierForWeekly(int weekly) => TierForDaily((weekly - 1) * 7);
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
public void Move(Vector2 pos) => Position = pos;
private partial class StreakPiece : FillFlowContainer
{
private readonly OsuSpriteText valueText;
public LocalisableString Value
{
set => valueText.Text = value;
}
public ColourInfo ValueColour
{
set => valueText.Colour = value;
}
public StreakPiece(LocalisableString title)
{
AutoSizeAxes = Axes.Both;
Direction = FillDirection.Vertical;
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12),
Text = title,
},
valueText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 40, weight: FontWeight.Light),
}
};
}
}
private partial class StatisticsPiece : CompositeDrawable
{
private readonly OsuSpriteText valueText;
public LocalisableString Value
{
set => valueText.Text = value;
}
public ColourInfo ValueColour
{
set => valueText.Colour = value;
}
public StatisticsPiece(LocalisableString title)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12),
Text = title,
},
valueText = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.GetFont(size: 12),
}
};
}
}
}
public record DailyChallengeTooltipData(OverlayColourProvider ColourProvider, APIUserDailyChallengeStatistics Statistics);
}

View File

@ -44,22 +44,41 @@ namespace osu.Game.Overlays.Profile.Header.Components
Spacing = new Vector2(0, 15), Spacing = new Vector2(0, 15),
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new GridContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal, ColumnDimensions = new[]
Spacing = new Vector2(20),
Children = new Drawable[]
{ {
detailGlobalRank = new ProfileValueDisplay(true) new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new[]
{ {
Title = UsersStrings.ShowRankGlobalSimple, detailGlobalRank = new ProfileValueDisplay(true)
}, {
detailCountryRank = new ProfileValueDisplay(true) Title = UsersStrings.ShowRankGlobalSimple,
{ },
Title = UsersStrings.ShowRankCountrySimple, Empty(),
}, detailCountryRank = new ProfileValueDisplay(true)
{
Title = UsersStrings.ShowRankCountrySimple,
},
new DailyChallengeStatsDisplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
User = { BindTarget = User },
}
}
} }
}, },
new Container new Container

View File

@ -2,7 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
@ -10,6 +14,8 @@ namespace osu.Game.Overlays.Settings
public partial class SettingsEnumDropdown<T> : SettingsDropdown<T> public partial class SettingsEnumDropdown<T> : SettingsDropdown<T>
where T : struct, Enum where T : struct, Enum
{ {
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.GetLocalisableDescription()));
protected override OsuDropdown<T> CreateDropdown() => new DropdownControl(); protected override OsuDropdown<T> CreateDropdown() => new DropdownControl();
protected new partial class DropdownControl : OsuEnumDropdown<T> protected new partial class DropdownControl : OsuEnumDropdown<T>

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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osuTK.Graphics;
namespace osu.Game.Overlays.Volume
{
public partial class MasterVolumeMeter : VolumeMeter
{
private MuteButton muteButton = null!;
public Bindable<bool> IsMuted { get; } = new Bindable<bool>();
private readonly BindableDouble muteAdjustment = new BindableDouble();
[Resolved]
private VolumeOverlay volumeOverlay { get; set; } = null!;
public MasterVolumeMeter(string name, float circleSize, Color4 meterColour)
: base(name, circleSize, meterColour)
{
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
IsMuted.BindValueChanged(muted =>
{
if (muted.NewValue)
audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment);
else
audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment);
});
Add(muteButton = new MuteButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
X = CircleSize / 2,
Y = CircleSize * 0.23f,
Current = { BindTarget = IsMuted }
});
muteButton.Current.ValueChanged += _ => volumeOverlay.Show();
}
public void ToggleMute() => muteButton.Current.Value = !muteButton.Current.Value;
}
}

View File

@ -7,13 +7,13 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Volume namespace osu.Game.Overlays.Volume
{ {
@ -33,18 +33,18 @@ namespace osu.Game.Overlays.Volume
} }
} }
private Color4 hoveredColour, unhoveredColour; private ColourInfo hoveredBorderColour;
private ColourInfo unhoveredBorderColour;
private const float width = 100; private CompositeDrawable border = null!;
public const float HEIGHT = 35;
public MuteButton() public MuteButton()
{ {
Content.BorderThickness = 3; const float width = 30;
Content.CornerRadius = HEIGHT / 2; const float height = 30;
Content.CornerExponent = 2;
Size = new Vector2(width, HEIGHT); Size = new Vector2(width, height);
Content.CornerRadius = height / 2;
Content.CornerExponent = 2;
Action = () => Current.Value = !Current.Value; Action = () => Current.Value = !Current.Value;
} }
@ -52,10 +52,9 @@ namespace osu.Game.Overlays.Volume
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
hoveredColour = colours.YellowDark;
Content.BorderColour = unhoveredColour = colours.Gray1;
BackgroundColour = colours.Gray1; BackgroundColour = colours.Gray1;
hoveredBorderColour = colours.PinkLight;
unhoveredBorderColour = colours.Gray1;
SpriteIcon icon; SpriteIcon icon;
@ -65,26 +64,39 @@ namespace osu.Game.Overlays.Volume
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
},
border = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 3,
BorderColour = unhoveredBorderColour,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
} }
}); });
Current.BindValueChanged(muted => Current.BindValueChanged(muted =>
{ {
icon.Icon = muted.NewValue ? FontAwesome.Solid.VolumeMute : FontAwesome.Solid.VolumeUp; icon.Icon = muted.NewValue ? FontAwesome.Solid.VolumeMute : FontAwesome.Solid.VolumeUp;
icon.Size = new Vector2(muted.NewValue ? 18 : 20); icon.Size = new Vector2(muted.NewValue ? 12 : 16);
icon.Margin = new MarginPadding { Right = muted.NewValue ? 2 : 0 }; icon.Margin = new MarginPadding { Right = muted.NewValue ? 2 : 0 };
}, true); }, true);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
Content.TransformTo<Container<Drawable>, ColourInfo>("BorderColour", hoveredColour, 500, Easing.OutQuint); border.TransformTo(nameof(BorderColour), hoveredBorderColour, 500, Easing.OutQuint);
return false; return false;
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
Content.TransformTo<Container<Drawable>, ColourInfo>("BorderColour", unhoveredColour, 500, Easing.OutQuint); border.TransformTo(nameof(BorderColour), unhoveredBorderColour, 500, Easing.OutQuint);
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -35,8 +35,12 @@ namespace osu.Game.Overlays.Volume
private CircularProgress volumeCircle; private CircularProgress volumeCircle;
private CircularProgress volumeCircleGlow; private CircularProgress volumeCircleGlow;
protected static readonly Vector2 LABEL_SIZE = new Vector2(120, 20);
public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1, Precision = 0.01 }; public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1, Precision = 0.01 };
private readonly float circleSize;
protected readonly float CircleSize;
private readonly Color4 meterColour; private readonly Color4 meterColour;
private readonly string name; private readonly string name;
@ -73,7 +77,7 @@ namespace osu.Game.Overlays.Volume
public VolumeMeter(string name, float circleSize, Color4 meterColour) public VolumeMeter(string name, float circleSize, Color4 meterColour)
{ {
this.circleSize = circleSize; CircleSize = circleSize;
this.meterColour = meterColour; this.meterColour = meterColour;
this.name = name; this.name = name;
@ -101,7 +105,7 @@ namespace osu.Game.Overlays.Volume
{ {
new Container new Container
{ {
Size = new Vector2(circleSize), Size = new Vector2(CircleSize),
Children = new Drawable[] Children = new Drawable[]
{ {
new BufferedContainer new BufferedContainer
@ -199,7 +203,7 @@ namespace osu.Game.Overlays.Volume
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 0.16f * circleSize) Font = OsuFont.Numeric.With(size: 0.16f * CircleSize)
}).WithEffect(new GlowEffect }).WithEffect(new GlowEffect
{ {
Colour = Color4.Transparent, Colour = Color4.Transparent,
@ -209,10 +213,10 @@ namespace osu.Game.Overlays.Volume
}, },
new Container new Container
{ {
Size = new Vector2(120, 20), Size = LABEL_SIZE,
CornerRadius = 10, CornerRadius = 10,
Masking = true, Masking = true,
Margin = new MarginPadding { Left = circleSize + 10 }, Margin = new MarginPadding { Left = CircleSize + 10 },
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Children = new Drawable[] Children = new Drawable[]

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -20,21 +21,19 @@ using osuTK.Graphics;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
[Cached]
public partial class VolumeOverlay : VisibilityContainer public partial class VolumeOverlay : VisibilityContainer
{ {
public Bindable<bool> IsMuted { get; } = new Bindable<bool>();
private const float offset = 10; private const float offset = 10;
private VolumeMeter volumeMeterMaster = null!; private VolumeMeter volumeMeterMaster = null!;
private VolumeMeter volumeMeterEffect = null!; private VolumeMeter volumeMeterEffect = null!;
private VolumeMeter volumeMeterMusic = null!; private VolumeMeter volumeMeterMusic = null!;
private MuteButton muteButton = null!;
private SelectionCycleFillFlowContainer<VolumeMeter> volumeMeters = null!; private SelectionCycleFillFlowContainer<VolumeMeter> volumeMeters = null!;
private readonly BindableDouble muteAdjustment = new BindableDouble();
public Bindable<bool> IsMuted { get; } = new Bindable<bool>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours) private void load(AudioManager audio, OsuColour colours)
{ {
@ -49,14 +48,7 @@ namespace osu.Game.Overlays
Width = 300, Width = 300,
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.75f), Color4.Black.Opacity(0)) Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.75f), Color4.Black.Opacity(0))
}, },
muteButton = new MuteButton new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding(10),
Current = { BindTarget = IsMuted }
},
volumeMeters = new SelectionCycleFillFlowContainer<VolumeMeter>
{ {
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -64,26 +56,29 @@ namespace osu.Game.Overlays
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Spacing = new Vector2(0, offset), Spacing = new Vector2(0, offset),
Margin = new MarginPadding { Left = offset }, Margin = new MarginPadding { Left = offset },
Children = new[] Children = new Drawable[]
{ {
volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker), volumeMeters = new SelectionCycleFillFlowContainer<VolumeMeter>
volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker), {
volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker), Direction = FillDirection.Vertical,
} AutoSizeAxes = Axes.Both,
} Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Spacing = new Vector2(0, offset),
Children = new[]
{
volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker),
volumeMeterMaster = new MasterVolumeMeter("MASTER", 150, colours.PinkDarker) { IsMuted = { BindTarget = IsMuted }, },
volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker),
}
},
},
},
}); });
volumeMeterMaster.Bindable.BindTo(audio.Volume); volumeMeterMaster.Bindable.BindTo(audio.Volume);
volumeMeterEffect.Bindable.BindTo(audio.VolumeSample); volumeMeterEffect.Bindable.BindTo(audio.VolumeSample);
volumeMeterMusic.Bindable.BindTo(audio.VolumeTrack); volumeMeterMusic.Bindable.BindTo(audio.VolumeTrack);
IsMuted.BindValueChanged(muted =>
{
if (muted.NewValue)
audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment);
else
audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment);
});
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -92,8 +87,6 @@ namespace osu.Game.Overlays
foreach (var volumeMeter in volumeMeters) foreach (var volumeMeter in volumeMeters)
volumeMeter.Bindable.ValueChanged += _ => Show(); volumeMeter.Bindable.ValueChanged += _ => Show();
muteButton.Current.ValueChanged += _ => Show();
} }
public bool Adjust(GlobalAction action, float amount = 1, bool isPrecise = false) public bool Adjust(GlobalAction action, float amount = 1, bool isPrecise = false)
@ -130,7 +123,7 @@ namespace osu.Game.Overlays
case GlobalAction.ToggleMute: case GlobalAction.ToggleMute:
Show(); Show();
muteButton.Current.Value = !muteButton.Current.Value; volumeMeters.OfType<MasterVolumeMeter>().First().ToggleMute();
return true; return true;
} }

View File

@ -209,9 +209,7 @@ namespace osu.Game.Rulesets.Edit
case MouseButtonEvent mouse: case MouseButtonEvent mouse:
// placement blueprints should generally block mouse from reaching underlying components (ie. performing clicks on interface buttons). // placement blueprints should generally block mouse from reaching underlying components (ie. performing clicks on interface buttons).
// for now, the one exception we want to allow is when using a non-main mouse button when shift is pressed, which is used to trigger object deletion return mouse.Button == MouseButton.Left || PlacementActive == PlacementState.Active;
// while in placement mode.
return mouse.Button == MouseButton.Left || !mouse.ShiftPressed;
default: default:
return false; return false;

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
protected readonly double Offset; protected readonly double Offset;
/// <summary> /// <summary>
/// The beatmap version. /// The .osu format (beatmap) version.
/// </summary> /// </summary>
protected readonly int FormatVersion; protected readonly int FormatVersion;
@ -48,7 +48,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
{ {
string[] split = text.Split(','); string[] split = text.Split(',');
Vector2 pos = new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE)); Vector2 pos =
FormatVersion >= LegacyBeatmapEncoder.FIRST_LAZER_VERSION
? new Vector2(Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE))
: new Vector2((int)Parsing.ParseFloat(split[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE));
double startTime = Parsing.ParseDouble(split[2]) + Offset; double startTime = Parsing.ParseDouble(split[2]) + Offset;

View File

@ -17,13 +17,12 @@ namespace osu.Game.Screens
private const float x_movement_amount = 50; private const float x_movement_amount = 50;
private readonly bool animateOnEnter;
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
protected BackgroundScreen(bool animateOnEnter = true) public bool AnimateEntry { get; set; } = true;
protected BackgroundScreen()
{ {
this.animateOnEnter = animateOnEnter;
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
} }
@ -53,12 +52,11 @@ namespace osu.Game.Screens
public override void OnEntering(ScreenTransitionEvent e) public override void OnEntering(ScreenTransitionEvent e)
{ {
if (animateOnEnter) if (AnimateEntry)
{ {
this.FadeOut(); this.FadeOut();
this.MoveToX(x_movement_amount);
this.FadeIn(TRANSITION_LENGTH, Easing.InOutQuart); this.FadeIn(TRANSITION_LENGTH, Easing.InOutQuart);
this.MoveToX(x_movement_amount);
this.MoveToX(0, TRANSITION_LENGTH, Easing.InOutQuart); this.MoveToX(0, TRANSITION_LENGTH, Easing.InOutQuart);
} }

View File

@ -27,10 +27,14 @@ namespace osu.Game.Screens
if (screen == null) if (screen == null)
return false; return false;
if (EqualityComparer<BackgroundScreen>.Default.Equals((BackgroundScreen)CurrentScreen, screen)) bool isFirstScreen = CurrentScreen == null;
screen.AnimateEntry = !isFirstScreen;
if (EqualityComparer<BackgroundScreen>.Default.Equals((BackgroundScreen?)CurrentScreen, screen))
return false; return false;
base.Push(screen); base.Push(screen);
return true; return true;
} }
} }

View File

@ -42,11 +42,6 @@ namespace osu.Game.Screens.Backgrounds
protected virtual bool AllowStoryboardBackground => true; protected virtual bool AllowStoryboardBackground => true;
public BackgroundScreenDefault(bool animateOnEnter = true)
: base(animateOnEnter)
{
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IAPIProvider api, SkinManager skinManager, OsuConfigManager config) private void load(IAPIProvider api, SkinManager skinManager, OsuConfigManager config)
{ {

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -16,6 +17,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.LoadComplete(); base.LoadComplete();
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText(); EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText();
EditorBeatmap.PlacementObject.BindValueChanged(_ => updateInspectorText());
EditorBeatmap.TransactionBegan += updateInspectorText; EditorBeatmap.TransactionBegan += updateInspectorText;
EditorBeatmap.TransactionEnded += updateInspectorText; EditorBeatmap.TransactionEnded += updateInspectorText;
updateInspectorText(); updateInspectorText();
@ -29,24 +31,33 @@ namespace osu.Game.Screens.Edit.Compose.Components
rollingTextUpdate?.Cancel(); rollingTextUpdate?.Cancel();
rollingTextUpdate = null; rollingTextUpdate = null;
AddInspectorValues(); HitObject[] objects;
if (EditorBeatmap.SelectedHitObjects.Count > 0)
objects = EditorBeatmap.SelectedHitObjects.ToArray();
else if (EditorBeatmap.PlacementObject.Value != null)
objects = new[] { EditorBeatmap.PlacementObject.Value };
else
objects = Array.Empty<HitObject>();
AddInspectorValues(objects);
// I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes. // I'd hope there's a better way to do this, but I don't want to bind to each and every property above to watch for changes.
// This is a good middle-ground for the time being. // This is a good middle-ground for the time being.
if (EditorBeatmap.SelectedHitObjects.Count > 0) if (objects.Length > 0)
rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250); rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
} }
protected virtual void AddInspectorValues() protected virtual void AddInspectorValues(HitObject[] objects)
{ {
switch (EditorBeatmap.SelectedHitObjects.Count) switch (objects.Length)
{ {
case 0: case 0:
AddValue("No selection"); AddValue("No selection");
break; break;
case 1: case 1:
var selected = EditorBeatmap.SelectedHitObjects.Single(); var selected = objects.Single();
AddHeader("Type"); AddHeader("Type");
AddValue($"{selected.GetType().ReadableName()}"); AddValue($"{selected.GetType().ReadableName()}");
@ -105,13 +116,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
default: default:
AddHeader("Selected Objects"); AddHeader("Selected Objects");
AddValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}"); AddValue($"{objects.Length:#,0.##}");
AddHeader("Start Time"); AddHeader("Start Time");
AddValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms"); AddValue($"{objects.Min(o => o.StartTime):#,0.##}ms");
AddHeader("End Time"); AddHeader("End Time");
AddValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms"); AddValue($"{objects.Max(o => o.GetEndTime()):#,0.##}ms");
break; break;
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -16,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
@ -50,14 +49,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
private readonly List<SelectionBlueprint<T>> selectedBlueprints; private readonly List<SelectionBlueprint<T>> selectedBlueprints;
protected SelectionBox SelectionBox { get; private set; } protected SelectionBox SelectionBox { get; private set; } = null!;
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
protected IEditorChangeHandler ChangeHandler { get; private set; } protected IEditorChangeHandler? ChangeHandler { get; private set; }
public SelectionRotationHandler RotationHandler { get; private set; } public SelectionRotationHandler RotationHandler { get; private set; } = null!;
public SelectionScaleHandler ScaleHandler { get; private set; } public SelectionScaleHandler ScaleHandler { get; private set; } = null!;
[Resolved(CanBeNull = true)]
protected OsuContextMenuContainer? ContextMenuContainer { get; private set; }
protected SelectionHandler() protected SelectionHandler()
{ {
@ -230,7 +232,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// Deselect all selected items. /// Deselect all selected items.
/// </summary> /// </summary>
protected void DeselectAll() => SelectedItems.Clear(); protected void DeselectAll()
{
SelectedItems.Clear();
ContextMenuContainer?.CloseMenu();
}
/// <summary> /// <summary>
/// Handle a blueprint becoming selected. /// Handle a blueprint becoming selected.
@ -243,6 +249,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectedItems.Add(blueprint.Item); SelectedItems.Add(blueprint.Item);
selectedBlueprints.Add(blueprint); selectedBlueprints.Add(blueprint);
ContextMenuContainer?.CloseMenu();
} }
/// <summary> /// <summary>
@ -263,7 +271,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <returns>Whether a selection was performed.</returns> /// <returns>Whether a selection was performed.</returns>
internal virtual bool MouseDownSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e) internal virtual bool MouseDownSelectionRequested(SelectionBlueprint<T> blueprint, MouseButtonEvent e)
{ {
if (e.ShiftPressed && e.Button == MouseButton.Right) if (e.Button == MouseButton.Middle || (e.ShiftPressed && e.Button == MouseButton.Right))
{ {
handleQuickDeletion(blueprint); handleQuickDeletion(blueprint);
return true; return true;

View File

@ -69,19 +69,24 @@ namespace osu.Game.Screens.Edit.Compose
if (ruleset == null || composer == null) if (ruleset == null || composer == null)
return base.CreateTimelineContent(); return base.CreateTimelineContent();
TimelineBreakDisplay breakDisplay = new TimelineBreakDisplay
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 0.75f,
};
return wrapSkinnableContent(new Container return wrapSkinnableContent(new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new[]
{ {
// We want to display this below hitobjects to better expose placement objects visually.
// It needs to be above the blueprint container to handle drags on breaks though.
breakDisplay.CreateProxy(),
new TimelineBlueprintContainer(composer), new TimelineBlueprintContainer(composer),
new TimelineBreakDisplay breakDisplay
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 0.75f,
},
} }
}); });
} }

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework; using osu.Framework;
@ -159,7 +160,7 @@ namespace osu.Game.Screens.Edit
private string lastSavedHash; private string lastSavedHash;
private Container<EditorScreen> screenContainer; private ScreenContainer screenContainer;
[CanBeNull] [CanBeNull]
private readonly EditorLoader loader; private readonly EditorLoader loader;
@ -329,7 +330,7 @@ namespace osu.Game.Screens.Edit
Name = "Screen container", Name = "Screen container",
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 40, Bottom = 50 }, Padding = new MarginPadding { Top = 40, Bottom = 50 },
Child = screenContainer = new Container<EditorScreen> Child = screenContainer = new ScreenContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
} }
@ -422,6 +423,7 @@ namespace osu.Game.Screens.Edit
MutationTracker, MutationTracker,
} }
}); });
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); changeHandler?.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
@ -1007,7 +1009,7 @@ namespace osu.Game.Screens.Edit
throw new InvalidOperationException("Editor menu bar switched to an unsupported mode"); throw new InvalidOperationException("Editor menu bar switched to an unsupported mode");
} }
LoadComponentAsync(currentScreen, newScreen => screenContainer.LoadComponentAsync(currentScreen, newScreen =>
{ {
if (newScreen == currentScreen) if (newScreen == currentScreen)
{ {
@ -1385,5 +1387,12 @@ namespace osu.Game.Screens.Edit
{ {
} }
} }
private partial class ScreenContainer : Container<EditorScreen>
{
public new Task LoadComponentAsync<TLoadable>([NotNull] TLoadable component, Action<TLoadable> onLoaded = null, CancellationToken cancellation = default, Scheduler scheduler = null)
where TLoadable : Drawable
=> base.LoadComponentAsync(component, onLoaded, cancellation, scheduler);
}
} }
} }

View File

@ -138,7 +138,7 @@ namespace osu.Game.Screens.Menu
}); });
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L)); buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L));
buttonsPlay.Add(new DailyChallengeButton(@"button-default-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E) buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E)

View File

@ -104,8 +104,7 @@ namespace osu.Game.Screens.Menu
{ {
base.LoadComplete(); base.LoadComplete();
info.BindValueChanged(_ => dailyChallengeChanged(postNotification: true)); info.BindValueChanged(dailyChallengeChanged, true);
dailyChallengeChanged(postNotification: false);
} }
protected override void Update() protected override void Update()
@ -131,7 +130,9 @@ namespace osu.Game.Screens.Menu
} }
} }
private void dailyChallengeChanged(bool postNotification) private long? lastNotifiedDailyChallengeRoomId;
private void dailyChallengeChanged(ValueChangedEvent<DailyChallengeInfo?> _)
{ {
UpdateState(); UpdateState();
@ -152,8 +153,14 @@ namespace osu.Game.Screens.Menu
Room = room; Room = room;
cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet; cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet;
if (postNotification) // We only want to notify the user if a new challenge recently went live.
if (room.StartDate.Value != null
&& Math.Abs((DateTimeOffset.Now - room.StartDate.Value!.Value).TotalSeconds) < 1800
&& room.RoomID.Value != lastNotifiedDailyChallengeRoomId)
{
lastNotifiedDailyChallengeRoomId = room.RoomID.Value;
notificationOverlay?.Post(new NewDailyChallengeNotification(room)); notificationOverlay?.Post(new NewDailyChallengeNotification(room));
}
updateCountdown(); updateCountdown();
Scheduler.AddDelayed(updateCountdown, 1000, true); Scheduler.AddDelayed(updateCountdown, 1000, true);

View File

@ -90,7 +90,7 @@ namespace osu.Game.Screens.Menu
/// </summary> /// </summary>
protected bool UsingThemedIntro { get; private set; } protected bool UsingThemedIntro { get; private set; }
protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(false) protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault
{ {
Colour = Color4.Black Colour = Color4.Black
}; };

View File

@ -150,7 +150,7 @@ namespace osu.Game.Screens.Menu
OnPlaylists = () => this.Push(new Playlists()), OnPlaylists = () => this.Push(new Playlists()),
OnDailyChallenge = room => OnDailyChallenge = room =>
{ {
this.Push(new DailyChallenge(room)); this.Push(new DailyChallengeIntro(room));
}, },
OnExit = () => OnExit = () =>
{ {

View File

@ -21,7 +21,6 @@ namespace osu.Game.Screens.OnlinePlay.Components
private PlaylistItemBackground? background; private PlaylistItemBackground? background;
protected OnlinePlayBackgroundScreen() protected OnlinePlayBackgroundScreen()
: base(false)
{ {
AddInternal(new Box AddInternal(new Box
{ {

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -44,7 +45,7 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{ {
[Cached(typeof(IPreviewTrackOwner))] [Cached(typeof(IPreviewTrackOwner))]
public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap
{ {
private readonly Room room; private readonly Room room;
private readonly PlaylistItem playlistItem; private readonly PlaylistItem playlistItem;
@ -76,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
[Cached] [Cached]
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker(); private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
[Resolved]
private OsuGame? game { get; set; }
[Resolved] [Resolved]
private BeatmapManager beatmapManager { get; set; } = null!; private BeatmapManager beatmapManager { get; set; } = null!;
@ -386,7 +390,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
base.LoadComplete(); base.LoadComplete();
beatmapAvailabilityTracker.SelectedItem.Value = playlistItem; beatmapAvailabilityTracker.SelectedItem.Value = playlistItem;
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem), true);
userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay); userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay);
userModsSelectOverlay.SelectedItem.Value = playlistItem; userModsSelectOverlay.SelectedItem.Value = playlistItem;
@ -398,15 +402,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
dailyChallengeInfo.BindValueChanged(dailyChallengeChanged); dailyChallengeInfo.BindValueChanged(dailyChallengeChanged);
} }
private void trySetDailyChallengeBeatmap()
{
var beatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == playlistItem.Beatmap.OnlineID);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally.
Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID);
applyLoopingToTrack();
}
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() => private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{ {
if (state.NewValue != APIState.Online) if (state.NewValue != APIState.Online)
@ -440,7 +435,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
waves.Show(); waves.Show();
roomManager.JoinRoom(room); roomManager.JoinRoom(room);
applyLoopingToTrack(); startLoopingTrack(this, musicController);
metadataClient.BeginWatchingMultiplayerRoom(room.RoomID.Value!.Value).ContinueWith(t => metadataClient.BeginWatchingMultiplayerRoom(room.RoomID.Value!.Value).ContinueWith(t =>
{ {
@ -452,6 +447,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
MultiplayerPlaylistItemStats[] stats = t.GetResultSafely(); MultiplayerPlaylistItemStats[] stats = t.GetResultSafely();
var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID); var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID);
if (itemStats == null) return; if (itemStats == null) return;
Schedule(() => Schedule(() =>
@ -459,17 +455,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
breakdown.SetInitialCounts(itemStats.TotalScoreDistribution); breakdown.SetInitialCounts(itemStats.TotalScoreDistribution);
totals.SetInitialCounts(itemStats.TotalScoreDistribution.Sum(c => c), itemStats.CumulativeScore); totals.SetInitialCounts(itemStats.TotalScoreDistribution.Sum(c => c), itemStats.CumulativeScore);
}); });
}); }, TaskContinuationOptions.OnlyOnRanToCompletion);
beatmapAvailabilityTracker.SelectedItem.Value = playlistItem;
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true);
userModsSelectOverlay.SelectedItem.Value = playlistItem; userModsSelectOverlay.SelectedItem.Value = playlistItem;
TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem);
} }
public override void OnResuming(ScreenTransitionEvent e) public override void OnResuming(ScreenTransitionEvent e)
{ {
base.OnResuming(e); base.OnResuming(e);
applyLoopingToTrack(); startLoopingTrack(this, musicController);
// re-apply mods as they may have been changed by a child screen // re-apply mods as they may have been changed by a child screen
// (one known instance of this is showing a replay). // (one known instance of this is showing a replay).
updateMods(); updateMods();
@ -498,17 +494,30 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
return base.OnExiting(e); return base.OnExiting(e);
} }
private void applyLoopingToTrack() public static void TrySetDailyChallengeBeatmap(OsuScreen screen, BeatmapManager beatmaps, RulesetStore rulesets, MusicController music, PlaylistItem item)
{ {
if (!this.IsCurrentScreen()) if (!screen.IsCurrentScreen())
return; return;
var track = Beatmap.Value?.Track; var beatmap = beatmaps.QueryBeatmap(b => b.OnlineID == item.Beatmap.OnlineID);
screen.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally.
screen.Ruleset.Value = rulesets.GetRuleset(item.RulesetID);
startLoopingTrack(screen, music);
}
private static void startLoopingTrack(OsuScreen screen, MusicController music)
{
if (!screen.IsCurrentScreen())
return;
var track = screen.Beatmap.Value?.Track;
if (track != null) if (track != null)
{ {
Beatmap.Value?.PrepareTrackForPreview(true); screen.Beatmap.Value?.PrepareTrackForPreview(true);
musicController.EnsurePlayingSomething(); music.EnsurePlayingSomething();
} }
} }
@ -546,5 +555,21 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
if (metadataClient.IsNotNull()) if (metadataClient.IsNotNull())
metadataClient.MultiplayerRoomScoreSet -= onRoomScoreSet; metadataClient.MultiplayerRoomScoreSet -= onRoomScoreSet;
} }
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
{
if (!this.IsCurrentScreen())
return;
// We can only handle the current daily challenge beatmap.
// If the import was for a different beatmap, pass the duty off to global handling.
if (beatmap.BeatmapSetInfo.OnlineID != playlistItem.Beatmap.BeatmapSet!.OnlineID)
{
this.Exit();
game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID);
}
// And if we're handling, we don't really have much to do here.
}
} }
} }

View File

@ -0,0 +1,534 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.Play.HUD;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
public partial class DailyChallengeIntro : OsuScreen
{
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override bool? ApplyModTrackAdjustments => true;
private readonly Room room;
private readonly PlaylistItem item;
private Container introContent = null!;
private Container topTitleDisplay = null!;
private Container bottomDateDisplay = null!;
private Container beatmapBackground = null!;
private Box flash = null!;
private FillFlowContainer beatmapContent = null!;
private Container titleContainer = null!;
private bool beatmapBackgroundLoaded;
private bool animationBegan;
private IBindable<StarDifficulty?> starDifficulty = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Cached]
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
private bool shouldBePlayingMusic;
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
[Resolved]
private MusicController musicController { get; set; } = null!;
private Sample? dateWindupSample;
private Sample? dateImpactSample;
private Sample? beatmapWindupSample;
private Sample? beatmapImpactSample;
private SampleChannel? dateWindupChannel;
private SampleChannel? dateImpactChannel;
private SampleChannel? beatmapWindupChannel;
private SampleChannel? beatmapImpactChannel;
private IDisposable? duckOperation;
public DailyChallengeIntro(Room room)
{
this.room = room;
item = room.Playlist.Single();
ValidForResume = false;
}
protected override BackgroundScreen CreateBackground() => new DailyChallengeIntroBackgroundScreen(colourProvider);
[BackgroundDependencyLoader]
private void load(BeatmapDifficultyCache difficultyCache, BeatmapModelDownloader beatmapDownloader, OsuConfigManager config, AudioManager audio)
{
const float horizontal_info_size = 500f;
Ruleset ruleset = Ruleset.Value.CreateInstance();
StarRatingDisplay starRatingDisplay;
InternalChildren = new Drawable[]
{
beatmapAvailabilityTracker,
introContent = new Container
{
Alpha = 0f,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = new Vector2(OsuGame.SHEAR, 0f),
Children = new Drawable[]
{
titleContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
topTitleDisplay = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
CornerRadius = 10f,
Masking = true,
X = -10,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Today's Challenge",
Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f },
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
},
}
},
bottomDateDisplay = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
CornerRadius = 10f,
Masking = true,
X = 10,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = room.Name.Value.Split(':', StringSplitOptions.TrimEntries).Last(),
Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f },
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
},
}
},
}
},
beatmapContent = new FillFlowContainer
{
AlwaysPresent = true, // so we can get the size ahead of time
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Scale = new Vector2(0.001f),
Spacing = new Vector2(10),
Children = new Drawable[]
{
beatmapBackground = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Size = new Vector2(horizontal_info_size, 150f),
CornerRadius = 20f,
BorderColour = colourProvider.Content2,
BorderThickness = 3f,
Masking = true,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
flash = new Box
{
Colour = Color4.White,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
Depth = float.MinValue,
}
}
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = horizontal_info_size,
AutoSizeAxes = Axes.Y,
CornerRadius = 10f,
Masking = true,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(5f),
Children = new Drawable[]
{
new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Shear = new Vector2(-OsuGame.SHEAR, 0f),
MaxWidth = horizontal_info_size,
Text = item.Beatmap.BeatmapSet!.Metadata.GetDisplayTitleRomanisable(false),
Padding = new MarginPadding { Horizontal = 5f },
Font = OsuFont.GetFont(size: 26),
},
new TruncatingSpriteText
{
Text = $"Difficulty: {item.Beatmap.DifficultyName}",
Font = OsuFont.GetFont(size: 20, italics: true),
MaxWidth = horizontal_info_size,
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
new TruncatingSpriteText
{
Text = $"by {item.Beatmap.Metadata.Author.Username}",
Font = OsuFont.GetFont(size: 16, italics: true),
MaxWidth = horizontal_info_size,
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
starRatingDisplay = new StarRatingDisplay(default)
{
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Margin = new MarginPadding(5),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}
}
},
}
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = horizontal_info_size,
AutoSizeAxes = Axes.Y,
CornerRadius = 10f,
Masking = true,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background3,
RelativeSizeAxes = Axes.Both,
},
new ModFlowDisplay
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Shear = new Vector2(-OsuGame.SHEAR, 0f),
Current =
{
Value = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray()
},
}
}
}
}
},
}
}
};
starDifficulty = difficultyCache.GetBindableDifficulty(item.Beatmap);
starDifficulty.BindValueChanged(star =>
{
if (star.NewValue != null)
starRatingDisplay.Current.Value = star.NewValue.Value;
}, true);
LoadComponentAsync(new OnlineBeatmapSetCover(item.Beatmap.BeatmapSet as IBeatmapSetOnlineInfo)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fit,
Scale = new Vector2(1.2f),
Shear = new Vector2(-OsuGame.SHEAR, 0f),
}, c =>
{
beatmapBackground.Add(c);
beatmapBackgroundLoaded = true;
updateAnimationState();
});
if (config.Get<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps))
{
if (!beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = item.Beatmap.BeatmapSet!.OnlineID }))
beatmapDownloader.Download(item.Beatmap.BeatmapSet!, config.Get<bool>(OsuSetting.PreferNoVideo));
}
dateWindupSample = audio.Samples.Get(@"DailyChallenge/date-windup");
dateImpactSample = audio.Samples.Get(@"DailyChallenge/date-impact");
beatmapWindupSample = audio.Samples.Get(@"DailyChallenge/beatmap-windup");
beatmapImpactSample = audio.Samples.Get(@"DailyChallenge/beatmap-impact");
}
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
beatmapAvailabilityTracker.SelectedItem.Value = item;
beatmapAvailabilityTracker.Availability.BindValueChanged(availability =>
{
if (shouldBePlayingMusic && availability.NewValue.State == DownloadState.LocallyAvailable)
DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item);
}, true);
this.FadeInFromZero(400, Easing.OutQuint);
updateAnimationState();
playDateWindupSample();
}
public override void OnSuspending(ScreenTransitionEvent e)
{
this.FadeOut(800, Easing.OutQuint);
base.OnSuspending(e);
}
private void updateAnimationState()
{
if (!beatmapBackgroundLoaded || !this.IsCurrentScreen())
return;
if (animationBegan)
return;
beginAnimation();
animationBegan = true;
}
private void beginAnimation()
{
using (BeginDelayedSequence(200))
{
introContent.Show();
const float y_offset_start = 260;
const float y_offset_end = 20;
topTitleDisplay
.FadeInFromZero(400, Easing.OutQuint);
topTitleDisplay.MoveToY(-y_offset_start)
.MoveToY(-y_offset_end, 300, Easing.OutQuint)
.Then()
.MoveToY(0, 4000);
bottomDateDisplay.MoveToY(y_offset_start)
.MoveToY(y_offset_end, 300, Easing.OutQuint)
.Then()
.MoveToY(0, 4000);
using (BeginDelayedSequence(150))
{
Schedule(() =>
{
playDateImpactSample();
playBeatmapWindupSample();
duckOperation?.Dispose();
duckOperation = musicController.Duck(new DuckParameters
{
RestoreDuration = 1500f,
});
});
using (BeginDelayedSequence(2750))
{
Schedule(() =>
{
duckOperation?.Dispose();
});
}
}
using (BeginDelayedSequence(1000))
{
beatmapContent
.ScaleTo(3)
.ScaleTo(1f, 500, Easing.In)
.Then()
.ScaleTo(1.1f, 4000);
using (BeginDelayedSequence(100))
{
titleContainer
.ScaleTo(0.4f, 400, Easing.In)
.FadeOut(500, Easing.OutQuint);
}
using (BeginDelayedSequence(240))
{
beatmapContent.FadeInFromZero(280, Easing.InQuad);
using (BeginDelayedSequence(300))
{
Schedule(() =>
{
shouldBePlayingMusic = true;
DailyChallenge.TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, item);
ApplyToBackground(bs => ((RoomBackgroundScreen)bs).SelectedItem.Value = item);
playBeatmapImpactSample();
});
}
using (BeginDelayedSequence(400))
flash.FadeOutFromOne(5000, Easing.OutQuint);
using (BeginDelayedSequence(2600))
{
Schedule(() =>
{
if (this.IsCurrentScreen())
this.Push(new DailyChallenge(room));
});
}
}
}
}
}
private void playDateWindupSample()
{
dateWindupChannel = dateWindupSample?.GetChannel();
dateWindupChannel?.Play();
}
private void playDateImpactSample()
{
dateImpactChannel = dateImpactSample?.GetChannel();
dateImpactChannel?.Play();
}
private void playBeatmapWindupSample()
{
beatmapWindupChannel = beatmapWindupSample?.GetChannel();
beatmapWindupChannel?.Play();
}
private void playBeatmapImpactSample()
{
beatmapImpactChannel = beatmapImpactSample?.GetChannel();
beatmapImpactChannel?.Play();
}
protected override void Dispose(bool isDisposing)
{
resetAudio();
base.Dispose(isDisposing);
}
private void resetAudio()
{
dateWindupChannel?.Stop();
dateImpactChannel?.Stop();
beatmapWindupChannel?.Stop();
beatmapImpactChannel?.Stop();
duckOperation?.Dispose();
}
private partial class DailyChallengeIntroBackgroundScreen : RoomBackgroundScreen
{
private readonly OverlayColourProvider colourProvider;
public DailyChallengeIntroBackgroundScreen(OverlayColourProvider colourProvider)
: base(null)
{
this.colourProvider = colourProvider;
}
[BackgroundDependencyLoader]
private void load()
{
AddInternal(new Box
{
Depth = float.MinValue,
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5.Opacity(0.6f),
});
}
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -41,7 +42,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Background.Alpha = 0.2f; Background.Alpha = 0.2f;
TextBox.FocusLost = () => expandedFromTextBoxFocus.Value = false; TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder;
TextBox.Focus = () => TextBox.PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder;
TextBox.FocusLost = () =>
{
TextBox.PlaceholderText = ChatStrings.InGameInputPlaceholder;
expandedFromTextBoxFocus.Value = false;
};
} }
protected override bool OnHover(HoverEvent e) => true; // use UI mouse cursor. protected override bool OnHover(HoverEvent e) => true; // use UI mouse cursor.

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;

View File

@ -14,11 +14,10 @@ namespace osu.Game.Screens.Play
public ArgonKeyCounterDisplay() public ArgonKeyCounterDisplay()
{ {
InternalChild = KeyFlow = new FillFlowContainer<KeyCounter> Child = KeyFlow = new FillFlowContainer<KeyCounter>
{ {
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Alpha = 0,
Spacing = new Vector2(2), Spacing = new Vector2(2),
}; };
} }

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