mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 06:03:08 +08:00
Merge branch 'master' into gameplay-hud-redesign/counters
This commit is contained in:
commit
1d4f4cf4c3
@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1012.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1030.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 5.0 KiB |
@ -4,6 +4,7 @@ Version: 2.5
|
||||
[Mania]
|
||||
Keys: 4
|
||||
ColumnLineWidth: 3,1,3,1,1
|
||||
LightFramePerSecond: 15
|
||||
// some skins found in the wild had configuration keys where the @2x suffix was included in the values.
|
||||
// the expected compatibility behaviour is that the presence of the @2x suffix shouldn't change anything
|
||||
// if @2x assets are present.
|
||||
@ -15,5 +16,6 @@ Hit300: mania/hit300@2x
|
||||
Hit300g: mania/hit300g@2x
|
||||
StageLeft: mania/stage-left
|
||||
StageRight: mania/stage-right
|
||||
StageLight: mania/stage-light
|
||||
NoteImage0L: LongNoteTailWang
|
||||
NoteImage1L: LongNoteTailWang
|
||||
|
@ -255,16 +255,6 @@ namespace osu.Game.Rulesets.Mania
|
||||
case ModType.Conversion:
|
||||
return new Mod[]
|
||||
{
|
||||
new MultiMod(new ManiaModKey4(),
|
||||
new ManiaModKey5(),
|
||||
new ManiaModKey6(),
|
||||
new ManiaModKey7(),
|
||||
new ManiaModKey8(),
|
||||
new ManiaModKey9(),
|
||||
new ManiaModKey10(),
|
||||
new ManiaModKey1(),
|
||||
new ManiaModKey2(),
|
||||
new ManiaModKey3()),
|
||||
new ManiaModRandom(),
|
||||
new ManiaModDualStages(),
|
||||
new ManiaModMirror(),
|
||||
@ -272,7 +262,19 @@ namespace osu.Game.Rulesets.Mania
|
||||
new ManiaModClassic(),
|
||||
new ManiaModInvert(),
|
||||
new ManiaModConstantSpeed(),
|
||||
new ManiaModHoldOff()
|
||||
new ManiaModHoldOff(),
|
||||
new MultiMod(
|
||||
new ManiaModKey1(),
|
||||
new ManiaModKey2(),
|
||||
new ManiaModKey3(),
|
||||
new ManiaModKey4(),
|
||||
new ManiaModKey5(),
|
||||
new ManiaModKey6(),
|
||||
new ManiaModKey7(),
|
||||
new ManiaModKey8(),
|
||||
new ManiaModKey9(),
|
||||
new ManiaModKey10()
|
||||
),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -99,9 +100,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(30));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnWidth:
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(
|
||||
stage.IsSpecialColumn(columnIndex) ? 120 : 60
|
||||
));
|
||||
|
||||
float width;
|
||||
|
||||
bool isSpecialColumn = stage.IsSpecialColumn(columnIndex);
|
||||
|
||||
// Best effort until we have better mobile support.
|
||||
if (RuntimeInfo.IsMobile)
|
||||
width = 170 * Math.Min(1, 7f / beatmap.TotalColumns) * (isSpecialColumn ? 1.8f : 1);
|
||||
else
|
||||
width = 60 * (isSpecialColumn ? 2 : 1);
|
||||
|
||||
return SkinUtils.As<TValue>(new Bindable<float>(width));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour:
|
||||
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
isHitting.BindTo(holdNote.IsHitting);
|
||||
|
||||
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true).With(d =>
|
||||
bodySprite = skin.GetAnimation(imageName, wrapMode, wrapMode, true, true, frameLength: 30).With(d =>
|
||||
{
|
||||
if (d == null)
|
||||
return;
|
||||
|
@ -5,7 +5,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
@ -20,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
private Container lightContainer = null!;
|
||||
private Sprite light = null!;
|
||||
private Drawable light = null!;
|
||||
|
||||
public LegacyColumnBackground()
|
||||
{
|
||||
@ -39,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
Color4 lightColour = GetColumnSkinConfig<Color4>(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value
|
||||
?? Color4.White;
|
||||
|
||||
int lightFramePerSecond = skin.GetManiaSkinConfig<int>(LegacyManiaSkinConfigurationLookups.LightFramePerSecond)?.Value ?? 60;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
lightContainer = new Container
|
||||
@ -46,16 +47,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Bottom = lightPosition },
|
||||
Child = light = new Sprite
|
||||
Child = light = skin.GetAnimation(lightImage, true, true, frameLength: 1000d / lightFramePerSecond)?.With(l =>
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour),
|
||||
Texture = skin.GetTexture(lightImage),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 1,
|
||||
Alpha = 0
|
||||
}
|
||||
l.Anchor = Anchor.BottomCentre;
|
||||
l.Origin = Anchor.BottomCentre;
|
||||
l.Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour);
|
||||
l.RelativeSizeAxes = Axes.X;
|
||||
l.Width = 1;
|
||||
l.Alpha = 0;
|
||||
}) ?? Empty(),
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
string filename = this.GetManiaSkinConfig<string>(hit_result_mapping[result])?.Value
|
||||
?? default_hit_result_skin_filenames[result];
|
||||
|
||||
var animation = this.GetAnimation(filename, true, true);
|
||||
var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d);
|
||||
return animation == null ? null : new LegacyManiaJudgementPiece(result, animation);
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Input;
|
||||
@ -24,15 +21,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSliderVelocityAdjust : OsuGameTestScene
|
||||
{
|
||||
private Screens.Edit.Editor editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor;
|
||||
private Screens.Edit.Editor? editor => Game.ScreenStack.CurrentScreen as Screens.Edit.Editor;
|
||||
|
||||
private EditorBeatmap editorBeatmap => editor.ChildrenOfType<EditorBeatmap>().FirstOrDefault();
|
||||
private EditorBeatmap editorBeatmap => editor.ChildrenOfType<EditorBeatmap>().FirstOrDefault()!;
|
||||
|
||||
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().FirstOrDefault();
|
||||
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().FirstOrDefault()!;
|
||||
|
||||
private Slider slider => editorBeatmap.HitObjects.OfType<Slider>().FirstOrDefault();
|
||||
private Slider? slider => editorBeatmap.HitObjects.OfType<Slider>().FirstOrDefault();
|
||||
|
||||
private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType<TimelineHitObjectBlueprint>().FirstOrDefault();
|
||||
private TimelineHitObjectBlueprint blueprint => editor.ChildrenOfType<TimelineHitObjectBlueprint>().FirstOrDefault()!;
|
||||
|
||||
private DifficultyPointPiece difficultyPointPiece => blueprint.ChildrenOfType<DifficultyPointPiece>().First();
|
||||
|
||||
@ -46,6 +43,55 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
double? velocity = null;
|
||||
|
||||
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||
AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True);
|
||||
|
||||
AddStep("seek to first control point", () => editorClock.Seek(editorBeatmap.ControlPointInfo.TimingPoints.First().Time));
|
||||
AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3));
|
||||
|
||||
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.Centre));
|
||||
AddStep("start placement", () => InputManager.Click(MouseButton.Left));
|
||||
|
||||
AddStep("move mouse to bottom right", () => InputManager.MoveMouseTo(editor.ChildrenOfType<Playfield>().First().ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
|
||||
AddStep("end placement", () => InputManager.Click(MouseButton.Right));
|
||||
|
||||
AddStep("exit placement mode", () => InputManager.Key(Key.Number1));
|
||||
|
||||
AddAssert("slider placed", () => slider, () => Is.Not.Null);
|
||||
|
||||
AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddAssert("ensure one slider placed", () => slider, () => Is.Not.Null);
|
||||
|
||||
AddStep("store velocity", () => velocity = slider!.Velocity);
|
||||
|
||||
if (adjustVelocity)
|
||||
{
|
||||
AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick());
|
||||
AddStep("change velocity", () => velocityTextBox.Current.Value = 2);
|
||||
|
||||
AddAssert("velocity adjusted", () => slider!.Velocity,
|
||||
() => Is.EqualTo(velocity!.Value * 2).Within(Precision.DOUBLE_EPSILON));
|
||||
|
||||
AddStep("store velocity", () => velocity = slider!.Velocity);
|
||||
}
|
||||
|
||||
AddStep("save", () => InputManager.Keys(PlatformAction.Save));
|
||||
AddStep("exit", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||
AddUntilStep("wait for editor load", () => editor?.ReadyForUse, () => Is.True);
|
||||
|
||||
AddStep("seek to slider", () => editorClock.Seek(slider!.StartTime));
|
||||
AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocity));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVelocityUndo()
|
||||
{
|
||||
double? velocityBefore = null;
|
||||
double? durationBefore = null;
|
||||
|
||||
AddStep("enter editor", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||
AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
|
||||
|
||||
@ -60,36 +106,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
|
||||
AddStep("exit placement mode", () => InputManager.Key(Key.Number1));
|
||||
|
||||
AddAssert("slider placed", () => slider != null);
|
||||
|
||||
AddAssert("slider placed", () => slider, () => Is.Not.Null);
|
||||
AddStep("select slider", () => editorBeatmap.SelectedHitObjects.Add(slider));
|
||||
|
||||
AddAssert("ensure one slider placed", () => slider != null);
|
||||
|
||||
AddStep("store velocity", () => velocity = slider.Velocity);
|
||||
|
||||
if (adjustVelocity)
|
||||
AddStep("store velocity", () =>
|
||||
{
|
||||
AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick());
|
||||
AddStep("change velocity", () => velocityTextBox.Current.Value = 2);
|
||||
velocityBefore = slider!.Velocity;
|
||||
durationBefore = slider.Duration;
|
||||
});
|
||||
|
||||
AddAssert("velocity adjusted", () =>
|
||||
{
|
||||
Debug.Assert(velocity != null);
|
||||
return Precision.AlmostEquals(velocity.Value * 2, slider.Velocity);
|
||||
});
|
||||
AddStep("open velocity adjust panel", () => difficultyPointPiece.TriggerClick());
|
||||
AddStep("change velocity", () => velocityTextBox.Current.Value = 2);
|
||||
|
||||
AddStep("store velocity", () => velocity = slider.Velocity);
|
||||
}
|
||||
AddAssert("velocity adjusted", () => slider!.Velocity, () => Is.EqualTo(velocityBefore!.Value * 2).Within(Precision.DOUBLE_EPSILON));
|
||||
|
||||
AddStep("save", () => InputManager.Keys(PlatformAction.Save));
|
||||
AddStep("exit", () => InputManager.Key(Key.Escape));
|
||||
AddStep("undo", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.Z);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
|
||||
AddStep("enter editor (again)", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||
AddUntilStep("wait for editor load", () => editor?.ReadyForUse == true);
|
||||
|
||||
AddStep("seek to slider", () => editorClock.Seek(slider.StartTime));
|
||||
AddAssert("slider has correct velocity", () => slider.Velocity == velocity);
|
||||
AddAssert("slider has correct velocity", () => slider!.Velocity, () => Is.EqualTo(velocityBefore));
|
||||
AddAssert("slider has correct duration", () => slider!.Duration, () => Is.EqualTo(durationBefore));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
|
||||
};
|
||||
|
||||
var sprites = this.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray();
|
||||
var sprites = this.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray();
|
||||
Debug.Assert(sprites.Length <= 2);
|
||||
});
|
||||
|
||||
@ -103,8 +103,8 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
private partial class TestLegacyMainCirclePiece : LegacyMainCirclePiece
|
||||
{
|
||||
public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
|
||||
public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
|
||||
public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
|
||||
public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture?.AssetName)).DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
|
||||
|
||||
public TestLegacyMainCirclePiece(string? priorityLookupPrefix)
|
||||
: base(priorityLookupPrefix, false)
|
||||
|
@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@ -10,6 +11,8 @@ using osu.Framework.Timing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
@ -47,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
manualClock = null;
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
@ -102,6 +106,33 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
assertSpinnerHit(false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVibrateWithoutSpinningOnCentreWithDoubleTime()
|
||||
{
|
||||
List<ReplayFrame> frames = new List<ReplayFrame>();
|
||||
|
||||
const int rate = 2;
|
||||
// the track clock is going to be playing twice as fast,
|
||||
// so the vibration time in clock time needs to be twice as long
|
||||
// to keep constant speed in real time.
|
||||
const int vibrate_time = 50 * rate;
|
||||
|
||||
int direction = -1;
|
||||
|
||||
for (double i = time_spinner_start; i <= time_spinner_end; i += vibrate_time)
|
||||
{
|
||||
frames.Add(new OsuReplayFrame(i, new Vector2(centre_x + direction * 50, centre_y), OsuAction.LeftButton));
|
||||
frames.Add(new OsuReplayFrame(i + vibrate_time, new Vector2(centre_x - direction * 50, centre_y), OsuAction.LeftButton));
|
||||
|
||||
direction *= -1;
|
||||
}
|
||||
|
||||
AddStep("set DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = rate } } });
|
||||
performTest(frames);
|
||||
|
||||
assertSpinnerHit(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spins in a single direction.
|
||||
/// </summary>
|
||||
|
@ -2,11 +2,9 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -22,6 +20,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
@ -90,21 +89,18 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
break;
|
||||
|
||||
default:
|
||||
addBubble();
|
||||
BubbleDrawable bubble = bubblePool.Get();
|
||||
|
||||
bubble.WasHit = drawable.IsHit;
|
||||
bubble.Position = getPosition(drawableOsuHitObject);
|
||||
bubble.AccentColour = drawable.AccentColour.Value;
|
||||
bubble.InitialSize = new Vector2(bubbleSize);
|
||||
bubble.FadeTime = bubbleFade;
|
||||
bubble.MaxSize = maxSize;
|
||||
|
||||
bubbleContainer.Add(bubble);
|
||||
break;
|
||||
}
|
||||
|
||||
void addBubble()
|
||||
{
|
||||
BubbleDrawable bubble = bubblePool.Get();
|
||||
|
||||
bubble.DrawableOsuHitObject = drawableOsuHitObject;
|
||||
bubble.InitialSize = new Vector2(bubbleSize);
|
||||
bubble.FadeTime = bubbleFade;
|
||||
bubble.MaxSize = maxSize;
|
||||
|
||||
bubbleContainer.Add(bubble);
|
||||
}
|
||||
};
|
||||
|
||||
drawableObject.OnRevertResult += (drawable, _) =>
|
||||
@ -118,18 +114,38 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
};
|
||||
}
|
||||
|
||||
private Vector2 getPosition(DrawableOsuHitObject drawableObject)
|
||||
{
|
||||
switch (drawableObject)
|
||||
{
|
||||
// SliderHeads are derived from HitCircles,
|
||||
// so we must handle them before to avoid them using the wrong positioning logic
|
||||
case DrawableSliderHead:
|
||||
return drawableObject.HitObject.Position;
|
||||
|
||||
// Using hitobject position will cause issues with HitCircle placement due to stack leniency.
|
||||
case DrawableHitCircle:
|
||||
return drawableObject.Position;
|
||||
|
||||
default:
|
||||
return drawableObject.HitObject.Position;
|
||||
}
|
||||
}
|
||||
|
||||
#region Pooled Bubble drawable
|
||||
|
||||
private partial class BubbleDrawable : PoolableDrawable
|
||||
{
|
||||
public DrawableOsuHitObject? DrawableOsuHitObject { get; set; }
|
||||
|
||||
public Vector2 InitialSize { get; set; }
|
||||
|
||||
public float MaxSize { get; set; }
|
||||
|
||||
public double FadeTime { get; set; }
|
||||
|
||||
public bool WasHit { get; set; }
|
||||
|
||||
public Color4 AccentColour { get; set; }
|
||||
|
||||
private readonly Box colourBox;
|
||||
private readonly CircularContainer content;
|
||||
|
||||
@ -157,15 +173,12 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
protected override void PrepareForUse()
|
||||
{
|
||||
Debug.Assert(DrawableOsuHitObject.IsNotNull());
|
||||
|
||||
Colour = DrawableOsuHitObject.IsHit ? Colour4.White : Colour4.Black;
|
||||
Colour = WasHit ? Colour4.White : Colour4.Black;
|
||||
Scale = new Vector2(1);
|
||||
Position = getPosition(DrawableOsuHitObject);
|
||||
Size = InitialSize;
|
||||
|
||||
//We want to fade to a darker colour to avoid colours such as white hiding the "ripple" effect.
|
||||
ColourInfo colourDarker = DrawableOsuHitObject.AccentColour.Value.Darken(0.1f);
|
||||
ColourInfo colourDarker = AccentColour.Darken(0.1f);
|
||||
|
||||
// The absolute length of the bubble's animation, can be used in fractions for animations of partial length
|
||||
double duration = 1700 + Math.Pow(FadeTime, 1.07f);
|
||||
@ -178,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
.ScaleTo(MaxSize * 1.5f, duration * 0.2f, Easing.OutQuint)
|
||||
.FadeOut(duration * 0.2f, Easing.OutCirc).Expire();
|
||||
|
||||
if (!DrawableOsuHitObject.IsHit) return;
|
||||
if (!WasHit) return;
|
||||
|
||||
content.BorderThickness = InitialSize.X / 3.5f;
|
||||
content.BorderColour = Colour4.White;
|
||||
@ -192,24 +205,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
// Avoids transparency overlap issues during the bubble "pop"
|
||||
.TransformTo(nameof(BorderThickness), 0f);
|
||||
}
|
||||
|
||||
private Vector2 getPosition(DrawableOsuHitObject drawableObject)
|
||||
{
|
||||
switch (drawableObject)
|
||||
{
|
||||
// SliderHeads are derived from HitCircles,
|
||||
// so we must handle them before to avoid them using the wrong positioning logic
|
||||
case DrawableSliderHead:
|
||||
return drawableObject.HitObject.Position;
|
||||
|
||||
// Using hitobject position will cause issues with HitCircle placement due to stack leniency.
|
||||
case DrawableHitCircle:
|
||||
return drawableObject.Position;
|
||||
|
||||
default:
|
||||
return drawableObject.HitObject.Position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -41,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
switch (hitObject)
|
||||
{
|
||||
case Slider slider:
|
||||
slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value;
|
||||
|
||||
foreach (var head in slider.NestedHitObjects.OfType<SliderHeadCircle>())
|
||||
head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value;
|
||||
|
||||
slider.ClassicSliderBehaviour = NoSliderHeadAccuracy.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
/// </summary>
|
||||
public Container OverlayElementContainer { get; private set; }
|
||||
|
||||
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
|
||||
public override bool DisplayResult => HitObject.ClassicSliderBehaviour;
|
||||
|
||||
[CanBeNull]
|
||||
public PlaySliderBody SliderBody => Body.Drawable as PlaySliderBody;
|
||||
@ -272,30 +272,31 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
if (userTriggered || !TailCircle.Judged || Time.Current < HitObject.EndTime)
|
||||
return;
|
||||
|
||||
// If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
|
||||
// But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
|
||||
if (HitObject.OnlyJudgeNestedObjects)
|
||||
if (HitObject.ClassicSliderBehaviour)
|
||||
{
|
||||
ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
|
||||
ApplyResult(r =>
|
||||
{
|
||||
int totalTicks = NestedHitObjects.Count;
|
||||
int hitTicks = NestedHitObjects.Count(h => h.IsHit);
|
||||
|
||||
if (hitTicks == totalTicks)
|
||||
r.Type = HitResult.Great;
|
||||
else if (hitTicks == 0)
|
||||
r.Type = HitResult.Miss;
|
||||
else
|
||||
// Classic behaviour means a slider is judged proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
|
||||
ApplyResult(r =>
|
||||
{
|
||||
double hitFraction = (double)hitTicks / totalTicks;
|
||||
r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh;
|
||||
}
|
||||
});
|
||||
int totalTicks = NestedHitObjects.Count;
|
||||
int hitTicks = NestedHitObjects.Count(h => h.IsHit);
|
||||
|
||||
if (hitTicks == totalTicks)
|
||||
r.Type = HitResult.Great;
|
||||
else if (hitTicks == 0)
|
||||
r.Type = HitResult.Miss;
|
||||
else
|
||||
{
|
||||
double hitFraction = (double)hitTicks / totalTicks;
|
||||
r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh;
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
|
||||
// But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
|
||||
ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
|
||||
}
|
||||
}
|
||||
|
||||
public override void PlaySamples()
|
||||
|
@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||
|
||||
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
||||
public override bool DisplayResult
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HitObject?.ClassicSliderBehaviour == true)
|
||||
return false;
|
||||
|
||||
return base.DisplayResult;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IBindable<int> pathVersion = new Bindable<int>();
|
||||
|
||||
@ -56,12 +65,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
Debug.Assert(HitObject != null);
|
||||
|
||||
if (HitObject.JudgeAsNormalHitCircle)
|
||||
return base.ResultFor(timeOffset);
|
||||
if (HitObject.ClassicSliderBehaviour)
|
||||
{
|
||||
// With classic slider behaviour, heads are considered fully hit if in the largest hit window.
|
||||
// We can't award a full Great because the true Great judgement is awarded on the Slider itself,
|
||||
// reduced based on number of ticks hit,
|
||||
// so we use the most suitable LargeTick judgement here instead.
|
||||
return base.ResultFor(timeOffset).IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
|
||||
}
|
||||
|
||||
// If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring.
|
||||
var result = base.ResultFor(timeOffset);
|
||||
return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
|
||||
return base.ResultFor(timeOffset);
|
||||
}
|
||||
|
||||
public override void Shake()
|
||||
|
@ -275,6 +275,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
|
||||
if (spinningSample != null && spinnerFrequencyModulate)
|
||||
spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress;
|
||||
|
||||
// Ticks can theoretically be judged at any point in the spinner's duration.
|
||||
// A tick must be alive to correctly play back samples,
|
||||
// but for performance reasons, we only want to keep the next tick alive.
|
||||
var next = NestedHitObjects.FirstOrDefault(h => !h.Judged);
|
||||
|
||||
// See default `LifetimeStart` as set in `DrawableSpinnerTick`.
|
||||
if (next?.LifetimeStart == double.MaxValue)
|
||||
next.LifetimeStart = HitObject.StartTime;
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
|
@ -11,8 +11,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public override bool DisplayResult => false;
|
||||
|
||||
protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject;
|
||||
|
||||
public DrawableSpinnerTick()
|
||||
: this(null)
|
||||
{
|
||||
@ -29,10 +27,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
base.OnApply();
|
||||
|
||||
// the tick can be theoretically judged at any point in the spinner's duration,
|
||||
// so it must be alive throughout the spinner's entire lifetime.
|
||||
// this mostly matters for correct sample playback.
|
||||
LifetimeStart = DrawableSpinner.HitObject.StartTime;
|
||||
// Lifetime will be managed by `DrawableSpinner`.
|
||||
LifetimeStart = double.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -49,13 +49,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set
|
||||
{
|
||||
path.ControlPoints.Clear();
|
||||
path.ExpectedDistance.Value = null;
|
||||
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
path.ControlPoints.AddRange(value.ControlPoints.Select(c => new PathControlPoint(c.Position, c.Type)));
|
||||
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
|
||||
}
|
||||
path.ExpectedDistance.Value = value.ExpectedDistance.Value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,10 +124,21 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public double TickDistanceMultiplier = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="Slider"/>'s judgement is fully handled by its nested <see cref="HitObject"/>s.
|
||||
/// If <c>false</c>, this <see cref="Slider"/> will be judged proportionally to the number of nested <see cref="HitObject"/>s hit.
|
||||
/// If <see langword="false"/>, <see cref="Slider"/>'s judgement is fully handled by its nested <see cref="HitObject"/>s.
|
||||
/// If <see langword="true"/>, this <see cref="Slider"/> will be judged proportionally to the number of nested <see cref="HitObject"/>s hit.
|
||||
/// </summary>
|
||||
public bool OnlyJudgeNestedObjects = true;
|
||||
public bool ClassicSliderBehaviour
|
||||
{
|
||||
get => classicSliderBehaviour;
|
||||
set
|
||||
{
|
||||
classicSliderBehaviour = value;
|
||||
if (HeadCircle != null)
|
||||
HeadCircle.ClassicSliderBehaviour = value;
|
||||
}
|
||||
}
|
||||
|
||||
private bool classicSliderBehaviour;
|
||||
|
||||
public BindableNumber<double> SliderVelocityMultiplierBindable { get; } = new BindableDouble(1)
|
||||
{
|
||||
@ -191,7 +198,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
StartTime = e.Time,
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
StackHeight = StackHeight,
|
||||
Scale = Scale,
|
||||
});
|
||||
break;
|
||||
|
||||
@ -201,6 +207,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
StartTime = e.Time,
|
||||
Position = Position,
|
||||
StackHeight = StackHeight,
|
||||
ClassicSliderBehaviour = ClassicSliderBehaviour,
|
||||
});
|
||||
break;
|
||||
|
||||
@ -210,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
RepeatIndex = e.SpanIndex,
|
||||
StartTime = e.Time,
|
||||
Position = EndPosition,
|
||||
StackHeight = StackHeight
|
||||
StackHeight = StackHeight,
|
||||
});
|
||||
break;
|
||||
|
||||
@ -221,7 +228,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
StackHeight = StackHeight,
|
||||
Scale = Scale,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -266,7 +272,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
TailSamples = this.GetNodeSamples(repeatCount + 1);
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
|
||||
public override Judgement CreateJudgement() => ClassicSliderBehaviour
|
||||
// See logic in `DrawableSlider.CheckForResult()`
|
||||
? new OsuJudgement()
|
||||
// Of note, this creates a combo discrepancy for non-classic-mod sliders (there is no combo increase for tail or slider judgement).
|
||||
: new OsuIgnoreJudgement();
|
||||
|
||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||
}
|
||||
|
@ -9,11 +9,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public class SliderHeadCircle : HitCircle
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to treat this <see cref="SliderHeadCircle"/> as a normal <see cref="HitCircle"/> for judgement purposes.
|
||||
/// If <c>false</c>, this <see cref="SliderHeadCircle"/> will be judged as a <see cref="SliderTick"/> instead.
|
||||
/// If <see langword="false"/>, treat this <see cref="SliderHeadCircle"/> as a normal <see cref="HitCircle"/> for judgement purposes.
|
||||
/// If <see langword="true"/>, this <see cref="SliderHeadCircle"/> will be judged as a <see cref="SliderTick"/> instead.
|
||||
/// </summary>
|
||||
public bool JudgeAsNormalHitCircle = true;
|
||||
public bool ClassicSliderBehaviour;
|
||||
|
||||
public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement();
|
||||
public override Judgement CreateJudgement() => ClassicSliderBehaviour ? new SliderTickJudgement() : base.CreateJudgement();
|
||||
}
|
||||
}
|
||||
|
@ -101,11 +101,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
rotationTransferred = true;
|
||||
}
|
||||
|
||||
Debug.Assert(Math.Abs(delta) <= 180);
|
||||
|
||||
double rate = gameplayClock?.GetTrueGameplayRate() ?? Clock.Rate;
|
||||
delta = (float)(delta * Math.Abs(rate));
|
||||
|
||||
Debug.Assert(Math.Abs(delta) <= 180);
|
||||
|
||||
currentRotation += delta;
|
||||
drawableSpinner.Result.History.ReportDelta(Time.Current, delta);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
@ -62,12 +63,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
// otherwise fall back to the default prefix "hitcircle".
|
||||
string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle";
|
||||
|
||||
Vector2 maxSize = OsuHitObject.OBJECT_DIMENSIONS * 2;
|
||||
|
||||
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
|
||||
// the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
|
||||
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
||||
InternalChildren = new[]
|
||||
{
|
||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2) })
|
||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -76,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2))
|
||||
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
@ -41,11 +42,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
|
||||
|
||||
InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true, maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2) ?? Empty()).With(d =>
|
||||
InternalChild = arrow = new Sprite
|
||||
{
|
||||
d.Anchor = Anchor.Centre;
|
||||
d.Origin = Anchor.Centre;
|
||||
});
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Texture = skin?.GetTexture(lookupName)?.WithMaximumSize(maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2),
|
||||
};
|
||||
|
||||
textureIsDefaultSkin = skin is ISkinTransformer transformer && transformer.Skin is DefaultLegacySkin;
|
||||
|
||||
|
@ -48,40 +48,45 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider)
|
||||
{
|
||||
Drawable? getDrawableFor(string lookup)
|
||||
Drawable? getDrawableFor(string lookup, bool animatable)
|
||||
{
|
||||
const string normal_hit = "taikohit";
|
||||
const string big_hit = "taikobig";
|
||||
|
||||
string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit;
|
||||
|
||||
return skin.GetAnimation($"{prefix}{lookup}", true, false, maxSize: max_circle_sprite_size) ??
|
||||
return skin.GetAnimation($"{prefix}{lookup}", animatable, false, maxSize: max_circle_sprite_size) ??
|
||||
// fallback to regular size if "big" version doesn't exist.
|
||||
skin.GetAnimation($"{normal_hit}{lookup}", true, false, maxSize: max_circle_sprite_size);
|
||||
skin.GetAnimation($"{normal_hit}{lookup}", animatable, false, maxSize: max_circle_sprite_size);
|
||||
}
|
||||
|
||||
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
|
||||
AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle")));
|
||||
AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle", false))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
|
||||
foregroundLayer = getDrawableFor("circleoverlay", true);
|
||||
|
||||
foregroundLayer = getDrawableFor("circleoverlay");
|
||||
if (foregroundLayer != null)
|
||||
{
|
||||
foregroundLayer.Anchor = Anchor.Centre;
|
||||
foregroundLayer.Origin = Anchor.Centre;
|
||||
|
||||
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
||||
// For now just stop at first frame for sanity.
|
||||
if (foregroundLayer is IFramedAnimation animatedForegroundLayer)
|
||||
animatedForegroundLayer.Stop();
|
||||
|
||||
AddInternal(foregroundLayer);
|
||||
}
|
||||
|
||||
drawableHitObject.StartTimeBindable.BindValueChanged(startTime =>
|
||||
{
|
||||
timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT;
|
||||
}, true);
|
||||
|
||||
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
|
||||
// For now just stop at first frame for sanity.
|
||||
foreach (var c in InternalChildren)
|
||||
{
|
||||
(c as IFramedAnimation)?.Stop();
|
||||
|
||||
c.Anchor = Anchor.Centre;
|
||||
c.Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
if (gameplayState != null)
|
||||
currentCombo.BindTo(gameplayState.ScoreProcessor.Combo);
|
||||
}
|
||||
@ -101,11 +106,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
foreach (var c in InternalChildren)
|
||||
c.Scale = new Vector2(DrawHeight / circle_piece_size.Y);
|
||||
|
||||
if (foregroundLayer is IFramedAnimation animatableForegroundLayer)
|
||||
animateForegroundLayer(animatableForegroundLayer);
|
||||
if (foregroundLayer is IFramedAnimation animatedForegroundLayer)
|
||||
animateForegroundLayer(animatedForegroundLayer);
|
||||
}
|
||||
|
||||
private void animateForegroundLayer(IFramedAnimation animatableForegroundLayer)
|
||||
private void animateForegroundLayer(IFramedAnimation animation)
|
||||
{
|
||||
int multiplier;
|
||||
|
||||
@ -119,12 +124,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
}
|
||||
else
|
||||
{
|
||||
animatableForegroundLayer.GotoFrame(0);
|
||||
animation.GotoFrame(0);
|
||||
return;
|
||||
}
|
||||
|
||||
animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1;
|
||||
animatableForegroundLayer.GotoFrame(animationFrame);
|
||||
animation.GotoFrame(animationFrame);
|
||||
}
|
||||
|
||||
private Color4 accentColour;
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests
|
||||
@ -46,12 +47,15 @@ namespace osu.Game.Tests
|
||||
public partial class TestOsuGameBase : OsuGameBase
|
||||
{
|
||||
public RealmAccess Realm => Dependencies.Get<RealmAccess>();
|
||||
public new IAPIProvider API => base.API;
|
||||
|
||||
private readonly bool withBeatmap;
|
||||
|
||||
public TestOsuGameBase(bool withBeatmap)
|
||||
{
|
||||
this.withBeatmap = withBeatmap;
|
||||
|
||||
base.API = new DummyAPIAccess();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -11,7 +11,10 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -67,6 +70,116 @@ namespace osu.Game.Tests.Scores.IO
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void TestLastPlayedUpdate(bool isLocalUser)
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
if (!isLocalUser)
|
||||
osu.API.Logout();
|
||||
|
||||
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
|
||||
var beatmapInfo = beatmap.Beatmaps.First();
|
||||
|
||||
DateTimeOffset replayDate = DateTimeOffset.Now;
|
||||
|
||||
var toImport = new ScoreInfo
|
||||
{
|
||||
Rank = ScoreRank.B,
|
||||
TotalScore = 987654,
|
||||
Accuracy = 0.8,
|
||||
MaxCombo = 500,
|
||||
Combo = 250,
|
||||
User = new APIUser
|
||||
{
|
||||
Username = "Test user",
|
||||
Id = DummyAPIAccess.DUMMY_USER_ID,
|
||||
},
|
||||
Date = replayDate,
|
||||
OnlineID = 12345,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
BeatmapInfo = beatmapInfo
|
||||
};
|
||||
|
||||
var imported = LoadScoreIntoOsu(osu, toImport);
|
||||
|
||||
Assert.AreEqual(toImport.Rank, imported.Rank);
|
||||
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
|
||||
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
|
||||
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
|
||||
Assert.AreEqual(toImport.User.Username, imported.User.Username);
|
||||
Assert.AreEqual(toImport.Date, imported.Date);
|
||||
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
|
||||
|
||||
if (isLocalUser)
|
||||
Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(replayDate));
|
||||
else
|
||||
Assert.That(imported.BeatmapInfo!.LastPlayed, Is.Null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLastPlayedNotUpdatedDueToNewerPlays()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = LoadOsuIntoHost(host, true);
|
||||
|
||||
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
|
||||
var beatmapInfo = beatmap.Beatmaps.First();
|
||||
|
||||
var realmAccess = osu.Dependencies.Get<RealmAccess>();
|
||||
realmAccess.Write(r => r.Find<BeatmapInfo>(beatmapInfo.ID)!.LastPlayed = new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero));
|
||||
|
||||
var toImport = new ScoreInfo
|
||||
{
|
||||
Rank = ScoreRank.B,
|
||||
TotalScore = 987654,
|
||||
Accuracy = 0.8,
|
||||
MaxCombo = 500,
|
||||
Combo = 250,
|
||||
User = new APIUser
|
||||
{
|
||||
Username = "Test user",
|
||||
Id = DummyAPIAccess.DUMMY_USER_ID,
|
||||
},
|
||||
Date = new DateTimeOffset(2023, 10, 27, 0, 0, 0, TimeSpan.Zero),
|
||||
OnlineID = 12345,
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
BeatmapInfo = beatmapInfo
|
||||
};
|
||||
|
||||
var imported = LoadScoreIntoOsu(osu, toImport);
|
||||
|
||||
Assert.AreEqual(toImport.Rank, imported.Rank);
|
||||
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
|
||||
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
|
||||
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
|
||||
Assert.AreEqual(toImport.User.Username, imported.User.Username);
|
||||
Assert.AreEqual(toImport.Date, imported.Date);
|
||||
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
|
||||
|
||||
Assert.That(imported.BeatmapInfo!.LastPlayed, Is.EqualTo(new DateTimeOffset(2023, 10, 30, 0, 0, 0, TimeSpan.Zero)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestImportMods()
|
||||
{
|
||||
|
@ -7,8 +7,10 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -44,6 +46,47 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestContextMenuWithObjectBehind()
|
||||
{
|
||||
TimelineHitObjectBlueprint blueprint;
|
||||
|
||||
AddStep("add object", () =>
|
||||
{
|
||||
EditorBeatmap.Add(new HitCircle { StartTime = 3000 });
|
||||
});
|
||||
|
||||
AddStep("enter slider placement", () =>
|
||||
{
|
||||
InputManager.Key(Key.Number3);
|
||||
InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre);
|
||||
});
|
||||
|
||||
AddStep("start conflicting slider", () =>
|
||||
{
|
||||
InputManager.Click(MouseButton.Left);
|
||||
|
||||
blueprint = this.ChildrenOfType<TimelineHitObjectBlueprint>().First();
|
||||
InputManager.MoveMouseTo(blueprint.ScreenSpaceDrawQuad.TopLeft - new Vector2(10, 0));
|
||||
});
|
||||
|
||||
AddStep("end conflicting slider", () =>
|
||||
{
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
|
||||
AddStep("click object", () =>
|
||||
{
|
||||
InputManager.Key(Key.Number1);
|
||||
blueprint = this.ChildrenOfType<TimelineHitObjectBlueprint>().First();
|
||||
InputManager.MoveMouseTo(blueprint);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||
AddAssert("context menu open", () => this.ChildrenOfType<OsuContextMenu>().SingleOrDefault()?.State == MenuState.Open);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNudgeSelection()
|
||||
{
|
||||
@ -139,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddStep("click away", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Editor.ChildrenOfType<TimelineArea>().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One);
|
||||
InputManager.MoveMouseTo(Editor.ChildrenOfType<Timeline>().First().ScreenSpaceDrawQuad.TopLeft + new Vector2(5));
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
|
@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestPauseWithLargeOffset()
|
||||
{
|
||||
double lastTime;
|
||||
double lastStopTime;
|
||||
bool alwaysGoingForward = true;
|
||||
|
||||
AddStep("force large offset", () =>
|
||||
@ -84,20 +84,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddStep("add time forward check hook", () =>
|
||||
{
|
||||
lastTime = double.MinValue;
|
||||
lastStopTime = double.MinValue;
|
||||
alwaysGoingForward = true;
|
||||
|
||||
Player.OnUpdate += _ =>
|
||||
{
|
||||
double currentTime = Player.GameplayClockContainer.CurrentTime;
|
||||
bool goingForward = currentTime >= lastTime - 500;
|
||||
var masterClock = (MasterGameplayClockContainer)Player.GameplayClockContainer;
|
||||
|
||||
double currentTime = masterClock.CurrentTime;
|
||||
|
||||
bool goingForward = currentTime >= (masterClock.LastStopTime ?? lastStopTime);
|
||||
|
||||
alwaysGoingForward &= goingForward;
|
||||
|
||||
if (!goingForward)
|
||||
Logger.Log($"Backwards time occurred ({currentTime:N1} -> {lastTime:N1})");
|
||||
Logger.Log($"Went too far backwards (last stop: {lastStopTime:N1} current: {currentTime:N1})");
|
||||
|
||||
lastTime = currentTime;
|
||||
if (masterClock.LastStopTime != null)
|
||||
lastStopTime = masterClock.LastStopTime.Value;
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -214,10 +214,18 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
// Files starting with _ are temporary, created by CreateFileSafely call.
|
||||
AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null);
|
||||
AddAssert("filesize is non-zero", () =>
|
||||
AddUntilStep("filesize is non-zero", () =>
|
||||
{
|
||||
using (var stream = LocalStorage.GetStream(filePath))
|
||||
return stream.Length;
|
||||
try
|
||||
{
|
||||
using (var stream = LocalStorage.GetStream(filePath))
|
||||
return stream.Length;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// file move may still be in progress.
|
||||
return 0;
|
||||
}
|
||||
}, () => Is.Not.Zero);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
@ -27,6 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
/// <remarks>
|
||||
/// The HUD is hidden as it does't really affect game balance if HUD elements are larger than they should be.
|
||||
/// </remarks>
|
||||
[Ignore("This test is for visual testing, and has no value in being run in standard CI runs.")]
|
||||
public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers
|
||||
{
|
||||
// scale textures to 4 times their size.
|
||||
|
@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -7000, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -5000, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20));
|
||||
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 2000, volume: 20));
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
|
@ -693,7 +693,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
|
||||
[Test]
|
||||
[FlakyTest] // See above
|
||||
[Ignore("Failing too often, needs revisiting in some future.")]
|
||||
// This test is failing even after 10 retries (see https://github.com/ppy/osu/actions/runs/6700910613/job/18208272419)
|
||||
// Something is stopping the ready button from changing states, over multiple runs.
|
||||
public void TestGameplayExitFlow()
|
||||
{
|
||||
Bindable<double>? holdDelay = null;
|
||||
|
@ -175,7 +175,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
double scrollPosition = 0;
|
||||
|
||||
AddStep("set game volume to max", () => Game.Dependencies.Get<FrameworkConfigManager>().SetValue(FrameworkSetting.VolumeUniversal, 1d));
|
||||
AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType<VolumeOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
AddUntilStep("wait for volume overlay to hide", () => Game.ChildrenOfType<VolumeOverlay>().SingleOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
|
@ -5,8 +5,10 @@ using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Profile;
|
||||
@ -28,7 +30,14 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("create header", () => Child = header = new ProfileHeader());
|
||||
AddStep("create header", () =>
|
||||
{
|
||||
Child = new OsuScrollContainer(Direction.Vertical)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = header = new ProfileHeader()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -136,5 +145,260 @@ namespace osu.Game.Tests.Visual.Online
|
||||
PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" }
|
||||
}, new OsuRuleset().RulesetInfo));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManyTournamentBanners()
|
||||
{
|
||||
AddStep("Show user w/ many tournament banners", () => header.User.Value = new UserProfileData(new APIUser
|
||||
{
|
||||
Id = 728,
|
||||
Username = "Certain Guy",
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
|
||||
Statistics = new UserStatistics
|
||||
{
|
||||
IsRanked = false,
|
||||
// web will sometimes return non-empty rank history even for unranked users.
|
||||
RankHistory = new APIRankHistory
|
||||
{
|
||||
Mode = @"osu",
|
||||
Data = Enumerable.Range(2345, 85).ToArray()
|
||||
},
|
||||
},
|
||||
TournamentBanners = new[]
|
||||
{
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15329,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15588,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15589,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15590,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15591,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_JP@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15592,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RU@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15593,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KR@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15594,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NZ@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15595,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TH@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15596,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_TW@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15603,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ID@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15604,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_KZ@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15605,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AR@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15606,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_BR@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15607,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15639,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_MX@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15640,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_AU@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15641,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_IT@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15642,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_UA@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15643,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NL@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15644,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FI@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15645,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_RO@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15646,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SG@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15647,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_DE@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15648,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_ES@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15649,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_SE@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15650,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CA@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15651,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_NO@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15652,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_GB@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15653,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_US@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15654,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PL@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15655,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_FR@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15686,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_HK@2x.jpg"
|
||||
}
|
||||
}
|
||||
}, new OsuRuleset().RulesetInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,12 +121,29 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
|
||||
},
|
||||
},
|
||||
TournamentBanner = new TournamentBanner
|
||||
TournamentBanners = new[]
|
||||
{
|
||||
Id = 13926,
|
||||
TournamentId = 35,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US@2x.jpg",
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15588,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CN@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15589,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_PH@2x.jpg"
|
||||
},
|
||||
new TournamentBanner
|
||||
{
|
||||
Id = 15590,
|
||||
TournamentId = 41,
|
||||
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL.jpg",
|
||||
Image = "https://assets.ppy.sh/tournament-banners/official/owc2023/profile/supporter_CL@2x.jpg"
|
||||
}
|
||||
},
|
||||
Badges = new[]
|
||||
{
|
||||
|
@ -140,6 +140,17 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
AddUntilStep("top-level textbox focused", () => settings.SectionsContainer.ChildrenOfType<FocusedTextBox>().FirstOrDefault()?.HasFocus == true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSearchTextBoxSelectedOnShow()
|
||||
{
|
||||
SearchTextBox searchTextBox = null!;
|
||||
|
||||
AddStep("set text", () => (searchTextBox = settings.SectionsContainer.ChildrenOfType<SearchTextBox>().First()).Current.Value = "some text");
|
||||
AddAssert("no text selected", () => searchTextBox.SelectedText == string.Empty);
|
||||
AddRepeatStep("toggle visibility", () => settings.ToggleVisibility(), 2);
|
||||
AddAssert("search text selected", () => searchTextBox.SelectedText == searchTextBox.Current.Value);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
@ -1111,6 +1112,23 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddAssert("0 matching shown", () => songSelect.ChildrenOfType<FilterControl>().Single().InformationalText == "0 matches");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCutInFilterTextBox()
|
||||
{
|
||||
createSongSelect();
|
||||
|
||||
AddStep("set filter text", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text = "nonono");
|
||||
AddStep("select all", () => InputManager.Keys(PlatformAction.SelectAll));
|
||||
AddStep("press ctrl-x", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.Key(Key.X);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
});
|
||||
|
||||
AddAssert("filter text cleared", () => songSelect!.FilterControl.ChildrenOfType<SearchTextBox>().First().Text, () => Is.Empty);
|
||||
}
|
||||
|
||||
private void waitForInitialSelection()
|
||||
{
|
||||
AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault);
|
||||
|
@ -799,8 +799,11 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
|
||||
}
|
||||
|
||||
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
|
||||
() => modSelectOverlay.ChildrenOfType<ModColumn>().Any() && modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));
|
||||
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
|
||||
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
|
||||
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)
|
||||
&& modSelectOverlay.ChildrenOfType<ModPresetColumn>().Any()
|
||||
&& modSelectOverlay.ChildrenOfType<ModPresetColumn>().All(column => column.IsLoaded));
|
||||
|
||||
private void changeRuleset(int id)
|
||||
{
|
||||
|
@ -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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Tests.Visual;
|
||||
@ -14,13 +15,19 @@ namespace osu.Game.Tournament.Tests.Components
|
||||
{
|
||||
public partial class TestSceneDrawableTournamentTeam : OsuGridTestScene
|
||||
{
|
||||
[Cached]
|
||||
protected LadderInfo Ladder { get; private set; } = new LadderInfo();
|
||||
|
||||
public TestSceneDrawableTournamentTeam()
|
||||
: base(4, 3)
|
||||
{
|
||||
AddToggleStep("toggle seed view", v => Ladder.DisplayTeamSeeds.Value = v);
|
||||
|
||||
var team = new TournamentTeam
|
||||
{
|
||||
FlagName = { Value = "AU" },
|
||||
FullName = { Value = "Australia" },
|
||||
Seed = { Value = "#5" },
|
||||
Players =
|
||||
{
|
||||
new TournamentUser { Username = "ASecretBox" },
|
||||
@ -30,7 +37,7 @@ namespace osu.Game.Tournament.Tests.Components
|
||||
new TournamentUser { Username = "Parkes" },
|
||||
new TournamentUser { Username = "Shiroha" },
|
||||
new TournamentUser { Username = "Jordan The Bear" },
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var match = new TournamentMatch { Team1 = { Value = team } };
|
||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Tournament.Tests
|
||||
FlagName = { Value = "JP" },
|
||||
FullName = { Value = "Japan" },
|
||||
LastYearPlacing = { Value = 10 },
|
||||
Seed = { Value = "Low" },
|
||||
Seed = { Value = "#12" },
|
||||
SeedingResults =
|
||||
{
|
||||
new SeedingResult
|
||||
@ -140,6 +140,7 @@ namespace osu.Game.Tournament.Tests
|
||||
Acronym = { Value = "USA" },
|
||||
FlagName = { Value = "US" },
|
||||
FullName = { Value = "United States" },
|
||||
Seed = { Value = "#3" },
|
||||
Players =
|
||||
{
|
||||
new TournamentUser { Username = "Hello" },
|
||||
|
45
osu.Game.Tournament/Components/DrawableTeamSeed.cs
Normal file
45
osu.Game.Tournament/Components/DrawableTeamSeed.cs
Normal file
@ -0,0 +1,45 @@
|
||||
// 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.Game.Tournament.Models;
|
||||
|
||||
namespace osu.Game.Tournament.Components
|
||||
{
|
||||
public partial class DrawableTeamSeed : TournamentSpriteTextWithBackground
|
||||
{
|
||||
private readonly TournamentTeam? team;
|
||||
|
||||
private IBindable<string> seed = null!;
|
||||
private Bindable<bool> displaySeed = null!;
|
||||
|
||||
public DrawableTeamSeed(TournamentTeam? team)
|
||||
{
|
||||
this.team = team;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
private LadderInfo ladder { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Text.Font = Text.Font.With(size: 36);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (team == null)
|
||||
return;
|
||||
|
||||
seed = team.Seed.GetBoundCopy();
|
||||
seed.BindValueChanged(s => Text.Text = s.NewValue, true);
|
||||
|
||||
displaySeed = ladder.DisplayTeamSeeds.GetBoundCopy();
|
||||
displaySeed.BindValueChanged(v => Alpha = v.NewValue ? 1 : 0, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -18,11 +18,12 @@ namespace osu.Game.Tournament.Components
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 10),
|
||||
Spacing = new Vector2(0, 5),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DrawableTeamHeader(colour),
|
||||
new DrawableTeamTitle(team),
|
||||
new DrawableTeamSeed(team),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Components
|
||||
Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR,
|
||||
Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 50),
|
||||
Padding = new MarginPadding { Left = 10, Right = 20 },
|
||||
Text = text
|
||||
Text = text,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models
|
||||
public Bindable<bool> AutoProgressScreens = new BindableBool(true);
|
||||
|
||||
public Bindable<bool> SplitMapPoolByMods = new BindableBool(true);
|
||||
|
||||
public Bindable<bool> DisplayTeamSeeds = new BindableBool();
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
||||
{
|
||||
private readonly TeamScore score;
|
||||
|
||||
private readonly TournamentSpriteTextWithBackground teamText;
|
||||
private readonly TournamentSpriteTextWithBackground teamNameText;
|
||||
|
||||
private readonly Bindable<string> teamName = new Bindable<string>("???");
|
||||
|
||||
@ -95,7 +95,13 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
||||
}
|
||||
}
|
||||
},
|
||||
teamText = new TournamentSpriteTextWithBackground
|
||||
teamNameText = new TournamentSpriteTextWithBackground
|
||||
{
|
||||
Scale = new Vector2(0.5f),
|
||||
Origin = anchor,
|
||||
Anchor = anchor,
|
||||
},
|
||||
new DrawableTeamSeed(Team)
|
||||
{
|
||||
Scale = new Vector2(0.5f),
|
||||
Origin = anchor,
|
||||
@ -119,7 +125,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
||||
if (Team != null)
|
||||
teamName.BindTo(Team.FullName);
|
||||
|
||||
teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
|
||||
teamName.BindValueChanged(name => teamNameText.Text.Text = name.NewValue, true);
|
||||
}
|
||||
|
||||
private void updateDisplay()
|
||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
private Drawable chroma = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(LadderInfo ladder, MatchIPCInfo ipc)
|
||||
private void load(MatchIPCInfo ipc)
|
||||
{
|
||||
this.ipc = ipc;
|
||||
|
||||
@ -49,7 +49,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
},
|
||||
header = new MatchHeader
|
||||
{
|
||||
ShowLogo = false
|
||||
ShowLogo = false,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
@ -118,12 +118,12 @@ namespace osu.Game.Tournament.Screens.Gameplay
|
||||
LabelText = "Players per team",
|
||||
Current = LadderInfo.PlayersPerTeam,
|
||||
KeyboardStep = 1,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true);
|
||||
LadderInfo.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true);
|
||||
|
||||
warmup.BindValueChanged(w =>
|
||||
{
|
||||
|
@ -140,6 +140,12 @@ namespace osu.Game.Tournament.Screens.Setup
|
||||
Description = "Screens will progress automatically from gameplay -> results -> map pool",
|
||||
Current = LadderInfo.AutoProgressScreens,
|
||||
},
|
||||
new LabelledSwitchButton
|
||||
{
|
||||
Label = "Display team seeds",
|
||||
Description = "Team seeds will display alongside each team at the top in gameplay/map pool screens.",
|
||||
Current = LadderInfo.DisplayTeamSeeds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using System.Globalization;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Extensions;
|
||||
@ -46,7 +47,7 @@ namespace osu.Game.Online.API
|
||||
if (WebRequest != null)
|
||||
{
|
||||
Response = ((OsuJsonWebRequest<T>)WebRequest).ResponseObject;
|
||||
Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
|
||||
Logger.Log($"{GetType().ReadableName()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,7 @@ namespace osu.Game.Online.API
|
||||
LocalUser.Value = new APIUser
|
||||
{
|
||||
Username = username,
|
||||
Id = 1001,
|
||||
Id = DUMMY_USER_ID,
|
||||
};
|
||||
|
||||
state.Value = APIState.Online;
|
||||
|
@ -234,9 +234,8 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
set => Statistics.RankHistory = value;
|
||||
}
|
||||
|
||||
[JsonProperty(@"active_tournament_banner")]
|
||||
[CanBeNull]
|
||||
public TournamentBanner TournamentBanner;
|
||||
[JsonProperty(@"active_tournament_banners")]
|
||||
public TournamentBanner[] TournamentBanners;
|
||||
|
||||
[JsonProperty("badges")]
|
||||
public Badge[] Badges;
|
||||
|
@ -11,7 +11,7 @@ using osu.Game.Overlays.Profile.Header.Components;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header
|
||||
{
|
||||
public partial class BannerHeaderContainer : CompositeDrawable
|
||||
public partial class BannerHeaderContainer : FillFlowContainer
|
||||
{
|
||||
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
|
||||
|
||||
@ -19,9 +19,9 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
private void load()
|
||||
{
|
||||
Alpha = 0;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
FillMode = FillMode.Fit;
|
||||
FillAspectRatio = 1000 / 60f;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Direction = FillDirection.Vertical;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -40,13 +40,21 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
|
||||
ClearInternal();
|
||||
|
||||
var banner = user?.TournamentBanner;
|
||||
var banners = user?.TournamentBanners;
|
||||
|
||||
if (banner != null)
|
||||
if (banners?.Length > 0)
|
||||
{
|
||||
Show();
|
||||
|
||||
LoadComponentAsync(new DrawableTournamentBanner(banner), AddInternal, cancellationTokenSource.Token);
|
||||
for (int index = 0; index < banners.Length; index++)
|
||||
{
|
||||
int displayIndex = index;
|
||||
LoadComponentAsync(new DrawableTournamentBanner(banners[index]), asyncBanner =>
|
||||
{
|
||||
// load in stable order regardless of async load order.
|
||||
Insert(displayIndex, asyncBanner);
|
||||
}, cancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -15,12 +15,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
[LongRunningLoad]
|
||||
public partial class DrawableTournamentBanner : OsuClickableContainer
|
||||
{
|
||||
private const float banner_aspect_ratio = 60 / 1000f;
|
||||
private readonly TournamentBanner banner;
|
||||
|
||||
public DrawableTournamentBanner(TournamentBanner banner)
|
||||
{
|
||||
this.banner = banner;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -41,6 +42,12 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
this.FadeInFromZero(200);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
Height = DrawWidth * banner_aspect_ratio;
|
||||
}
|
||||
|
||||
public override LocalisableString TooltipText => "view in browser";
|
||||
}
|
||||
}
|
||||
|
@ -498,7 +498,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
|
||||
if (existingBinding == null)
|
||||
{
|
||||
realm.WriteAsync(r => r.Find<RealmKeyBinding>(keyBinding.ID)!.KeyCombinationString = keyBinding.KeyCombination.ToString());
|
||||
realm.Write(r => r.Find<RealmKeyBinding>(keyBinding.ID)!.KeyCombinationString = keyBinding.KeyCombination.ToString());
|
||||
BindingUpdated?.Invoke(this, new KeyBindingUpdatedEventArgs(bindingConflictResolved: false, advanceToNextBinding));
|
||||
return;
|
||||
}
|
||||
|
@ -135,7 +135,7 @@ namespace osu.Game.Overlays
|
||||
},
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Child = searchTextBox = new SeekLimitedSearchTextBox
|
||||
Child = searchTextBox = new SettingsSearchTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Origin = Anchor.TopCentre,
|
||||
|
22
osu.Game/Overlays/SettingsSearchTextBox.cs
Normal file
22
osu.Game/Overlays/SettingsSearchTextBox.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// 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.Input.Events;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
public partial class SettingsSearchTextBox : SeekLimitedSearchTextBox
|
||||
{
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
base.OnFocus(e);
|
||||
|
||||
// on mobile platforms, focus is not held by the search text box, and the select all feature
|
||||
// will not make sense on it, and might annoy the user when they try to focus manually.
|
||||
if (HoldFocus)
|
||||
SelectAll();
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -51,6 +52,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
private SelectionState state;
|
||||
|
||||
[CanBeNull]
|
||||
public event Action<SelectionState> StateChanged;
|
||||
|
||||
public SelectionState State
|
||||
|
@ -107,12 +107,52 @@ namespace osu.Game.Rulesets.Mods
|
||||
[JsonIgnore]
|
||||
public virtual bool HasImplementation => this is IApplicableMod;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mod can be played by a real human user.
|
||||
/// Non-user-playable mods are not viable for single-player score submission.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="ModDoubleTime"/> is user-playable.</item>
|
||||
/// <item><see cref="ModAutoplay"/> is not user-playable.</item>
|
||||
/// </list>
|
||||
/// </example>
|
||||
[JsonIgnore]
|
||||
public virtual bool UserPlayable => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mod can be specified as a "required" mod in a multiplayer context.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="ModHardRock"/> is valid for multiplayer.</item>
|
||||
/// <item>
|
||||
/// <see cref="ModDoubleTime"/> is valid for multiplayer as long as it is a <b>required</b> mod,
|
||||
/// as that ensures the same duration of gameplay for all users in the room.
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <see cref="ModAdaptiveSpeed"/> is not valid for multiplayer, as it leads to varying
|
||||
/// gameplay duration depending on how the users in the room play.
|
||||
/// </item>
|
||||
/// <item><see cref="ModAutoplay"/> is not valid for multiplayer.</item>
|
||||
/// </list>
|
||||
/// </example>
|
||||
[JsonIgnore]
|
||||
public virtual bool ValidForMultiplayer => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this mod can be specified as a "free" or "allowed" mod in a multiplayer context.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <list type="bullet">
|
||||
/// <item><see cref="ModHardRock"/> is valid for multiplayer as a free mod.</item>
|
||||
/// <item>
|
||||
/// <see cref="ModDoubleTime"/> is <b>not</b> valid for multiplayer as a free mod,
|
||||
/// as it could to varying gameplay duration between users in the room depending on whether they picked it.
|
||||
/// </item>
|
||||
/// <item><see cref="ModAutoplay"/> is not valid for multiplayer as a free mod.</item>
|
||||
/// </list>
|
||||
/// </example>
|
||||
[JsonIgnore]
|
||||
public virtual bool ValidForMultiplayerAsFreeMod => true;
|
||||
|
||||
|
@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
|
||||
public override bool ValidForMultiplayer => false;
|
||||
public override bool ValidForMultiplayerAsFreeMod => false;
|
||||
public sealed override bool ValidForMultiplayer => false;
|
||||
public sealed override bool ValidForMultiplayerAsFreeMod => false;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp), typeof(ModAutoplay) };
|
||||
|
||||
|
@ -20,9 +20,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override LocalisableString Description => "Watch a perfect automated play through the song.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override bool UserPlayable => false;
|
||||
public override bool ValidForMultiplayer => false;
|
||||
public override bool ValidForMultiplayerAsFreeMod => false;
|
||||
public sealed override bool UserPlayable => false;
|
||||
public sealed override bool ValidForMultiplayer => false;
|
||||
public sealed override bool ValidForMultiplayerAsFreeMod => false;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) };
|
||||
|
||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModRateAdjust : Mod, IApplicableToRate
|
||||
{
|
||||
public override bool ValidForMultiplayerAsFreeMod => false;
|
||||
public sealed override bool ValidForMultiplayerAsFreeMod => false;
|
||||
|
||||
public abstract BindableNumber<double> SpeedChange { get; }
|
||||
|
||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// This mod is used strictly to mark osu!stable scores set with the "Score V2" mod active.
|
||||
/// It should not be used in any real capacity going forward.
|
||||
/// </remarks>
|
||||
public class ModScoreV2 : Mod
|
||||
public sealed class ModScoreV2 : Mod
|
||||
{
|
||||
public override string Name => "Score V2";
|
||||
public override string Acronym => @"SV2";
|
||||
@ -17,5 +17,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool UserPlayable => false;
|
||||
public override bool ValidForMultiplayer => false;
|
||||
public override bool ValidForMultiplayerAsFreeMod => false;
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
|
||||
public abstract BindableBool AdjustPitch { get; }
|
||||
|
||||
public override bool ValidForMultiplayerAsFreeMod => false;
|
||||
public sealed override bool ValidForMultiplayerAsFreeMod => false;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };
|
||||
|
||||
|
@ -5,7 +5,7 @@ using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class UnknownMod : Mod
|
||||
public sealed class UnknownMod : Mod
|
||||
{
|
||||
/// <summary>
|
||||
/// The acronym of the mod which could not be resolved.
|
||||
|
@ -182,6 +182,12 @@ namespace osu.Game.Scoring
|
||||
base.PostImport(model, realm, parameters);
|
||||
|
||||
populateUserDetails(model);
|
||||
|
||||
Debug.Assert(model.BeatmapInfo != null);
|
||||
|
||||
// This needs to be run after user detail population to ensure we have a valid user id.
|
||||
if (api.IsLoggedIn && api.LocalUser.Value.OnlineID == model.UserID && (model.BeatmapInfo.LastPlayed == null || model.Date > model.BeatmapInfo.LastPlayed))
|
||||
model.BeatmapInfo.LastPlayed = model.Date;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -166,6 +166,8 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
public override void Add(Drawable drawable)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drawable);
|
||||
|
||||
if (drawable is Background)
|
||||
throw new InvalidOperationException($"Use {nameof(Background)} to set a background.");
|
||||
|
||||
|
@ -321,7 +321,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
Spacing = new Vector2(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
divisorTextBox = new OsuNumberBox
|
||||
divisorTextBox = new AutoSelectTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
PlaceholderText = "Beat divisor"
|
||||
@ -341,8 +341,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
base.LoadComplete();
|
||||
BeatDivisor.BindValueChanged(_ => updateState(), true);
|
||||
divisorTextBox.OnCommit += (_, _) => setPresetsFromTextBoxEntry();
|
||||
|
||||
Schedule(() => GetContainingInputManager().ChangeFocus(divisorTextBox));
|
||||
}
|
||||
|
||||
private void setPresetsFromTextBoxEntry()
|
||||
@ -590,5 +588,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private partial class AutoSelectTextBox : OsuNumberBox
|
||||
{
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
GetContainingInputManager().ChangeFocus(this);
|
||||
SelectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,11 +40,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
public PlacementBlueprint CurrentPlacement { get; private set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private EditorScreenWithTimeline editorScreen { get; set; }
|
||||
|
||||
/// <remarks>
|
||||
/// Positional input must be received outside the container's bounds,
|
||||
/// in order to handle composer blueprints which are partially offscreen.
|
||||
/// </remarks>
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => editorScreen?.MainContent.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
public ComposeBlueprintContainer(HitObjectComposer composer)
|
||||
: base(composer)
|
||||
|
@ -11,13 +11,14 @@ using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
[Cached]
|
||||
public abstract partial class EditorScreenWithTimeline : EditorScreen
|
||||
{
|
||||
public const float PADDING = 10;
|
||||
|
||||
private Container timelineContainer = null!;
|
||||
public Container TimelineContent { get; private set; } = null!;
|
||||
|
||||
private Container mainContent = null!;
|
||||
public Container MainContent { get; private set; } = null!;
|
||||
|
||||
private LoadingSpinner spinner = null!;
|
||||
|
||||
@ -70,7 +71,7 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
timelineContainer = new Container
|
||||
TimelineContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
@ -93,7 +94,7 @@ namespace osu.Game.Screens.Edit
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
mainContent = new Container
|
||||
MainContent = new Container
|
||||
{
|
||||
Name = "Main content",
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -116,10 +117,10 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
spinner.State.Value = Visibility.Hidden;
|
||||
|
||||
mainContent.Add(content);
|
||||
MainContent.Add(content);
|
||||
content.FadeInFromZero(300, Easing.OutQuint);
|
||||
|
||||
LoadComponentAsync(new TimelineArea(CreateTimelineContent()), timelineContainer.Add);
|
||||
LoadComponentAsync(new TimelineArea(CreateTimelineContent()), TimelineContent.Add);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -123,6 +123,8 @@ namespace osu.Game.Screens.Edit
|
||||
oldWithRepeats.NodeSamples.Clear();
|
||||
oldWithRepeats.NodeSamples.AddRange(newWithRepeats.NodeSamples);
|
||||
}
|
||||
|
||||
editorBeatmap.Update(oldObject);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,16 +34,18 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
private IAdjustableClock metronomeClock = null!;
|
||||
|
||||
private Sample? sampleTick;
|
||||
private Sample? sampleTickDownbeat;
|
||||
private Sample? sampleLatch;
|
||||
|
||||
private ScheduledDelegate? tickPlaybackDelegate;
|
||||
private readonly MetronomeTick metronomeTick = new MetronomeTick();
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider overlayColourProvider { get; set; } = null!;
|
||||
|
||||
public bool EnableClicking { get; set; } = true;
|
||||
public bool EnableClicking
|
||||
{
|
||||
get => metronomeTick.EnableClicking;
|
||||
set => metronomeTick.EnableClicking = value;
|
||||
}
|
||||
|
||||
public MetronomeDisplay()
|
||||
{
|
||||
@ -53,8 +55,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleTick = audio.Samples.Get(@"UI/metronome-tick");
|
||||
sampleTickDownbeat = audio.Samples.Get(@"UI/metronome-tick-downbeat");
|
||||
sampleLatch = audio.Samples.Get(@"UI/metronome-latch");
|
||||
|
||||
const float taper = 25;
|
||||
@ -67,8 +67,11 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
metronomeTick.Ticked = onTickPlayed;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
metronomeTick,
|
||||
new Container
|
||||
{
|
||||
Name = @"Taper adjust",
|
||||
@ -265,9 +268,6 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
|
||||
isSwinging = false;
|
||||
|
||||
tickPlaybackDelegate?.Cancel();
|
||||
tickPlaybackDelegate = null;
|
||||
|
||||
// instantly latch if pendulum arm is close enough to center (to prevent awkward delayed playback of latch sound)
|
||||
if (Precision.AlmostEquals(swing.Rotation, 0, 1))
|
||||
{
|
||||
@ -306,27 +306,53 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
float targetAngle = currentAngle > 0 ? -angle : angle;
|
||||
|
||||
swing.RotateTo(targetAngle, beatLength, Easing.InOutQuad);
|
||||
}
|
||||
|
||||
if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging)
|
||||
private void onTickPlayed()
|
||||
{
|
||||
// Originally, this flash only occurred when the pendulum correctly passess the centre.
|
||||
// Mappers weren't happy with the metronome tick not playing immediately after starting playback
|
||||
// so now this matches the actual tick sample.
|
||||
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private partial class MetronomeTick : BeatSyncedContainer
|
||||
{
|
||||
public bool EnableClicking;
|
||||
|
||||
private Sample? sampleTick;
|
||||
private Sample? sampleTickDownbeat;
|
||||
|
||||
public Action? Ticked;
|
||||
|
||||
public MetronomeTick()
|
||||
{
|
||||
using (BeginDelayedSequence(beatLength / 2))
|
||||
{
|
||||
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
|
||||
AllowMistimedEventFiring = false;
|
||||
}
|
||||
|
||||
tickPlaybackDelegate = Schedule(() =>
|
||||
{
|
||||
if (!EnableClicking)
|
||||
return;
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
sampleTick = audio.Samples.Get(@"UI/metronome-tick");
|
||||
sampleTickDownbeat = audio.Samples.Get(@"UI/metronome-tick-downbeat");
|
||||
}
|
||||
|
||||
var channel = beatIndex % timingPoint.TimeSignature.Numerator == 0 ? sampleTickDownbeat?.GetChannel() : sampleTick?.GetChannel();
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
{
|
||||
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
||||
|
||||
if (channel == null)
|
||||
return;
|
||||
if (!IsBeatSyncedWithTrack || !EnableClicking)
|
||||
return;
|
||||
|
||||
channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f);
|
||||
channel.Play();
|
||||
});
|
||||
}
|
||||
var channel = beatIndex % timingPoint.TimeSignature.Numerator == 0 ? sampleTickDownbeat?.GetChannel() : sampleTick?.GetChannel();
|
||||
|
||||
if (channel == null)
|
||||
return;
|
||||
|
||||
channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f);
|
||||
channel.Play();
|
||||
|
||||
Ticked?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Screens.Play
|
||||
///
|
||||
/// In the future I want to change this.
|
||||
/// </summary>
|
||||
private double? actualStopTime;
|
||||
internal double? LastStopTime;
|
||||
|
||||
[Resolved]
|
||||
private MusicController musicController { get; set; } = null!;
|
||||
@ -100,7 +100,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
protected override void StopGameplayClock()
|
||||
{
|
||||
actualStopTime = GameplayClock.CurrentTime;
|
||||
LastStopTime = GameplayClock.CurrentTime;
|
||||
|
||||
if (IsLoaded)
|
||||
{
|
||||
@ -127,17 +127,17 @@ namespace osu.Game.Screens.Play
|
||||
public override void Seek(double time)
|
||||
{
|
||||
// Safety in case the clock is seeked while stopped.
|
||||
actualStopTime = null;
|
||||
LastStopTime = null;
|
||||
|
||||
base.Seek(time);
|
||||
}
|
||||
|
||||
protected override void PrepareStart()
|
||||
{
|
||||
if (actualStopTime != null)
|
||||
if (LastStopTime != null)
|
||||
{
|
||||
Seek(actualStopTime.Value);
|
||||
actualStopTime = null;
|
||||
Seek(LastStopTime.Value);
|
||||
LastStopTime = null;
|
||||
}
|
||||
else
|
||||
base.PrepareStart();
|
||||
|
@ -7,6 +7,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -190,6 +191,7 @@ namespace osu.Game.Screens.Play
|
||||
private const float padding = 2;
|
||||
public const float WIDTH = cube_size + padding;
|
||||
|
||||
[CanBeNull]
|
||||
public event Action<ColumnState> StateChanged;
|
||||
|
||||
private readonly List<Box> drawableRows = new List<Box>();
|
||||
|
@ -188,7 +188,10 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
// token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
|
||||
if (token == null)
|
||||
{
|
||||
Logger.Log("No token, skipping score submission");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (scoreSubmissionSource != null)
|
||||
return scoreSubmissionSource.Task;
|
||||
@ -197,6 +200,8 @@ namespace osu.Game.Screens.Play
|
||||
if (!score.ScoreInfo.Statistics.Any(s => s.Key.IsHit() && s.Value > 0))
|
||||
return Task.CompletedTask;
|
||||
|
||||
Logger.Log($"Beginning score submission (token:{token.Value})...");
|
||||
|
||||
scoreSubmissionSource = new TaskCompletionSource<bool>();
|
||||
var request = CreateSubmissionRequest(score, token.Value);
|
||||
|
||||
@ -206,11 +211,12 @@ namespace osu.Game.Screens.Play
|
||||
score.ScoreInfo.Position = s.Position;
|
||||
|
||||
scoreSubmissionSource.SetResult(true);
|
||||
Logger.Log($"Score submission completed! (token:{token.Value} id:{s.ID})");
|
||||
};
|
||||
|
||||
request.Failure += e =>
|
||||
{
|
||||
Logger.Error(e, $"Failed to submit score ({e.Message})");
|
||||
Logger.Error(e, $"Failed to submit score (token:{token.Value}): {e.Message}");
|
||||
scoreSubmissionSource.SetResult(false);
|
||||
};
|
||||
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Collections;
|
||||
@ -23,6 +24,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.Select
|
||||
{
|
||||
@ -254,9 +256,6 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
public OsuSpriteText FilterText { get; private set; }
|
||||
|
||||
// clipboard is disabled because one of the "cut" platform key bindings (shift-delete) conflicts with the beatmap deletion action.
|
||||
protected override bool AllowClipboardExport => false;
|
||||
|
||||
public FilterControlTextBox()
|
||||
{
|
||||
Height += filter_text_size;
|
||||
@ -277,6 +276,15 @@ namespace osu.Game.Screens.Select
|
||||
Colour = colours.Yellow
|
||||
});
|
||||
}
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||
{
|
||||
// the "cut" platform key binding (shift-delete) conflicts with the beatmap deletion action.
|
||||
if (e.Action == PlatformAction.Cut && e.ShiftPressed && e.CurrentState.Keyboard.Keys.IsPressed(Key.Delete))
|
||||
return false;
|
||||
|
||||
return base.OnPressed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ namespace osu.Game.Skinning
|
||||
public float ScorePosition = 300 * POSITION_SCALE_FACTOR;
|
||||
public bool ShowJudgementLine = true;
|
||||
public bool KeysUnderNotes;
|
||||
public int LightFramePerSecond = 60;
|
||||
|
||||
public LegacyNoteBodyStyle? NoteBodyStyle;
|
||||
|
||||
|
@ -74,6 +74,7 @@ namespace osu.Game.Skinning
|
||||
Hit50,
|
||||
Hit0,
|
||||
KeysUnderNotes,
|
||||
NoteBodyStyle
|
||||
NoteBodyStyle,
|
||||
LightFramePerSecond
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +123,11 @@ namespace osu.Game.Skinning
|
||||
currentConfig.WidthForNoteHeightScale = (float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR;
|
||||
break;
|
||||
|
||||
case "LightFramePerSecond":
|
||||
int lightFramePerSecond = int.Parse(pair.Value, CultureInfo.InvariantCulture);
|
||||
currentConfig.LightFramePerSecond = lightFramePerSecond > 0 ? lightFramePerSecond : 24;
|
||||
break;
|
||||
|
||||
case string when pair.Key.StartsWith("Colour", StringComparison.Ordinal):
|
||||
HandleColours(currentConfig, line, true);
|
||||
break;
|
||||
|
@ -273,6 +273,9 @@ namespace osu.Game.Skinning
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.KeysUnderNotes:
|
||||
return SkinUtils.As<TValue>(new Bindable<bool>(existing.KeysUnderNotes));
|
||||
|
||||
case LegacyManiaSkinConfigurationLookups.LightFramePerSecond:
|
||||
return SkinUtils.As<TValue>(new Bindable<int>(existing.LightFramePerSecond));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -115,7 +115,18 @@ namespace osu.Game.Skinning
|
||||
|
||||
maxSize *= texture.ScaleAdjust;
|
||||
|
||||
var croppedTexture = texture.Crop(new RectangleF(texture.Width / 2f - maxSize.X / 2f, texture.Height / 2f - maxSize.Y / 2f, maxSize.X, maxSize.Y));
|
||||
// Importantly, check per-axis for the minimum dimension to avoid accidentally inflating
|
||||
// textures with weird aspect ratios.
|
||||
float newWidth = Math.Min(texture.Width, maxSize.X);
|
||||
float newHeight = Math.Min(texture.Height, maxSize.Y);
|
||||
|
||||
var croppedTexture = texture.Crop(new RectangleF(
|
||||
texture.Width / 2f - newWidth / 2f,
|
||||
texture.Height / 2f - newHeight / 2f,
|
||||
newWidth,
|
||||
newHeight
|
||||
));
|
||||
|
||||
croppedTexture.ScaleAdjust = texture.ScaleAdjust;
|
||||
return croppedTexture;
|
||||
}
|
||||
|
@ -36,7 +36,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="11.5.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1012.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2023.1030.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1030.0" />
|
||||
<PackageReference Include="Sentry" Version="3.40.0" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
|
@ -23,6 +23,6 @@
|
||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1012.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1030.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user