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": {
"version": "2024.517.0",
"version": "2024.802.0",
"commands": [
"localisation"
]

View File

@ -121,9 +121,7 @@ jobs:
build-only-ios:
name: Build only (iOS)
# `macos-13` is required, because the newest Microsoft.iOS.Sdk versions require Xcode 14.3.
# 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
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
@ -137,8 +135,5 @@ jobs:
- name: Install .NET Workloads
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
run: dotnet build -c Debug osu.iOS

View File

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

View File

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

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Skinning.Legacy
@ -28,76 +29,93 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
if (lookup is SkinComponentsContainerLookup containerLookup)
switch (lookup)
{
switch (containerLookup.Target)
{
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
var components = base.GetDrawableComponent(lookup) as Container;
case SkinComponentsContainerLookup containerLookup:
if (containerLookup.Target != SkinComponentsContainerLookup.TargetArea.MainHUDComponents)
return base.GetDrawableComponent(lookup);
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.
// todo: this should be done in an elegant way per ruleset, defining which HUD skin components should be displayed.
foreach (var legacyComboCounter in components.OfType<LegacyComboCounter>())
legacyComboCounter.HiddenByRulesetImplementation = false;
// 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;
}
return components;
}
}
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)
})
{
Children = new Drawable[]
{
if (hasOldStyleCatcherSprite())
return new LegacyCatcherOld();
new LegacyKeyCounterDisplay(),
}
};
if (hasNewStyleCatcherSprite())
return new LegacyCatcherNew();
case CatchSkinComponentLookup catchSkinComponent:
switch (catchSkinComponent.Component)
{
case CatchSkinComponents.Fruit:
if (hasPear)
return new LegacyFruitPiece();
return null;
return null;
case CatchSkinComponents.CatchComboCounter:
if (providesComboCounter)
return new LegacyCatchComboCounter();
case CatchSkinComponents.Banana:
if (GetTexture("fruit-bananas") != null)
return new LegacyBananaPiece();
return null;
return null;
case CatchSkinComponents.HitExplosion:
if (hasOldStyleCatcherSprite() || hasNewStyleCatcherSprite())
return new LegacyHitExplosion();
case CatchSkinComponents.Droplet:
if (GetTexture("fruit-drop") != null)
return new LegacyDropletPiece();
return null;
return null;
default:
throw new UnsupportedSkinComponentException(lookup);
}
case CatchSkinComponents.Catcher:
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);

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
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();
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
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();
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
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();

View File

@ -42,7 +42,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
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)
{
@ -50,14 +55,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
foreach (var h in hitObjects)
h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
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.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++)
{

View File

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

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@ -86,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
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) +
(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))
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) +
(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))
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.
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 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;
}
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 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
snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true;

View File

@ -11,14 +11,14 @@ namespace osu.Game.Rulesets.Osu.Edit
{
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 lastInSelection = (OsuHitObject)EditorBeatmap.SelectedHitObjects.MaxBy(ho => ho.GetEndTime())!;
var firstInSelection = (OsuHitObject)objects.MinBy(ho => ho.StartTime)!;
var lastInSelection = (OsuHitObject)objects.MaxBy(ho => ho.GetEndTime())!;
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.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components;
@ -50,12 +51,33 @@ namespace osu.Game.Rulesets.Osu.Edit
{
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...
foreach (var h in hitObjects)
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
h.Position += localDelta;
// but this will be corrected.
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;
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu.Objects;
@ -41,139 +42,179 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
{
if (lookup is OsuSkinComponentLookup osuComponent)
switch (lookup)
{
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 SkinComponentsContainerLookup containerLookup:
// Only handle per ruleset defaults here.
if (containerLookup.Ruleset == null)
return base.GetDrawableComponent(lookup);
case OsuSkinComponents.SliderScorePoint:
return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS);
// Skin has configuration.
if (base.GetDrawableComponent(lookup) is UserConfiguredLayoutContainer d)
return d;
case OsuSkinComponents.SliderFollowCircle:
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
if (followCircleContent != null)
return new LegacyFollowCircle(followCircleContent);
// Our own ruleset components default.
switch (containerLookup.Target)
{
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:
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
return new LegacySliderBall(this);
return null;
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:
if (hasHitCircle.Value)
return new LegacySliderBody();
case OsuSkinComponents.SliderScorePoint:
return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS);
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;
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.SliderBall:
if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
return new LegacySliderBall(this);
case OsuSkinComponents.SpinnerBody:
bool hasBackground = GetTexture("spinner-background") != null;
return null;
if (GetTexture("spinner-top") != null && !hasBackground)
return new LegacyNewStyleSpinner();
else if (hasBackground)
return new LegacyOldStyleSpinner();
case OsuSkinComponents.SliderBody:
if (hasHitCircle.Value)
return new LegacySliderBody();
return null;
return null;
case OsuSkinComponents.ApproachCircle:
if (GetTexture(@"approachcircle") != null)
return new LegacyApproachCircle();
case OsuSkinComponents.SliderTailHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderendcircle", false);
return null;
return null;
default:
throw new UnsupportedSkinComponentException(lookup);
}
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;
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)

View File

@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Timing;
using osuTK;
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
parts[i].InvalidationID = -1;
}
AddLayout(partSizeCache);
}
[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>
/// The amount of time to fade the cursor trail pieces.
/// </summary>
@ -156,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected void AddTrail(Vector2 position)
{
position = ToLocalSpace(position);
if (InterpolateMovements)
{
if (!lastPosition.HasValue)
@ -174,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length;
Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier;
float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
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].InvalidationID;

View File

@ -206,6 +206,15 @@ namespace osu.Game.Rulesets.Osu.UI
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
{
public void Add(Drawable proxy) => AddInternal(proxy);

View File

@ -33,9 +33,30 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader]
private void load()
{
OsuResumeOverlayInputBlocker? inputBlocker = null;
if (drawableRuleset != null)
{
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
}
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;
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
// 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());
ResumeRequested?.Invoke();
return true;
}
@ -143,5 +161,27 @@ namespace osu.Game.Rulesets.Osu.UI
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:
// This should eventually be moved to a skin setting, when supported.
@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
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]
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]
public void TestNoChanges()
{
@ -272,21 +310,25 @@ namespace osu.Game.Tests.Database
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
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);
realm.Run(r => r.Refresh());
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));
});
}

View File

@ -52,10 +52,7 @@ namespace osu.Game.Tests.Editing
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[]
{
composer = new TestHitObjectComposer()
};
Child = composer = new TestHitObjectComposer();
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 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>
/// Create a test beatmap set model.
@ -88,7 +93,7 @@ namespace osu.Game.Tests.Resources
RulesetInfo getRuleset() => rulesets?[j++ % rulesets.Length];
int setId = Interlocked.Increment(ref importId);
int setId = GetNextTestID();
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
"Archives/modified-default-20230809.osk",
// Covers legacy rank display
"Archives/modified-classic-20230809.osk"
"Archives/modified-classic-20230809.osk",
// Covers legacy key counter
"Archives/modified-classic-20240724.osk"
};
/// <summary>

View File

@ -304,11 +304,6 @@ namespace osu.Game.Tests.Visual.Background
{
private bool? lastLoadTriggerCausedChange;
public TestBackgroundScreenDefault()
: base(false)
{
}
public override bool 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();
[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]
public void TestCommitPlacementViaGlobalAction()
{

View File

@ -9,7 +9,6 @@ using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
@ -45,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
// best way to check without exposing.
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()
{
@ -79,7 +78,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("showhud is set", () => hudOverlay.ShowHud.Value);
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);
}
@ -104,7 +103,7 @@ namespace osu.Game.Tests.Visual.Gameplay
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.
AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent);
AddAssert("key counter flow not affected", () => keyCounterContent.IsPresent);
}
[Test]
@ -150,11 +149,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
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);
AddUntilStep("hidetarget is visible", () => hideTarget.Alpha, () => Is.GreaterThan(0));
AddUntilStep("key counters still hidden", () => !keyCounterFlow.IsPresent);
AddUntilStep("key counters still hidden", () => !keyCounterContent.IsPresent);
}
[Test]

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
using osuTK.Input;
@ -56,6 +57,11 @@ namespace osu.Game.Tests.Visual.Gameplay
Anchor = Anchor.Centre,
Scale = new Vector2(1, -1)
},
new LegacyKeyCounterDisplay
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@ -89,6 +95,12 @@ namespace osu.Game.Tests.Visual.Gameplay
Anchor = Anchor.Centre,
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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
@ -13,6 +14,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
@ -32,6 +34,23 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved]
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) =>
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("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
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);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
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]
@ -90,30 +107,29 @@ namespace osu.Game.Tests.Visual.Gameplay
KeyCounter counter = null!;
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);
AddStep("press D", () => InputManager.PressKey(Key.D));
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, true);
AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false);
AddStep("pause", () => Player.Pause());
AddStep("press D", () => InputManager.PressKey(Key.D));
AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, false);
AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false);
AddStep("resume", () => Player.Resume());
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);
AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 2, false);
}
@ -145,8 +161,11 @@ namespace osu.Game.Tests.Visual.Gameplay
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));
checkKey(() => counterZ, 1, false);
checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
}
[Test]
@ -155,12 +174,12 @@ namespace osu.Game.Tests.Visual.Gameplay
KeyCounter counter = null!;
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("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, true);
AddStep("resume", () => Player.Resume());
@ -202,12 +221,14 @@ namespace osu.Game.Tests.Visual.Gameplay
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));
checkKey(() => counterZ, 1, false);
checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, true);
AddStep("release X", () => InputManager.ReleaseKey(Key.X));
checkKey(() => counterZ, 1, false);
checkKey(() => counterX, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
}
[Test]
@ -216,24 +237,50 @@ namespace osu.Game.Tests.Visual.Gameplay
KeyCounter counter = null!;
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);
AddStep("pause", () => Player.Pause());
AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("press D", () => InputManager.PressKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
AddStep("press space", () => InputManager.PressKey(Key.Space));
AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
checkKey(() => counter, 1, true);
AddStep("release D", () => InputManager.ReleaseKey(Key.D));
AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
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)
{
AddStep("set ruleset", () => currentRuleset = createRuleset());
@ -241,9 +288,10 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinComponentsContainer>().All(s => s.ComponentsLoaded));
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(20000));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(20000).Within(500));
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500));
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)

View File

@ -7,21 +7,25 @@ using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning;
using osu.Game.Skinning.Components;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
@ -39,6 +43,9 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
public readonly EditorClipboard Clipboard = new EditorClipboard();
[Resolved]
private SkinManager skins { get; set; } = null!;
private SkinComponentsContainer targetContainer => Player.ChildrenOfType<SkinComponentsContainer>().First();
[SetUpSteps]
@ -46,6 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
base.SetUpSteps();
AddStep("reset skin", () => skins.CurrentSkinInfo.SetDefault());
AddUntilStep("wait for hud load", () => targetContainer.ComponentsLoaded);
AddStep("reload skin editor", () =>
@ -369,6 +377,93 @@ namespace osu.Game.Tests.Visual.Gameplay
() => 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();
private partial class TestSkinEditorChangeHandler : SkinEditorChangeHandler

View File

@ -4,7 +4,6 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
@ -28,17 +27,5 @@ namespace osu.Game.Tests.Visual.Gameplay
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.Mods;
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.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osu.Game.Tests.Gameplay;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -91,10 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
SetContents(_ =>
{
hudOverlay = new HUDOverlay(null, Array.Empty<Mod>());
// Add any key just to display the key counter visually.
hudOverlay.InputCountController.Add(new KeyCounterKeyboardTrigger(Key.Space));
hudOverlay = new HUDOverlay(new DrawableOsuRuleset(new OsuRuleset(), new OsuBeatmap()), Array.Empty<Mod>());
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.Menu;
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);
}
[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]
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() });
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]

View File

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

View File

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

View File

@ -19,7 +19,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@ -33,6 +32,7 @@ using osu.Game.Overlays.Chat.ChannelList;
using osuTK;
using osuTK.Input;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Online
{
@ -122,7 +122,7 @@ namespace osu.Game.Tests.Visual.Online
return true;
case PostMessageRequest postMessage:
postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000))
postMessage.TriggerSuccess(new Message(TestResources.GetNextTestID())
{
Content = postMessage.Message.Content,
ChannelId = postMessage.Message.ChannelId,
@ -719,7 +719,8 @@ namespace osu.Game.Tests.Visual.Online
private Channel createPrivateChannel()
{
int id = RNG.Next(0, DummyAPIAccess.DUMMY_USER_ID - 1);
int id = TestResources.GetNextTestID();
return new Channel(new APIUser
{
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]
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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
@ -24,7 +25,17 @@ namespace osu.Game.Tests.Visual.Online
[SetUpSteps]
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]
@ -131,6 +142,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue,
PlayMode = "osu",
});
return true;
}
@ -174,6 +186,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue,
PlayMode = "osu",
}));
int hue2 = 0;
@ -189,6 +202,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue2,
PlayMode = "osu",
}));
}
@ -282,6 +296,15 @@ namespace osu.Game.Tests.Visual.Online
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",
Colour = "ff0000",
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);
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);
}
}
@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
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);
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);
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);
}
}

View File

@ -58,6 +58,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
new PlaylistItem(beatmap)
},
StartDate = { Value = DateTimeOffset.Now.AddMinutes(-5) },
EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) }
});
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));
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
@ -105,7 +111,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
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.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -10,6 +11,8 @@ using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osuTK;
using osuTK.Input;
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 ModCustomisationPanel panel = null!;
private ModCustomisationHeader header = null!;
private Container content = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = Array.Empty<Mod>();
InputManager.MoveMouseTo(Vector2.One);
Child = new Container
{
RelativeSizeAxes = Axes.Both,
@ -36,6 +44,9 @@ namespace osu.Game.Tests.Visual.UserInterface
SelectedMods = { BindTarget = SelectedMods },
}
};
header = panel.Children.OfType<ModCustomisationHeader>().First();
content = panel.Children.OfType<Container>().First();
});
[Test]
@ -44,23 +55,112 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set DT", () =>
{
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", () =>
{
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", () =>
{
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", () =>
{
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 mods", () => SelectedMods.SetDefault());
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 up presets", () =>
{
@ -998,7 +999,9 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("press mouse", () => InputManager.PressButton(MouseButton.Left));
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
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)
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)
{
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();

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)
{
var originalDateAdded = original.DateAdded;
Guid originalId = original.ID;
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 (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.
ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst);
});
@ -79,7 +84,7 @@ namespace osu.Game.Beatmaps
original.DeletePending = true;
// Transfer local values which should be persisted across a beatmap update.
updated.DateAdded = original.DateAdded;
updated.DateAdded = originalDateAdded;
transferCollectionReferences(realm, original, updated);
@ -278,6 +283,9 @@ namespace osu.Game.Beatmaps
protected override void UndeleteForReuse(BeatmapSetInfo existing)
{
if (!existing.DeletePending)
return;
base.UndeleteForReuse(existing);
existing.DateAdded = DateTimeOffset.UtcNow;
}

View File

@ -563,7 +563,7 @@ namespace osu.Game.Beatmaps
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

View File

@ -18,6 +18,12 @@ namespace osu.Game.Beatmaps.Formats
{
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 LegacyDecoder(int version)
@ -93,14 +99,8 @@ namespace osu.Game.Beatmaps.Formats
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)
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");
}
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 (!(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).
ShowDragHandle.Value = false;
Masking = true;
CornerRadius = item_height / 2;
}
protected override Drawable CreateContent() => new ItemContent(Model);
@ -50,7 +53,7 @@ namespace osu.Game.Collections
/// <summary>
/// The main content of the <see cref="DrawableCollectionListItem"/>.
/// </summary>
private partial class ItemContent : CircularContainer
private partial class ItemContent : CompositeDrawable
{
private readonly Live<BeatmapCollection> collection;
@ -65,13 +68,12 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.X;
Height = item_height;
Masking = true;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new[]
InternalChildren = new[]
{
collection.IsManaged
? 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!;
@ -155,7 +157,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = fadeContainer = new Container
Child = fadeContainer = new Container
{
RelativeSizeAxes = Axes.Both,
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);
@ -195,12 +205,7 @@ namespace osu.Game.Collections
{
background.FlashColour(Color4.White, 150);
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0)
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
return true;
return base.OnClick(e);
}
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.Token, string.Empty);
#pragma warning disable CS0618 // Type or member is obsolete
// 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.AutomaticallyDownloadMissingBeatmaps, true);
SetDefault(OsuSetting.SavePassword, true).ValueChanged += enabled =>
{
@ -244,12 +238,6 @@ namespace osu.Game.Configuration
// migrations can be added here using a condition like:
// 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()
@ -424,9 +412,6 @@ namespace osu.Game.Configuration
EditorAutoSeekOnPlacement,
DiscordRichPresence,
[Obsolete($"Use {nameof(AutomaticallyDownloadMissingBeatmaps)} instead.")] // can be removed 20240318
AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent,
LastProcessedMetadataId,
SafeAreaConsiderations,

View File

@ -64,6 +64,7 @@ namespace osu.Game.Graphics.Containers
{
InternalChildren = new Drawable[]
{
new HoverClickSounds(),
new GridContainer
{
RelativeSizeAxes = Axes.X,
@ -92,7 +93,6 @@ namespace osu.Game.Graphics.Containers
ColumnDimensions = 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
{
[Cached(typeof(OsuContextMenuContainer))]
public partial class OsuContextMenuContainer : ContextMenuContainer
{
[Cached]
private OsuContextMenuSamples samples = new OsuContextMenuSamples();
private OsuContextMenu menu = null!;
public OsuContextMenuContainer()
{
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.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Graphics.Cursor
{
@ -27,15 +27,20 @@ namespace osu.Game.Graphics.Cursor
public partial class OsuTooltip : Tooltip
{
private const float max_width = 500;
private readonly Box background;
private readonly OsuSpriteText text;
private readonly TextFlowContainer text;
private bool instantMovement = true;
public override void SetContent(LocalisableString contentString)
{
if (contentString == text.Text) return;
private LocalisableString lastContent;
text.Text = contentString;
public override void SetContent(LocalisableString content)
{
if (content.Equals(lastContent))
return;
text.Text = content;
if (IsPresent)
{
@ -44,6 +49,8 @@ namespace osu.Game.Graphics.Cursor
}
else
AutoSizeDuration = 0;
lastContent = content;
}
public OsuTooltip()
@ -65,10 +72,14 @@ namespace osu.Game.Graphics.Cursor
RelativeSizeAxes = Axes.Both,
Alpha = 0.9f,
},
text = new OsuSpriteText
text = new TextFlowContainer(f =>
{
Padding = new MarginPadding(5),
Font = OsuFont.GetFont(weight: FontWeight.Regular)
f.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>
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>
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}";
}
}

View File

@ -272,6 +272,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("groups")]
public APIUserGroup[] Groups;
[JsonProperty("daily_challenge_user_stats")]
public APIUserDailyChallengeStatistics DailyChallengeStatistics = new APIUserDailyChallengeStatistics();
public override string ToString() => Username;
/// <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 Action Focus;
public Action FocusLost;
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
@ -153,13 +156,17 @@ namespace osu.Game.Online.Chat
BackgroundFocused = new Color4(10, 10, 10, 255);
}
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
Focus?.Invoke();
}
protected override void OnFocusLost(FocusLostEvent e)
{
base.OnFocusLost(e);
FocusLost?.Invoke();
}
public Action FocusLost;
}
public partial class StandAloneDrawableChannel : DrawableChannel

View File

@ -782,7 +782,21 @@ namespace osu.Game.Online.Multiplayer
}
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);

View File

@ -642,10 +642,10 @@ namespace osu.Game
Live<BeatmapSetInfo> databasedSet = null;
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)
databasedSet ??= BeatmapManager.QueryBeatmapSet(s => s.Hash == localBeatmap.Hash);
databasedSet ??= BeatmapManager.QueryBeatmapSet(s => s.Hash == localBeatmap.Hash && !s.DeletePending);
if (databasedSet == null)
{

View File

@ -18,6 +18,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osuTK;
@ -104,6 +105,8 @@ namespace osu.Game.Overlays.Chat
}
}
private bool isMention;
/// <summary>
/// The colour used to paint the author's username.
/// </summary>
@ -255,12 +258,21 @@ namespace osu.Game.Overlays.Chat
private void styleMessageContent(SpriteText text)
{
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);
text.Colour = messageHasColour ? Color4Extensions.FromHex(message.Sender.Colour) : colourProvider?.Content1 ?? Colour4.White;
Color4 messageColour = 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()
{
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
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.AddLinks(message.DisplayContent, message.Links);
}

View File

@ -9,12 +9,14 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osuTK;
using osuTK.Graphics;
using static osu.Game.Overlays.Mods.ModCustomisationPanel;
namespace osu.Game.Overlays.Mods
{
@ -29,11 +31,13 @@ namespace osu.Game.Overlays.Mods
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;
}
@ -54,6 +58,7 @@ namespace osu.Game.Overlays.Mods
RelativeSizeAxes = Axes.Both,
Colour = Color4.White.Opacity(0.4f),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
new OsuSpriteText
{
@ -101,10 +106,48 @@ namespace osu.Game.Overlays.Mods
}
}, 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);
}
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 Expanded = new BindableBool();
public readonly Bindable<ModCustomisationPanelState> ExpandedState = new Bindable<ModCustomisationPanelState>();
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.
// 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).
public override bool HandlePositionalInput => Expanded.Value;
public override bool HandleNonPositionalInput => Expanded.Value;
// (handling OnHover or overriding Block{Non}PositionalInput doesn't work).
public override bool HandlePositionalInput => ExpandedState.Value != ModCustomisationPanelState.Collapsed;
public override bool HandleNonPositionalInput => ExpandedState.Value != ModCustomisationPanelState.Collapsed;
[BackgroundDependencyLoader]
private void load()
@ -57,15 +57,15 @@ namespace osu.Game.Overlays.Mods
InternalChildren = new Drawable[]
{
new ModCustomisationHeader
new ModCustomisationHeader(this)
{
Depth = float.MinValue,
RelativeSizeAxes = Axes.X,
Height = header_height,
Enabled = { BindTarget = Enabled },
Expanded = { BindTarget = Expanded },
ExpandedState = { BindTarget = ExpandedState },
},
content = new FocusGrabbingContainer
content = new FocusGrabbingContainer(this)
{
RelativeSizeAxes = Axes.X,
BorderColour = colourProvider.Dark3,
@ -80,7 +80,7 @@ namespace osu.Game.Overlays.Mods
Roundness = 5f,
Colour = Color4.Black.Opacity(0.25f),
},
Expanded = { BindTarget = Expanded },
ExpandedState = { BindTarget = ExpandedState },
Children = new Drawable[]
{
new Box
@ -122,7 +122,7 @@ namespace osu.Game.Overlays.Mods
this.FadeColour(OsuColour.Gray(e.NewValue ? 1f : 0.6f), 300, Easing.OutQuint);
}, true);
Expanded.BindValueChanged(_ => updateDisplay(), true);
ExpandedState.BindValueChanged(_ => updateDisplay(), true);
SelectedMods.BindValueChanged(_ => updateMods(), true);
FinishTransforms(true);
@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Mods
protected override bool OnClick(ClickEvent e)
{
Expanded.Value = false;
ExpandedState.Value = ModCustomisationPanelState.Collapsed;
return base.OnClick(e);
}
@ -147,7 +147,7 @@ namespace osu.Game.Overlays.Mods
switch (e.Action)
{
case GlobalAction.Back:
Expanded.Value = false;
ExpandedState.Value = ModCustomisationPanelState.Collapsed;
return true;
}
@ -162,7 +162,7 @@ namespace osu.Game.Overlays.Mods
{
content.ClearTransforms();
if (Expanded.Value)
if (ExpandedState.Value != ModCustomisationPanelState.Collapsed)
{
content.AutoSizeDuration = 400;
content.AutoSizeEasing = Easing.OutQuint;
@ -179,7 +179,7 @@ namespace osu.Game.Overlays.Mods
private void updateMods()
{
Expanded.Value = false;
ExpandedState.Value = ModCustomisationPanelState.Collapsed;
sectionsFlow.Clear();
// 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
{
public IBindable<bool> Expanded { get; } = new BindableBool();
public readonly Bindable<ModCustomisationPanelState> ExpandedState = new Bindable<ModCustomisationPanelState>();
public override bool RequestsFocus => Expanded.Value;
public override bool AcceptsFocus => Expanded.Value;
public override bool RequestsFocus => panel.ExpandedState.Value != ModCustomisationPanelState.Collapsed;
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
{
Padding = new MarginPadding { Top = 1, Bottom = 3 },
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{

View File

@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Mods
ActiveMods.Value = ComputeActiveMods();
}, true);
customisationPanel.Expanded.BindValueChanged(_ => updateCustomisationVisualState(), true);
customisationPanel.ExpandedState.BindValueChanged(_ => updateCustomisationVisualState(), true);
SearchTextBox.Current.BindValueChanged(query =>
{
@ -368,18 +368,18 @@ namespace osu.Game.Overlays.Mods
customisationPanel.Enabled.Value = true;
if (anyModPendingConfiguration)
customisationPanel.Expanded.Value = true;
customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Expanded;
}
else
{
customisationPanel.Expanded.Value = false;
customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Collapsed;
customisationPanel.Enabled.Value = false;
}
}
private void updateCustomisationVisualState()
{
if (customisationPanel.Expanded.Value)
if (customisationPanel.ExpandedState.Value != ModCustomisationPanel.ModCustomisationPanelState.Collapsed)
{
columnScroll.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;
}
customisationPanel.Expanded.Value = false;
customisationPanel.ExpandedState.Value = ModCustomisationPanel.ModCustomisationPanelState.Collapsed;
}
#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.
case GlobalAction.DeselectAllMods:
{
if (!SearchTextBox.HasFocus && !customisationPanel.Expanded.Value)
if (!SearchTextBox.HasFocus && customisationPanel.ExpandedState.Value == ModCustomisationPanel.ModCustomisationPanelState.Collapsed)
{
DisplayedFooterContent?.DeselectAllModsButton?.TriggerClick();
return true;
@ -637,7 +637,7 @@ namespace osu.Game.Overlays.Mods
if (e.Repeat || e.Key != Key.Tab)
return false;
if (customisationPanel.Expanded.Value)
if (customisationPanel.ExpandedState.Value != ModCustomisationPanel.ModCustomisationPanelState.Collapsed)
return true;
// TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`)
@ -668,6 +668,8 @@ namespace osu.Game.Overlays.Mods
[Cached]
internal partial class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public ColumnScrollContainer()
: 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),
Children = new Drawable[]
{
new FillFlowContainer
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(20),
Children = new Drawable[]
ColumnDimensions = new[]
{
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,
},
detailCountryRank = new ProfileValueDisplay(true)
{
Title = UsersStrings.ShowRankCountrySimple,
},
detailGlobalRank = new ProfileValueDisplay(true)
{
Title = UsersStrings.ShowRankGlobalSimple,
},
Empty(),
detailCountryRank = new ProfileValueDisplay(true)
{
Title = UsersStrings.ShowRankCountrySimple,
},
new DailyChallengeStatsDisplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
User = { BindTarget = User },
}
}
}
},
new Container

View File

@ -2,7 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
@ -10,6 +14,8 @@ namespace osu.Game.Overlays.Settings
public partial class SettingsEnumDropdown<T> : SettingsDropdown<T>
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 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.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Volume
{
@ -33,18 +33,18 @@ namespace osu.Game.Overlays.Volume
}
}
private Color4 hoveredColour, unhoveredColour;
private const float width = 100;
public const float HEIGHT = 35;
private ColourInfo hoveredBorderColour;
private ColourInfo unhoveredBorderColour;
private CompositeDrawable border = null!;
public MuteButton()
{
Content.BorderThickness = 3;
Content.CornerRadius = HEIGHT / 2;
Content.CornerExponent = 2;
const float width = 30;
const float height = 30;
Size = new Vector2(width, HEIGHT);
Size = new Vector2(width, height);
Content.CornerRadius = height / 2;
Content.CornerExponent = 2;
Action = () => Current.Value = !Current.Value;
}
@ -52,10 +52,9 @@ namespace osu.Game.Overlays.Volume
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
hoveredColour = colours.YellowDark;
Content.BorderColour = unhoveredColour = colours.Gray1;
BackgroundColour = colours.Gray1;
hoveredBorderColour = colours.PinkLight;
unhoveredBorderColour = colours.Gray1;
SpriteIcon icon;
@ -65,26 +64,39 @@ namespace osu.Game.Overlays.Volume
{
Anchor = 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 =>
{
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 };
}, true);
}
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;
}
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)

View File

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

View File

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

View File

@ -209,9 +209,7 @@ namespace osu.Game.Rulesets.Edit
case MouseButtonEvent mouse:
// 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
// while in placement mode.
return mouse.Button == MouseButton.Left || !mouse.ShiftPressed;
return mouse.Button == MouseButton.Left || PlacementActive == PlacementState.Active;
default:
return false;

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
protected readonly double Offset;
/// <summary>
/// The beatmap version.
/// The .osu format (beatmap) version.
/// </summary>
protected readonly int FormatVersion;
@ -48,7 +48,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
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;

View File

@ -17,13 +17,12 @@ namespace osu.Game.Screens
private const float x_movement_amount = 50;
private readonly bool animateOnEnter;
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;
Origin = Anchor.Centre;
}
@ -53,12 +52,11 @@ namespace osu.Game.Screens
public override void OnEntering(ScreenTransitionEvent e)
{
if (animateOnEnter)
if (AnimateEntry)
{
this.FadeOut();
this.MoveToX(x_movement_amount);
this.FadeIn(TRANSITION_LENGTH, Easing.InOutQuart);
this.MoveToX(x_movement_amount);
this.MoveToX(0, TRANSITION_LENGTH, Easing.InOutQuart);
}

View File

@ -27,10 +27,14 @@ namespace osu.Game.Screens
if (screen == null)
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;
base.Push(screen);
return true;
}
}

View File

@ -42,11 +42,6 @@ namespace osu.Game.Screens.Backgrounds
protected virtual bool AllowStoryboardBackground => true;
public BackgroundScreenDefault(bool animateOnEnter = true)
: base(animateOnEnter)
{
}
[BackgroundDependencyLoader]
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Threading;
@ -16,6 +17,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.LoadComplete();
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, _) => updateInspectorText();
EditorBeatmap.PlacementObject.BindValueChanged(_ => updateInspectorText());
EditorBeatmap.TransactionBegan += updateInspectorText;
EditorBeatmap.TransactionEnded += updateInspectorText;
updateInspectorText();
@ -29,24 +31,33 @@ namespace osu.Game.Screens.Edit.Compose.Components
rollingTextUpdate?.Cancel();
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.
// This is a good middle-ground for the time being.
if (EditorBeatmap.SelectedHitObjects.Count > 0)
if (objects.Length > 0)
rollingTextUpdate ??= Scheduler.AddDelayed(updateInspectorText, 250);
}
protected virtual void AddInspectorValues()
protected virtual void AddInspectorValues(HitObject[] objects)
{
switch (EditorBeatmap.SelectedHitObjects.Count)
switch (objects.Length)
{
case 0:
AddValue("No selection");
break;
case 1:
var selected = EditorBeatmap.SelectedHitObjects.Single();
var selected = objects.Single();
AddHeader("Type");
AddValue($"{selected.GetType().ReadableName()}");
@ -105,13 +116,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
default:
AddHeader("Selected Objects");
AddValue($"{EditorBeatmap.SelectedHitObjects.Count:#,0.##}");
AddValue($"{objects.Length:#,0.##}");
AddHeader("Start Time");
AddValue($"{EditorBeatmap.SelectedHitObjects.Min(o => o.StartTime):#,0.##}ms");
AddValue($"{objects.Min(o => o.StartTime):#,0.##}ms");
AddHeader("End Time");
AddValue($"{EditorBeatmap.SelectedHitObjects.Max(o => o.GetEndTime()):#,0.##}ms");
AddValue($"{objects.Max(o => o.GetEndTime()):#,0.##}ms");
break;
}
}

View File

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

View File

@ -69,19 +69,24 @@ namespace osu.Game.Screens.Edit.Compose
if (ruleset == null || composer == null)
return base.CreateTimelineContent();
TimelineBreakDisplay breakDisplay = new TimelineBreakDisplay
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 0.75f,
};
return wrapSkinnableContent(new Container
{
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 TimelineBreakDisplay
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 0.75f,
},
breakDisplay
}
});
}

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework;
@ -159,7 +160,7 @@ namespace osu.Game.Screens.Edit
private string lastSavedHash;
private Container<EditorScreen> screenContainer;
private ScreenContainer screenContainer;
[CanBeNull]
private readonly EditorLoader loader;
@ -329,7 +330,7 @@ namespace osu.Game.Screens.Edit
Name = "Screen container",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 40, Bottom = 50 },
Child = screenContainer = new Container<EditorScreen>
Child = screenContainer = new ScreenContainer
{
RelativeSizeAxes = Axes.Both,
}
@ -422,6 +423,7 @@ namespace osu.Game.Screens.Edit
MutationTracker,
}
});
changeHandler?.CanUndo.BindValueChanged(v => undoMenuItem.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");
}
LoadComponentAsync(currentScreen, newScreen =>
screenContainer.LoadComponentAsync(currentScreen, newScreen =>
{
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.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);
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();
info.BindValueChanged(_ => dailyChallengeChanged(postNotification: true));
dailyChallengeChanged(postNotification: false);
info.BindValueChanged(dailyChallengeChanged, true);
}
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();
@ -152,8 +153,14 @@ namespace osu.Game.Screens.Menu
Room = room;
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));
}
updateCountdown();
Scheduler.AddDelayed(updateCountdown, 1000, true);

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -44,7 +45,7 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
[Cached(typeof(IPreviewTrackOwner))]
public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner
public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap
{
private readonly Room room;
private readonly PlaylistItem playlistItem;
@ -76,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
[Cached]
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
[Resolved]
private OsuGame? game { get; set; }
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
@ -386,7 +390,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
base.LoadComplete();
beatmapAvailabilityTracker.SelectedItem.Value = playlistItem;
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true);
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem), true);
userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay);
userModsSelectOverlay.SelectedItem.Value = playlistItem;
@ -398,15 +402,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
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(() =>
{
if (state.NewValue != APIState.Online)
@ -440,7 +435,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
waves.Show();
roomManager.JoinRoom(room);
applyLoopingToTrack();
startLoopingTrack(this, musicController);
metadataClient.BeginWatchingMultiplayerRoom(room.RoomID.Value!.Value).ContinueWith(t =>
{
@ -452,6 +447,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
MultiplayerPlaylistItemStats[] stats = t.GetResultSafely();
var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID);
if (itemStats == null) return;
Schedule(() =>
@ -459,17 +455,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
breakdown.SetInitialCounts(itemStats.TotalScoreDistribution);
totals.SetInitialCounts(itemStats.TotalScoreDistribution.Sum(c => c), itemStats.CumulativeScore);
});
});
}, TaskContinuationOptions.OnlyOnRanToCompletion);
beatmapAvailabilityTracker.SelectedItem.Value = playlistItem;
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => trySetDailyChallengeBeatmap(), true);
userModsSelectOverlay.SelectedItem.Value = playlistItem;
TrySetDailyChallengeBeatmap(this, beatmapManager, rulesets, musicController, playlistItem);
}
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
applyLoopingToTrack();
startLoopingTrack(this, musicController);
// re-apply mods as they may have been changed by a child screen
// (one known instance of this is showing a replay).
updateMods();
@ -498,17 +494,30 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
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;
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)
{
Beatmap.Value?.PrepareTrackForPreview(true);
musicController.EnsurePlayingSomething();
screen.Beatmap.Value?.PrepareTrackForPreview(true);
music.EnsurePlayingSomething();
}
}
@ -546,5 +555,21 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
if (metadataClient.IsNotNull())
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.Events;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.Play;
@ -41,7 +42,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
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.

View File

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

View File

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

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