mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 17:07:38 +08:00
Merge branch 'master' into fix-timeline-zooming
This commit is contained in:
commit
8471b24659
2
.github/workflows/sentry-release.yml
vendored
2
.github/workflows/sentry-release.yml
vendored
@ -23,4 +23,4 @@ jobs:
|
|||||||
SENTRY_URL: https://sentry.ppy.sh/
|
SENTRY_URL: https://sentry.ppy.sh/
|
||||||
with:
|
with:
|
||||||
environment: production
|
environment: production
|
||||||
version: ${{ github.ref }}
|
version: osu@${{ github.ref_name }}
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.528.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.530.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
@ -17,11 +17,11 @@ using osu.Framework.Testing.Input;
|
|||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu.Skinning;
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Tests.Gameplay;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Tests
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
public TestSceneGameplayCursor()
|
public TestSceneGameplayCursor()
|
||||||
{
|
{
|
||||||
var ruleset = new OsuRuleset();
|
var ruleset = new OsuRuleset();
|
||||||
gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty<Mod>());
|
gameplayState = TestGameplayState.Create(ruleset);
|
||||||
|
|
||||||
AddStep("change background colour", () =>
|
AddStep("change background colour", () =>
|
||||||
{
|
{
|
||||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
if (positionInfo == positionInfos.First())
|
if (positionInfo == positionInfos.First())
|
||||||
{
|
{
|
||||||
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
|
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.Y / 2);
|
||||||
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
|
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -116,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
if (!(osuObject is Slider slider))
|
if (!(osuObject is Slider slider))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - h.Position.X, h.Position.Y));
|
||||||
|
|
||||||
@ -137,6 +138,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
if (!(osuObject is Slider slider))
|
if (!(osuObject is Slider slider))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||||
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||||
|
|
||||||
@ -146,5 +148,41 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
|
|
||||||
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rotate a slider about its start position by the specified angle.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slider">The slider to be rotated.</param>
|
||||||
|
/// <param name="rotation">The angle, measured in radians, to rotate the slider by.</param>
|
||||||
|
public static void RotateSlider(Slider slider, float rotation)
|
||||||
|
{
|
||||||
|
void rotateNestedObject(OsuHitObject nested) => nested.Position = rotateVector(nested.Position - slider.Position, rotation) + slider.Position;
|
||||||
|
|
||||||
|
// No need to update the head and tail circles, since slider handles that when the new slider path is set
|
||||||
|
slider.NestedHitObjects.OfType<SliderTick>().ForEach(rotateNestedObject);
|
||||||
|
slider.NestedHitObjects.OfType<SliderRepeat>().ForEach(rotateNestedObject);
|
||||||
|
|
||||||
|
var controlPoints = slider.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray();
|
||||||
|
foreach (var point in controlPoints)
|
||||||
|
point.Position = rotateVector(point.Position, rotation);
|
||||||
|
|
||||||
|
slider.Path = new SliderPath(controlPoints, slider.Path.ExpectedDistance.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rotate a vector by the specified angle.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="vector">The vector to be rotated.</param>
|
||||||
|
/// <param name="rotation">The angle, measured in radians, to rotate the vector by.</param>
|
||||||
|
/// <returns>The rotated vector.</returns>
|
||||||
|
private static Vector2 rotateVector(Vector2 vector, float rotation)
|
||||||
|
{
|
||||||
|
float angle = MathF.Atan2(vector.Y, vector.X) + rotation;
|
||||||
|
float length = vector.Length;
|
||||||
|
return new Vector2(
|
||||||
|
length * MathF.Cos(angle),
|
||||||
|
length * MathF.Sin(angle)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Graphics.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Osu.UI;
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -37,15 +38,23 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
foreach (OsuHitObject hitObject in hitObjects)
|
foreach (OsuHitObject hitObject in hitObjects)
|
||||||
{
|
{
|
||||||
Vector2 relativePosition = hitObject.Position - previousPosition;
|
Vector2 relativePosition = hitObject.Position - previousPosition;
|
||||||
float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
|
float absoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
|
||||||
float relativeAngle = absoluteAngle - previousAngle;
|
float relativeAngle = absoluteAngle - previousAngle;
|
||||||
|
|
||||||
positionInfos.Add(new ObjectPositionInfo(hitObject)
|
ObjectPositionInfo positionInfo;
|
||||||
|
positionInfos.Add(positionInfo = new ObjectPositionInfo(hitObject)
|
||||||
{
|
{
|
||||||
RelativeAngle = relativeAngle,
|
RelativeAngle = relativeAngle,
|
||||||
DistanceFromPrevious = relativePosition.Length
|
DistanceFromPrevious = relativePosition.Length
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (hitObject is Slider slider)
|
||||||
|
{
|
||||||
|
float absoluteRotation = getSliderRotation(slider);
|
||||||
|
positionInfo.Rotation = absoluteRotation - absoluteAngle;
|
||||||
|
absoluteAngle = absoluteRotation;
|
||||||
|
}
|
||||||
|
|
||||||
previousPosition = hitObject.EndPosition;
|
previousPosition = hitObject.EndPosition;
|
||||||
previousAngle = absoluteAngle;
|
previousAngle = absoluteAngle;
|
||||||
}
|
}
|
||||||
@ -70,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
|
|
||||||
if (hitObject is Spinner)
|
if (hitObject is Spinner)
|
||||||
{
|
{
|
||||||
previous = null;
|
previous = current;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,16 +133,23 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
|
|
||||||
if (previous != null)
|
if (previous != null)
|
||||||
{
|
{
|
||||||
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
|
if (previous.HitObject is Slider s)
|
||||||
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
|
{
|
||||||
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
|
previousAbsoluteAngle = getSliderRotation(s);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
|
||||||
|
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
|
||||||
|
previousAbsoluteAngle = MathF.Atan2(relativePosition.Y, relativePosition.X);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
|
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
|
||||||
|
|
||||||
var posRelativeToPrev = new Vector2(
|
var posRelativeToPrev = new Vector2(
|
||||||
current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
|
current.PositionInfo.DistanceFromPrevious * MathF.Cos(absoluteAngle),
|
||||||
current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
|
current.PositionInfo.DistanceFromPrevious * MathF.Sin(absoluteAngle)
|
||||||
);
|
);
|
||||||
|
|
||||||
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
|
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
|
||||||
@ -141,6 +157,19 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
|
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
|
||||||
|
|
||||||
current.PositionModified = lastEndPosition + posRelativeToPrev;
|
current.PositionModified = lastEndPosition + posRelativeToPrev;
|
||||||
|
|
||||||
|
if (!(current.HitObject is Slider slider))
|
||||||
|
return;
|
||||||
|
|
||||||
|
absoluteAngle = MathF.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
|
||||||
|
|
||||||
|
Vector2 centreOfMassOriginal = calculateCentreOfMass(slider);
|
||||||
|
Vector2 centreOfMassModified = rotateVector(centreOfMassOriginal, current.PositionInfo.Rotation + absoluteAngle - getSliderRotation(slider));
|
||||||
|
centreOfMassModified = RotateAwayFromEdge(current.PositionModified, centreOfMassModified);
|
||||||
|
|
||||||
|
float relativeRotation = MathF.Atan2(centreOfMassModified.Y, centreOfMassModified.X) - MathF.Atan2(centreOfMassOriginal.Y, centreOfMassOriginal.X);
|
||||||
|
if (!Precision.AlmostEquals(relativeRotation, 0))
|
||||||
|
RotateSlider(slider, relativeRotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -172,13 +201,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
var previousPosition = workingObject.PositionModified;
|
var previousPosition = workingObject.PositionModified;
|
||||||
|
|
||||||
// Clamp slider position to the placement area
|
// Clamp slider position to the placement area
|
||||||
// If the slider is larger than the playfield, force it to stay at the original position
|
// If the slider is larger than the playfield, at least make sure that the head circle is inside the playfield
|
||||||
float newX = possibleMovementBounds.Width < 0
|
float newX = possibleMovementBounds.Width < 0
|
||||||
? workingObject.PositionOriginal.X
|
? Math.Clamp(possibleMovementBounds.Left, 0, OsuPlayfield.BASE_SIZE.X)
|
||||||
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
|
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
|
||||||
|
|
||||||
float newY = possibleMovementBounds.Height < 0
|
float newY = possibleMovementBounds.Height < 0
|
||||||
? workingObject.PositionOriginal.Y
|
? Math.Clamp(possibleMovementBounds.Top, 0, OsuPlayfield.BASE_SIZE.Y)
|
||||||
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
|
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
|
||||||
|
|
||||||
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
|
||||||
@ -287,6 +316,45 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Estimate the centre of mass of a slider relative to its start position.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slider">The slider to process.</param>
|
||||||
|
/// <returns>The centre of mass of the slider.</returns>
|
||||||
|
private static Vector2 calculateCentreOfMass(Slider slider)
|
||||||
|
{
|
||||||
|
const double sample_step = 50;
|
||||||
|
|
||||||
|
// just sample the start and end positions if the slider is too short
|
||||||
|
if (slider.Distance <= sample_step)
|
||||||
|
{
|
||||||
|
return Vector2.Divide(slider.Path.PositionAt(1), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
Vector2 sum = Vector2.Zero;
|
||||||
|
double pathDistance = slider.Distance;
|
||||||
|
|
||||||
|
for (double i = 0; i < pathDistance; i += sample_step)
|
||||||
|
{
|
||||||
|
sum += slider.Path.PositionAt(i / pathDistance);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum / count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the absolute rotation of a slider, defined as the angle from its start position to the end of its path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slider">The slider to process.</param>
|
||||||
|
/// <returns>The angle in radians.</returns>
|
||||||
|
private static float getSliderRotation(Slider slider)
|
||||||
|
{
|
||||||
|
var endPositionVector = slider.Path.PositionAt(1);
|
||||||
|
return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
|
||||||
|
}
|
||||||
|
|
||||||
public class ObjectPositionInfo
|
public class ObjectPositionInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -309,6 +377,13 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public float DistanceFromPrevious { get; set; }
|
public float DistanceFromPrevious { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The rotation of the hit object, relative to its jump angle.
|
||||||
|
/// For sliders, this is defined as the angle from the slider's start position to the end of its path, relative to its jump angle.
|
||||||
|
/// For hit circles and spinners, this property is ignored.
|
||||||
|
/// </summary>
|
||||||
|
public float Rotation { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
|
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -61,13 +61,13 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||||
|
|
||||||
// No header shouldn't cause any change
|
// No header shouldn't cause any change
|
||||||
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame());
|
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame());
|
||||||
|
|
||||||
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
|
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
|
||||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||||
|
|
||||||
// Reset with a miss instead.
|
// Reset with a miss instead.
|
||||||
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
|
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
|
||||||
{
|
{
|
||||||
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
|
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
|
||||||
});
|
});
|
||||||
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
|
||||||
|
|
||||||
// Reset with no judged hit.
|
// Reset with no judged hit.
|
||||||
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
|
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
|
||||||
{
|
{
|
||||||
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now)
|
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now)
|
||||||
});
|
});
|
||||||
|
@ -77,6 +77,12 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}";
|
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNoop()
|
||||||
|
{
|
||||||
|
AddStep("do nothing", () => { });
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestTapThenReset()
|
public void TestTapThenReset()
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||||
@ -18,6 +20,28 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer);
|
public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestContextMenu()
|
||||||
|
{
|
||||||
|
TimelineHitObjectBlueprint blueprint;
|
||||||
|
|
||||||
|
AddStep("add object", () =>
|
||||||
|
{
|
||||||
|
EditorBeatmap.Clear();
|
||||||
|
EditorBeatmap.Add(new HitCircle { StartTime = 3000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("click object", () =>
|
||||||
|
{
|
||||||
|
blueprint = this.ChildrenOfType<TimelineHitObjectBlueprint>().Single();
|
||||||
|
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]
|
[Test]
|
||||||
public void TestDisallowZeroDurationObjects()
|
public void TestDisallowZeroDurationObjects()
|
||||||
{
|
{
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Timing;
|
using osu.Game.Screens.Edit.Timing;
|
||||||
|
using osu.Game.Screens.Edit.Timing.RowAttributes;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
{
|
{
|
||||||
@ -22,6 +26,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
||||||
|
|
||||||
|
private TimingScreen timingScreen;
|
||||||
|
|
||||||
protected override bool ScrollUsingMouseWheel => false;
|
protected override bool ScrollUsingMouseWheel => false;
|
||||||
|
|
||||||
public TestSceneTimingScreen()
|
public TestSceneTimingScreen()
|
||||||
@ -36,12 +42,54 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
|
||||||
Beatmap.Disabled = true;
|
Beatmap.Disabled = true;
|
||||||
|
|
||||||
Child = new TimingScreen
|
Child = timingScreen = new TimingScreen
|
||||||
{
|
{
|
||||||
State = { Value = Visibility.Visible },
|
State = { Value = Visibility.Visible },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("Stop clock", () => Clock.Stop());
|
||||||
|
|
||||||
|
AddUntilStep("wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTrackingCurrentTimeWhileRunning()
|
||||||
|
{
|
||||||
|
AddStep("Select first effect point", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
|
||||||
|
AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
|
||||||
|
|
||||||
|
AddStep("Seek to just before next point", () => Clock.Seek(69000));
|
||||||
|
AddStep("Start clock", () => Clock.Start());
|
||||||
|
|
||||||
|
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTrackingCurrentTimeWhilePaused()
|
||||||
|
{
|
||||||
|
AddStep("Select first effect point", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(Child.ChildrenOfType<EffectRowAttribute>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 54670);
|
||||||
|
AddUntilStep("Ensure seeked to correct time", () => Clock.CurrentTimeAccurate == 54670);
|
||||||
|
|
||||||
|
AddStep("Seek to later", () => Clock.Seek(80000));
|
||||||
|
AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 69670);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
Beatmap.Disabled = false;
|
Beatmap.Disabled = false;
|
||||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.Cursor;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
@ -38,25 +39,29 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0);
|
Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0);
|
||||||
|
|
||||||
AddRange(new Drawable[]
|
Add(new OsuContextMenuContainer
|
||||||
{
|
{
|
||||||
EditorBeatmap,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Composer,
|
Children = new Drawable[]
|
||||||
new FillFlowContainer
|
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
EditorBeatmap,
|
||||||
Direction = FillDirection.Vertical,
|
Composer,
|
||||||
Spacing = new Vector2(0, 5),
|
new FillFlowContainer
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
new StartStopButton(),
|
AutoSizeAxes = Axes.Both,
|
||||||
new AudioVisualiser(),
|
Direction = FillDirection.Vertical,
|
||||||
|
Spacing = new Vector2(0, 5),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new StartStopButton(),
|
||||||
|
new AudioVisualiser(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
TimelineArea = new TimelineArea(CreateTestComponent())
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
}
|
}
|
||||||
},
|
|
||||||
TimelineArea = new TimelineArea(CreateTestComponent())
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Lists;
|
using osu.Framework.Lists;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
@ -22,7 +21,6 @@ using osu.Game.Screens.Play;
|
|||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osu.Game.Tests.Beatmaps;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
@ -33,18 +31,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private SkinManager skinManager { get; set; }
|
private SkinManager skinManager { get; set; }
|
||||||
|
|
||||||
[Cached]
|
|
||||||
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
|
||||||
|
|
||||||
[Cached(typeof(HealthProcessor))]
|
|
||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
|
||||||
|
|
||||||
[Cached]
|
|
||||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
|
||||||
|
|
||||||
[Cached]
|
|
||||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
|
||||||
|
|
||||||
protected override bool HasCustomSteps => true;
|
protected override bool HasCustomSteps => true;
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -81,11 +67,19 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
if (expectedComponentsContainer == null)
|
if (expectedComponentsContainer == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var expectedComponentsAdjustmentContainer = new Container
|
var expectedComponentsAdjustmentContainer = new DependencyProvidingContainer
|
||||||
{
|
{
|
||||||
Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
|
Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
|
||||||
Size = actualComponentsContainer.DrawSize,
|
Size = actualComponentsContainer.DrawSize,
|
||||||
Child = expectedComponentsContainer,
|
Child = expectedComponentsContainer,
|
||||||
|
// proxy the same required dependencies that `actualComponentsContainer` is using.
|
||||||
|
CachedDependencies = new (Type, object)[]
|
||||||
|
{
|
||||||
|
(typeof(ScoreProcessor), actualComponentsContainer.Dependencies.Get<ScoreProcessor>()),
|
||||||
|
(typeof(HealthProcessor), actualComponentsContainer.Dependencies.Get<HealthProcessor>()),
|
||||||
|
(typeof(GameplayState), actualComponentsContainer.Dependencies.Get<GameplayState>()),
|
||||||
|
(typeof(GameplayClock), actualComponentsContainer.Dependencies.Get<GameplayClock>())
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Add(expectedComponentsAdjustmentContainer);
|
Add(expectedComponentsAdjustmentContainer);
|
||||||
|
@ -15,7 +15,7 @@ using osu.Game.Rulesets.Osu;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Gameplay;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -14,16 +13,15 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Input.StateChanges;
|
using osu.Framework.Input.StateChanges;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Tests.Gameplay;
|
||||||
using osu.Game.Tests.Mods;
|
using osu.Game.Tests.Mods;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -41,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private TestReplayRecorder recorder;
|
private TestReplayRecorder recorder;
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
|
@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning.Editor;
|
using osu.Game.Skinning.Editor;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Gameplay;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||||
|
@ -16,7 +16,7 @@ using osu.Game.Rulesets.Mods;
|
|||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Beatmaps;
|
using osu.Game.Tests.Gameplay;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||||
|
@ -18,8 +18,8 @@ using osu.Game.Rulesets.UI;
|
|||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens;
|
using osu.Game.Screens;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Beatmaps;
|
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
|
using osu.Game.Tests.Gameplay;
|
||||||
using osu.Game.Tests.Visual.Multiplayer;
|
using osu.Game.Tests.Visual.Multiplayer;
|
||||||
using osu.Game.Tests.Visual.Spectator;
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -259,12 +259,15 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestFinalFramesPurgedBeforeEndingPlay()
|
public void TestFinalFramesPurgedBeforeEndingPlay()
|
||||||
{
|
{
|
||||||
AddStep("begin playing", () => spectatorClient.BeginPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()), new Score()));
|
AddStep("begin playing", () => spectatorClient.BeginPlaying(TestGameplayState.Create(new OsuRuleset()), new Score()));
|
||||||
|
|
||||||
AddStep("send frames and finish play", () =>
|
AddStep("send frames and finish play", () =>
|
||||||
{
|
{
|
||||||
spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero));
|
spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero));
|
||||||
spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true });
|
|
||||||
|
var completedGameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
completedGameplayState.HasPassed = true;
|
||||||
|
spectatorClient.EndPlaying(completedGameplayState);
|
||||||
});
|
});
|
||||||
|
|
||||||
// We can't access API because we're an "online" test.
|
// We can't access API because we're an "online" test.
|
||||||
|
@ -20,13 +20,13 @@ using osu.Game.Online.Spectator;
|
|||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Replays.Legacy;
|
using osu.Game.Replays.Legacy;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Tests.Gameplay;
|
||||||
using osu.Game.Tests.Mods;
|
using osu.Game.Tests.Mods;
|
||||||
using osu.Game.Tests.Visual.Spectator;
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
CachedDependencies = new[]
|
CachedDependencies = new[]
|
||||||
{
|
{
|
||||||
(typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())),
|
(typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())),
|
||||||
(typeof(GameplayState), new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>()))
|
(typeof(GameplayState), TestGameplayState.Create(new OsuRuleset()))
|
||||||
},
|
},
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
typeof(DashboardOverlay),
|
typeof(DashboardOverlay),
|
||||||
typeof(NewsOverlay),
|
typeof(NewsOverlay),
|
||||||
typeof(ChannelManager),
|
typeof(ChannelManager),
|
||||||
typeof(ChatOverlayV2),
|
typeof(ChatOverlay),
|
||||||
typeof(SettingsOverlay),
|
typeof(SettingsOverlay),
|
||||||
typeof(UserProfileOverlay),
|
typeof(UserProfileOverlay),
|
||||||
typeof(BeatmapSetOverlay),
|
typeof(BeatmapSetOverlay),
|
||||||
|
@ -86,9 +86,9 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestOverlaysAlwaysClosed()
|
public void TestOverlaysAlwaysClosed()
|
||||||
{
|
{
|
||||||
ChatOverlayV2 chat = null;
|
ChatOverlay chat = null;
|
||||||
AddUntilStep("is at menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
AddUntilStep("is at menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||||
AddUntilStep("wait for chat load", () => (chat = Game.ChildrenOfType<ChatOverlayV2>().SingleOrDefault()) != null);
|
AddUntilStep("wait for chat load", () => (chat = Game.ChildrenOfType<ChatOverlay>().SingleOrDefault()) != null);
|
||||||
|
|
||||||
AddStep("show chat", () => InputManager.Key(Key.F8));
|
AddStep("show chat", () => InputManager.Key(Key.F8));
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
leaveText.Text = $"OnRequestLeave: {channel.Name}";
|
leaveText.Text = $"OnRequestLeave: {channel.Name}";
|
||||||
leaveText.FadeOutFromOne(1000, Easing.InQuint);
|
leaveText.FadeOutFromOne(1000, Easing.InQuint);
|
||||||
selected.Value = null;
|
selected.Value = channelList.ChannelListingChannel;
|
||||||
channelList.RemoveChannel(channel);
|
channelList.RemoveChannel(channel);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -112,6 +112,12 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
channelList.AddChannel(createRandomPrivateChannel());
|
channelList.AddChannel(createRandomPrivateChannel());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddStep("Add Announce Channels", () =>
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
channelList.AddChannel(createRandomAnnounceChannel());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -170,5 +176,16 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Username = $"test user {id}",
|
Username = $"test user {id}",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Channel createRandomAnnounceChannel()
|
||||||
|
{
|
||||||
|
int id = RNG.Next(0, 10000);
|
||||||
|
return new Channel
|
||||||
|
{
|
||||||
|
Name = $"Announce {id}",
|
||||||
|
Type = ChannelType.Announce,
|
||||||
|
Id = id,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,129 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osu.Game.Overlays.Chat.Tabs;
|
|
||||||
using osuTK.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
|
||||||
{
|
|
||||||
public class TestSceneChannelTabControl : OsuTestScene
|
|
||||||
{
|
|
||||||
private readonly TestTabControl channelTabControl;
|
|
||||||
|
|
||||||
public TestSceneChannelTabControl()
|
|
||||||
{
|
|
||||||
SpriteText currentText;
|
|
||||||
Add(new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
channelTabControl = new TestTabControl
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Height = 50
|
|
||||||
},
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
Colour = Color4.Black.Opacity(0.1f),
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Height = 50,
|
|
||||||
Depth = -1,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Add(new Container
|
|
||||||
{
|
|
||||||
Origin = Anchor.TopLeft,
|
|
||||||
Anchor = Anchor.TopLeft,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
currentText = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Text = "Currently selected channel:"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel);
|
|
||||||
channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.NewValue;
|
|
||||||
|
|
||||||
AddStep("Add random private channel", addRandomPrivateChannel);
|
|
||||||
AddAssert("There is only one channels", () => channelTabControl.Items.Count == 2);
|
|
||||||
AddRepeatStep("Add 3 random private channels", addRandomPrivateChannel, 3);
|
|
||||||
AddAssert("There are four channels", () => channelTabControl.Items.Count == 5);
|
|
||||||
AddStep("Add random public channel", () => addChannel(RNG.Next().ToString()));
|
|
||||||
|
|
||||||
AddRepeatStep("Select a random channel", () =>
|
|
||||||
{
|
|
||||||
List<Channel> validChannels = channelTabControl.Items.Where(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel)).ToList();
|
|
||||||
channelTabControl.SelectChannel(validChannels[RNG.Next(0, validChannels.Count)]);
|
|
||||||
}, 20);
|
|
||||||
|
|
||||||
Channel channelBefore = null;
|
|
||||||
AddStep("set first channel", () => channelTabControl.SelectChannel(channelBefore = channelTabControl.Items.First(c => !(c is ChannelSelectorTabItem.ChannelSelectorTabChannel))));
|
|
||||||
|
|
||||||
AddStep("select selector tab", () => channelTabControl.SelectChannel(channelTabControl.Items.Single(c => c is ChannelSelectorTabItem.ChannelSelectorTabChannel)));
|
|
||||||
AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value);
|
|
||||||
|
|
||||||
AddAssert("check channel unchanged", () => channelBefore == channelTabControl.Current.Value);
|
|
||||||
|
|
||||||
AddStep("set second channel", () => channelTabControl.SelectChannel(channelTabControl.Items.GetNext(channelBefore)));
|
|
||||||
AddAssert("selector tab is inactive", () => !channelTabControl.ChannelSelectorActive.Value);
|
|
||||||
|
|
||||||
AddUntilStep("remove all channels", () =>
|
|
||||||
{
|
|
||||||
foreach (var item in channelTabControl.Items.ToList())
|
|
||||||
{
|
|
||||||
if (item is ChannelSelectorTabItem.ChannelSelectorTabChannel)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
channelTabControl.RemoveChannel(item);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
AddAssert("selector tab is active", () => channelTabControl.ChannelSelectorActive.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addRandomPrivateChannel() =>
|
|
||||||
channelTabControl.AddChannel(new Channel(new APIUser
|
|
||||||
{
|
|
||||||
Id = RNG.Next(1000, 10000000),
|
|
||||||
Username = "Test User " + RNG.Next(1000)
|
|
||||||
}));
|
|
||||||
|
|
||||||
private void addChannel(string name) =>
|
|
||||||
channelTabControl.AddChannel(new Channel
|
|
||||||
{
|
|
||||||
Type = ChannelType.Public,
|
|
||||||
Name = name
|
|
||||||
});
|
|
||||||
|
|
||||||
private class TestTabControl : ChannelTabControl
|
|
||||||
{
|
|
||||||
public void SelectChannel(Channel channel) => base.SelectTab(TabMap[channel]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,572 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading;
|
|
||||||
using JetBrains.Annotations;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Input;
|
|
||||||
using osu.Framework.Logging;
|
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Online.API;
|
|
||||||
using osu.Game.Online.API.Requests;
|
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osu.Game.Overlays;
|
|
||||||
using osu.Game.Overlays.Chat;
|
|
||||||
using osu.Game.Overlays.Chat.Listing;
|
|
||||||
using osu.Game.Overlays.Chat.ChannelList;
|
|
||||||
using osuTK;
|
|
||||||
using osuTK.Input;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class TestSceneChatOverlayV2 : OsuManualInputManagerTestScene
|
|
||||||
{
|
|
||||||
private TestChatOverlayV2 chatOverlay;
|
|
||||||
private ChannelManager channelManager;
|
|
||||||
|
|
||||||
private APIUser testUser;
|
|
||||||
private Channel testPMChannel;
|
|
||||||
private Channel[] testChannels;
|
|
||||||
|
|
||||||
private Channel testChannel1 => testChannels[0];
|
|
||||||
private Channel testChannel2 => testChannels[1];
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private OsuConfigManager config { get; set; } = null!;
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public void SetUp() => Schedule(() =>
|
|
||||||
{
|
|
||||||
testUser = new APIUser { Username = "test user", Id = 5071479 };
|
|
||||||
testPMChannel = new Channel(testUser);
|
|
||||||
testChannels = Enumerable.Range(1, 10).Select(createPublicChannel).ToArray();
|
|
||||||
|
|
||||||
Child = new DependencyProvidingContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
CachedDependencies = new (Type, object)[]
|
|
||||||
{
|
|
||||||
(typeof(ChannelManager), channelManager = new ChannelManager()),
|
|
||||||
},
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
channelManager,
|
|
||||||
chatOverlay = new TestChatOverlayV2(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
[SetUpSteps]
|
|
||||||
public void SetUpSteps()
|
|
||||||
{
|
|
||||||
AddStep("Setup request handler", () =>
|
|
||||||
{
|
|
||||||
((DummyAPIAccess)API).HandleRequest = req =>
|
|
||||||
{
|
|
||||||
switch (req)
|
|
||||||
{
|
|
||||||
case GetUpdatesRequest getUpdates:
|
|
||||||
getUpdates.TriggerFailure(new WebException());
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case JoinChannelRequest joinChannel:
|
|
||||||
joinChannel.TriggerSuccess();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case LeaveChannelRequest leaveChannel:
|
|
||||||
leaveChannel.TriggerSuccess();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case GetMessagesRequest getMessages:
|
|
||||||
getMessages.TriggerSuccess(createChannelMessages(getMessages.Channel));
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case GetUserRequest getUser:
|
|
||||||
if (getUser.Lookup == testUser.Username)
|
|
||||||
getUser.TriggerSuccess(testUser);
|
|
||||||
else
|
|
||||||
getUser.TriggerFailure(new WebException());
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case PostMessageRequest postMessage:
|
|
||||||
postMessage.TriggerSuccess(new Message(RNG.Next(0, 10000000))
|
|
||||||
{
|
|
||||||
Content = postMessage.Message.Content,
|
|
||||||
ChannelId = postMessage.Message.ChannelId,
|
|
||||||
Sender = postMessage.Message.Sender,
|
|
||||||
Timestamp = new DateTimeOffset(DateTime.Now),
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Logger.Log($"Unhandled Request Type: {req.GetType()}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
AddStep("Add test channels", () =>
|
|
||||||
{
|
|
||||||
(channelManager.AvailableChannels as BindableList<Channel>)?.AddRange(testChannels);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestBasic()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay with channel", () =>
|
|
||||||
{
|
|
||||||
chatOverlay.Show();
|
|
||||||
Channel joinedChannel = channelManager.JoinChannel(testChannel1);
|
|
||||||
channelManager.CurrentChannel.Value = joinedChannel;
|
|
||||||
});
|
|
||||||
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
|
|
||||||
AddUntilStep("Channel is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestShowHide()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddAssert("Overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
|
|
||||||
AddStep("Hide overlay", () => chatOverlay.Hide());
|
|
||||||
AddAssert("Overlay is hidden", () => chatOverlay.State.Value == Visibility.Hidden);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestChatHeight()
|
|
||||||
{
|
|
||||||
BindableFloat configChatHeight = new BindableFloat();
|
|
||||||
config.BindWith(OsuSetting.ChatDisplayHeight, configChatHeight);
|
|
||||||
float newHeight = 0;
|
|
||||||
|
|
||||||
AddStep("Reset config chat height", () => configChatHeight.SetDefault());
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default);
|
|
||||||
AddStep("Click top bar", () =>
|
|
||||||
{
|
|
||||||
InputManager.MoveMouseTo(chatOverlayTopBar);
|
|
||||||
InputManager.PressButton(MouseButton.Left);
|
|
||||||
});
|
|
||||||
AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300)));
|
|
||||||
AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left));
|
|
||||||
AddStep("Store new height", () => newHeight = chatOverlay.Height);
|
|
||||||
AddAssert("Config height changed", () => !configChatHeight.IsDefault && configChatHeight.Value == newHeight);
|
|
||||||
AddStep("Hide overlay", () => chatOverlay.Hide());
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestChannelSelection()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddAssert("Listing is visible", () => listingIsVisible);
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestSearchInListing()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddAssert("Listing is visible", () => listingIsVisible);
|
|
||||||
AddStep("Search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
|
|
||||||
AddUntilStep("Only channel 2 visibile", () =>
|
|
||||||
{
|
|
||||||
IEnumerable<ChannelListingItem> listingItems = chatOverlay.ChildrenOfType<ChannelListingItem>()
|
|
||||||
.Where(item => item.IsPresent);
|
|
||||||
return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestChannelCloseButton()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddStep("Join PM and public channels", () =>
|
|
||||||
{
|
|
||||||
channelManager.JoinChannel(testChannel1);
|
|
||||||
channelManager.JoinChannel(testPMChannel);
|
|
||||||
});
|
|
||||||
AddStep("Select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
|
|
||||||
AddStep("Click close button", () =>
|
|
||||||
{
|
|
||||||
ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType<ChannelListItemCloseButton>().Single();
|
|
||||||
clickDrawable(closeButton);
|
|
||||||
});
|
|
||||||
AddAssert("PM channel closed", () => !channelManager.JoinedChannels.Contains(testPMChannel));
|
|
||||||
AddStep("Select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddStep("Click close button", () =>
|
|
||||||
{
|
|
||||||
ChannelListItemCloseButton closeButton = getChannelListItem(testChannel1).ChildrenOfType<ChannelListItemCloseButton>().Single();
|
|
||||||
clickDrawable(closeButton);
|
|
||||||
});
|
|
||||||
AddAssert("Normal channel closed", () => !channelManager.JoinedChannels.Contains(testChannel1));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestChatCommand()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
|
|
||||||
AddAssert("PM channel is selected", () =>
|
|
||||||
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
|
|
||||||
AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat user_doesnt_exist"));
|
|
||||||
AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage);
|
|
||||||
|
|
||||||
// Make sure no unnecessary requests are made when the PM channel is already open.
|
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null);
|
|
||||||
AddStep("Open chat with user", () => channelManager.PostCommand($"chat {testUser.Username}"));
|
|
||||||
AddAssert("PM channel is selected", () =>
|
|
||||||
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single() == testUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestMultiplayerChannelIsNotShown()
|
|
||||||
{
|
|
||||||
Channel multiplayerChannel = null;
|
|
||||||
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddStep("Join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
|
|
||||||
{
|
|
||||||
Name = "#mp_1",
|
|
||||||
Type = ChannelType.Multiplayer,
|
|
||||||
}));
|
|
||||||
AddAssert("Channel is joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
|
|
||||||
AddUntilStep("Channel not present in listing", () => !chatOverlay.ChildrenOfType<ChannelListingItem>()
|
|
||||||
.Where(item => item.IsPresent)
|
|
||||||
.Select(item => item.Channel)
|
|
||||||
.Contains(multiplayerChannel));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestHighlightOnCurrentChannel()
|
|
||||||
{
|
|
||||||
Message message = null;
|
|
||||||
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddStep("Send message in channel 1", () =>
|
|
||||||
{
|
|
||||||
testChannel1.AddNewMessages(message = new Message
|
|
||||||
{
|
|
||||||
ChannelId = testChannel1.Id,
|
|
||||||
Content = "Message to highlight!",
|
|
||||||
Timestamp = DateTimeOffset.Now,
|
|
||||||
Sender = testUser,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
|
|
||||||
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestHighlightOnAnotherChannel()
|
|
||||||
{
|
|
||||||
Message message = null;
|
|
||||||
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
|
||||||
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
|
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddStep("Send message in channel 2", () =>
|
|
||||||
{
|
|
||||||
testChannel2.AddNewMessages(message = new Message
|
|
||||||
{
|
|
||||||
ChannelId = testChannel2.Id,
|
|
||||||
Content = "Message to highlight!",
|
|
||||||
Timestamp = DateTimeOffset.Now,
|
|
||||||
Sender = testUser,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
|
|
||||||
AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestHighlightOnLeftChannel()
|
|
||||||
{
|
|
||||||
Message message = null;
|
|
||||||
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
|
||||||
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
|
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddStep("Send message in channel 2", () =>
|
|
||||||
{
|
|
||||||
testChannel2.AddNewMessages(message = new Message
|
|
||||||
{
|
|
||||||
ChannelId = testChannel2.Id,
|
|
||||||
Content = "Message to highlight!",
|
|
||||||
Timestamp = DateTimeOffset.Now,
|
|
||||||
Sender = testUser,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
AddStep("Leave channel 2", () => channelManager.LeaveChannel(testChannel2));
|
|
||||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel2));
|
|
||||||
AddUntilStep("Channel 2 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestHighlightWhileChatNeverOpen()
|
|
||||||
{
|
|
||||||
Message message = null;
|
|
||||||
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
|
||||||
AddStep("Send message in channel 1", () =>
|
|
||||||
{
|
|
||||||
testChannel1.AddNewMessages(message = new Message
|
|
||||||
{
|
|
||||||
ChannelId = testChannel1.Id,
|
|
||||||
Content = "Message to highlight!",
|
|
||||||
Timestamp = DateTimeOffset.Now,
|
|
||||||
Sender = testUser,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
|
|
||||||
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestHighlightWithNullChannel()
|
|
||||||
{
|
|
||||||
Message message = null;
|
|
||||||
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
|
||||||
AddStep("Send message in channel 1", () =>
|
|
||||||
{
|
|
||||||
testChannel1.AddNewMessages(message = new Message
|
|
||||||
{
|
|
||||||
ChannelId = testChannel1.Id,
|
|
||||||
Content = "Message to highlight!",
|
|
||||||
Timestamp = DateTimeOffset.Now,
|
|
||||||
Sender = testUser,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
AddStep("Set null channel", () => channelManager.CurrentChannel.Value = null);
|
|
||||||
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, testChannel1));
|
|
||||||
AddUntilStep("Channel 1 is visible", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestTextBoxRetainsFocus()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay", () => chatOverlay.Show());
|
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
|
||||||
AddStep("Click drawable channel", () => clickDrawable(currentDrawableChannel));
|
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
|
||||||
AddStep("Click selector", () => clickDrawable(channelSelectorButton));
|
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
|
||||||
AddStep("Click listing", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelListing>().Single()));
|
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
|
||||||
AddStep("Click channel list", () => clickDrawable(chatOverlay.ChildrenOfType<ChannelList>().Single()));
|
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
|
||||||
AddStep("Click top bar", () => clickDrawable(chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single()));
|
|
||||||
AddAssert("TextBox is focused", () => InputManager.FocusedDrawable == chatOverlayTextBox);
|
|
||||||
AddStep("Hide overlay", () => chatOverlay.Hide());
|
|
||||||
AddAssert("TextBox is not focused", () => InputManager.FocusedDrawable == null);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestSlowLoadingChannel()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay (slow-loading)", () =>
|
|
||||||
{
|
|
||||||
chatOverlay.Show();
|
|
||||||
chatOverlay.SlowLoading = true;
|
|
||||||
});
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
|
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddAssert("Channel 1 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Loading);
|
|
||||||
|
|
||||||
AddStep("Join channel 2", () => channelManager.JoinChannel(testChannel2));
|
|
||||||
AddStep("Select channel 2", () => clickDrawable(getChannelListItem(testChannel2)));
|
|
||||||
AddAssert("Channel 2 loading", () => !channelIsVisible && chatOverlay.GetSlowLoadingChannel(testChannel2).LoadState == LoadState.Loading);
|
|
||||||
|
|
||||||
AddStep("Finish channel 1 load", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadEvent.Set());
|
|
||||||
AddAssert("Channel 1 ready", () => chatOverlay.GetSlowLoadingChannel(testChannel1).LoadState == LoadState.Ready);
|
|
||||||
AddAssert("Channel 1 not displayed", () => !channelIsVisible);
|
|
||||||
|
|
||||||
AddStep("Finish channel 2 load", () => chatOverlay.GetSlowLoadingChannel(testChannel2).LoadEvent.Set());
|
|
||||||
AddAssert("Channel 2 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel2).IsLoaded);
|
|
||||||
AddAssert("Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
|
|
||||||
|
|
||||||
AddStep("Select channel 1", () => clickDrawable(getChannelListItem(testChannel1)));
|
|
||||||
AddAssert("Channel 1 loaded", () => chatOverlay.GetSlowLoadingChannel(testChannel1).IsLoaded);
|
|
||||||
AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestKeyboardCloseAndRestoreChannel()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay with channel 1", () =>
|
|
||||||
{
|
|
||||||
channelManager.JoinChannel(testChannel1);
|
|
||||||
chatOverlay.Show();
|
|
||||||
});
|
|
||||||
AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
|
|
||||||
AddStep("Press document close keys", () => InputManager.Keys(PlatformAction.DocumentClose));
|
|
||||||
AddAssert("Listing is visible", () => listingIsVisible);
|
|
||||||
|
|
||||||
AddStep("Press tab restore keys", () => InputManager.Keys(PlatformAction.TabRestore));
|
|
||||||
AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestKeyboardNewChannel()
|
|
||||||
{
|
|
||||||
AddStep("Show overlay with channel 1", () =>
|
|
||||||
{
|
|
||||||
channelManager.JoinChannel(testChannel1);
|
|
||||||
chatOverlay.Show();
|
|
||||||
});
|
|
||||||
AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
|
|
||||||
AddStep("Press tab new keys", () => InputManager.Keys(PlatformAction.TabNew));
|
|
||||||
AddAssert("Listing is visible", () => listingIsVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestKeyboardNextChannel()
|
|
||||||
{
|
|
||||||
Channel pmChannel1 = createPrivateChannel();
|
|
||||||
Channel pmChannel2 = createPrivateChannel();
|
|
||||||
|
|
||||||
AddStep("Show overlay with channels", () =>
|
|
||||||
{
|
|
||||||
channelManager.JoinChannel(testChannel1);
|
|
||||||
channelManager.JoinChannel(testChannel2);
|
|
||||||
channelManager.JoinChannel(pmChannel1);
|
|
||||||
channelManager.JoinChannel(pmChannel2);
|
|
||||||
chatOverlay.Show();
|
|
||||||
});
|
|
||||||
|
|
||||||
AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
|
|
||||||
AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
|
|
||||||
AddAssert("Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel2);
|
|
||||||
|
|
||||||
AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
|
|
||||||
AddAssert("PM Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == pmChannel1);
|
|
||||||
|
|
||||||
AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
|
|
||||||
AddAssert("PM Channel 2 displayed", () => channelIsVisible && currentDrawableChannel.Channel == pmChannel2);
|
|
||||||
|
|
||||||
AddStep("Press document next keys", () => InputManager.Keys(PlatformAction.DocumentNext));
|
|
||||||
AddAssert("Channel 1 displayed", () => channelIsVisible && currentDrawableChannel.Channel == testChannel1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool listingIsVisible =>
|
|
||||||
chatOverlay.ChildrenOfType<ChannelListing>().Single().State.Value == Visibility.Visible;
|
|
||||||
|
|
||||||
private bool loadingIsVisible =>
|
|
||||||
chatOverlay.ChildrenOfType<LoadingLayer>().Single().State.Value == Visibility.Visible;
|
|
||||||
|
|
||||||
private bool channelIsVisible =>
|
|
||||||
!listingIsVisible && !loadingIsVisible;
|
|
||||||
|
|
||||||
private DrawableChannel currentDrawableChannel =>
|
|
||||||
chatOverlay.ChildrenOfType<DrawableChannel>().Single();
|
|
||||||
|
|
||||||
private ChannelListItem getChannelListItem(Channel channel) =>
|
|
||||||
chatOverlay.ChildrenOfType<ChannelListItem>().Single(item => item.Channel == channel);
|
|
||||||
|
|
||||||
private ChatTextBox chatOverlayTextBox =>
|
|
||||||
chatOverlay.ChildrenOfType<ChatTextBox>().Single();
|
|
||||||
|
|
||||||
private ChatOverlayTopBar chatOverlayTopBar =>
|
|
||||||
chatOverlay.ChildrenOfType<ChatOverlayTopBar>().Single();
|
|
||||||
|
|
||||||
private ChannelListItem channelSelectorButton =>
|
|
||||||
chatOverlay.ChildrenOfType<ChannelListItem>().Single(item => item.Channel is ChannelListing.ChannelListingChannel);
|
|
||||||
|
|
||||||
private void clickDrawable(Drawable d)
|
|
||||||
{
|
|
||||||
InputManager.MoveMouseTo(d);
|
|
||||||
InputManager.Click(MouseButton.Left);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Message> createChannelMessages(Channel channel)
|
|
||||||
{
|
|
||||||
var message = new Message
|
|
||||||
{
|
|
||||||
ChannelId = channel.Id,
|
|
||||||
Content = $"Hello, this is a message in {channel.Name}",
|
|
||||||
Sender = testUser,
|
|
||||||
Timestamp = new DateTimeOffset(DateTime.Now),
|
|
||||||
};
|
|
||||||
return new List<Message> { message };
|
|
||||||
}
|
|
||||||
|
|
||||||
private Channel createPublicChannel(int id) => new Channel
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
Name = $"#channel-{id}",
|
|
||||||
Topic = $"We talk about the number {id} here",
|
|
||||||
Type = ChannelType.Public,
|
|
||||||
};
|
|
||||||
|
|
||||||
private Channel createPrivateChannel()
|
|
||||||
{
|
|
||||||
int id = RNG.Next(0, 10000);
|
|
||||||
return new Channel(new APIUser
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
Username = $"test user {id}",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestChatOverlayV2 : ChatOverlayV2
|
|
||||||
{
|
|
||||||
public bool SlowLoading { get; set; }
|
|
||||||
|
|
||||||
public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType<SlowLoadingDrawableChannel>().Single(c => c.Channel == channel);
|
|
||||||
|
|
||||||
protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel)
|
|
||||||
{
|
|
||||||
return SlowLoading
|
|
||||||
? new SlowLoadingDrawableChannel(newChannel)
|
|
||||||
: new ChatOverlayDrawableChannel(newChannel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel
|
|
||||||
{
|
|
||||||
public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();
|
|
||||||
|
|
||||||
public SlowLoadingDrawableChannel([NotNull] Channel channel)
|
|
||||||
: base(channel)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
LoadEvent.Wait(10000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,6 +11,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Dashboard;
|
using osu.Game.Overlays.Dashboard;
|
||||||
using osu.Game.Tests.Visual.Spectator;
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
@ -42,7 +43,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
CachedDependencies = new (Type, object)[]
|
CachedDependencies = new (Type, object)[]
|
||||||
{
|
{
|
||||||
(typeof(SpectatorClient), spectatorClient),
|
(typeof(SpectatorClient), spectatorClient),
|
||||||
(typeof(UserLookupCache), lookupCache)
|
(typeof(UserLookupCache), lookupCache),
|
||||||
|
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)),
|
||||||
},
|
},
|
||||||
Child = currentlyPlaying = new CurrentlyPlayingDisplay
|
Child = currentlyPlaying = new CurrentlyPlayingDisplay
|
||||||
{
|
{
|
||||||
|
@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
};
|
};
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
public ChatOverlayV2 ChatOverlay { get; } = new ChatOverlayV2();
|
public ChatOverlay ChatOverlay { get; } = new ChatOverlay();
|
||||||
|
|
||||||
private readonly MessageNotifier messageNotifier = new MessageNotifier();
|
private readonly MessageNotifier messageNotifier = new MessageNotifier();
|
||||||
|
|
||||||
|
@ -173,6 +173,8 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
AddUntilStep("wait for scores loaded", () =>
|
AddUntilStep("wait for scores loaded", () =>
|
||||||
requestComplete
|
requestComplete
|
||||||
|
// request handler may need to fire more than once to get scores.
|
||||||
|
&& totalCount > 0
|
||||||
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
|
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
|
||||||
&& resultsScreen.ScorePanelList.AllPanelsVisible);
|
&& resultsScreen.ScorePanelList.AllPanelsVisible);
|
||||||
AddWaitStep("wait for display", 5);
|
AddWaitStep("wait for display", 5);
|
||||||
|
@ -6,6 +6,7 @@ using NUnit.Framework;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
AddStep("clear label", () => textBox.LabelText = default);
|
AddStep("clear label", () => textBox.LabelText = default);
|
||||||
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
||||||
|
|
||||||
AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...");
|
AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true));
|
||||||
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,16 +130,18 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
SettingsNumberBox numberBox = null;
|
SettingsNumberBox numberBox = null;
|
||||||
|
|
||||||
AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox());
|
AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox());
|
||||||
AddAssert("warning text not created", () => !numberBox.ChildrenOfType<SettingsNoticeText>().Any());
|
AddAssert("warning text not created", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
|
||||||
|
|
||||||
AddStep("set warning text", () => numberBox.WarningText = "this is a warning!");
|
AddStep("set warning text", () => numberBox.SetNoticeText("this is a warning!", true));
|
||||||
AddAssert("warning text created", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
|
AddAssert("warning text created", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
|
||||||
|
|
||||||
AddStep("unset warning text", () => numberBox.WarningText = default);
|
AddStep("unset warning text", () => numberBox.ClearNoticeText());
|
||||||
AddAssert("warning text hidden", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 0);
|
AddAssert("warning text hidden", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
|
||||||
|
|
||||||
AddStep("set warning text again", () => numberBox.WarningText = "another warning!");
|
AddStep("set warning text again", () => numberBox.SetNoticeText("another warning!", true));
|
||||||
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
|
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
|
||||||
|
|
||||||
|
AddStep("set non warning text", () => numberBox.SetNoticeText("you did good!"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Drawables
|
|||||||
downloadTrackers.Add(beatmapDownloadTracker);
|
downloadTrackers.Add(beatmapDownloadTracker);
|
||||||
AddInternal(beatmapDownloadTracker);
|
AddInternal(beatmapDownloadTracker);
|
||||||
|
|
||||||
|
// Note that this is downloading the beatmaps even if they are already downloaded.
|
||||||
|
// We could rely more on `BeatmapDownloadTracker`'s exposed state to avoid this.
|
||||||
beatmapDownloader.Download(beatmapSet);
|
beatmapDownloader.Download(beatmapSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Configuration
|
|||||||
|
|
||||||
SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation);
|
SetDefault(OsuSetting.RandomSelectAlgorithm, RandomSelectAlgorithm.RandomPermutation);
|
||||||
|
|
||||||
SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlayV2.DEFAULT_HEIGHT, 0.2f, 1f);
|
SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f);
|
||||||
|
|
||||||
SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal);
|
SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal);
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void performLookup()
|
private async Task performLookup()
|
||||||
{
|
{
|
||||||
// contains at most 50 unique IDs from tasks, which is used to perform the lookup.
|
// contains at most 50 unique IDs from tasks, which is used to perform the lookup.
|
||||||
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>();
|
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>();
|
||||||
@ -127,7 +127,7 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
// rather than queueing, we maintain our own single-threaded request stream.
|
// rather than queueing, we maintain our own single-threaded request stream.
|
||||||
// todo: we probably want retry logic here.
|
// todo: we probably want retry logic here.
|
||||||
api.Perform(request);
|
await api.PerformAsync(request).ConfigureAwait(false);
|
||||||
|
|
||||||
finishPendingTask();
|
finishPendingTask();
|
||||||
|
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.Toolkit.HighPerformance;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using SharpCompress.Archives.Zip;
|
using SharpCompress.Archives.Zip;
|
||||||
|
using SixLabors.ImageSharp.Memory;
|
||||||
|
|
||||||
namespace osu.Game.IO.Archives
|
namespace osu.Game.IO.Archives
|
||||||
{
|
{
|
||||||
@ -27,15 +31,12 @@ namespace osu.Game.IO.Archives
|
|||||||
if (entry == null)
|
if (entry == null)
|
||||||
throw new FileNotFoundException();
|
throw new FileNotFoundException();
|
||||||
|
|
||||||
// allow seeking
|
var owner = MemoryAllocator.Default.Allocate<byte>((int)entry.Size);
|
||||||
MemoryStream copy = new MemoryStream();
|
|
||||||
|
|
||||||
using (Stream s = entry.OpenEntryStream())
|
using (Stream s = entry.OpenEntryStream())
|
||||||
s.CopyTo(copy);
|
s.ReadToFill(owner.Memory.Span);
|
||||||
|
|
||||||
copy.Position = 0;
|
return new MemoryOwnerMemoryStream(owner);
|
||||||
|
|
||||||
return copy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
@ -45,5 +46,48 @@ namespace osu.Game.IO.Archives
|
|||||||
}
|
}
|
||||||
|
|
||||||
public override IEnumerable<string> Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
|
public override IEnumerable<string> Filenames => archive.Entries.Select(e => e.Key).ExcludeSystemFileNames();
|
||||||
|
|
||||||
|
private class MemoryOwnerMemoryStream : Stream
|
||||||
|
{
|
||||||
|
private readonly IMemoryOwner<byte> owner;
|
||||||
|
private readonly Stream stream;
|
||||||
|
|
||||||
|
public MemoryOwnerMemoryStream(IMemoryOwner<byte> owner)
|
||||||
|
{
|
||||||
|
this.owner = owner;
|
||||||
|
|
||||||
|
stream = owner.Memory.AsStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
owner?.Dispose();
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush() => stream.Flush();
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count) => stream.Read(buffer, offset, count);
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin) => stream.Seek(offset, origin);
|
||||||
|
|
||||||
|
public override void SetLength(long value) => stream.SetLength(value);
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count) => stream.Write(buffer, offset, count);
|
||||||
|
|
||||||
|
public override bool CanRead => stream.CanRead;
|
||||||
|
|
||||||
|
public override bool CanSeek => stream.CanSeek;
|
||||||
|
|
||||||
|
public override bool CanWrite => stream.CanWrite;
|
||||||
|
|
||||||
|
public override long Length => stream.Length;
|
||||||
|
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get => stream.Position;
|
||||||
|
set => stream.Position = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
29
osu.Game/Localisation/LayoutSettingsStrings.cs
Normal file
29
osu.Game/Localisation/LayoutSettingsStrings.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
|
||||||
|
namespace osu.Game.Localisation
|
||||||
|
{
|
||||||
|
public static class LayoutSettingsStrings
|
||||||
|
{
|
||||||
|
private const string prefix = @"osu.Game.Resources.Localisation.LayoutSettings";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Checking for fullscreen capabilities..."
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString CheckingForFullscreenCapabilities => new TranslatableString(getKey(@"checking_for_fullscreen_capabilities"), @"Checking for fullscreen capabilities...");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "osu! is running exclusive fullscreen, guaranteeing low latency!"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString OsuIsRunningExclusiveFullscreen => new TranslatableString(getKey(@"osu_is_running_exclusive_fullscreen"), @"osu! is running exclusive fullscreen, guaranteeing low latency!");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Unable to run exclusive fullscreen. You'll still experience some input latency."
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString UnableToRunExclusiveFullscreen => new TranslatableString(getKey(@"unable_to_run_exclusive_fullscreen"), @"Unable to run exclusive fullscreen. You'll still experience some input latency.");
|
||||||
|
|
||||||
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
|
}
|
||||||
|
}
|
@ -63,12 +63,13 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
public virtual void Queue(APIRequest request)
|
public virtual void Queue(APIRequest request)
|
||||||
{
|
{
|
||||||
if (HandleRequest?.Invoke(request) != true)
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
// this will fail due to not receiving an APIAccess, and trigger a failure on the request.
|
if (HandleRequest?.Invoke(request) != true)
|
||||||
// this is intended - any request in testing that needs non-failures should use HandleRequest.
|
{
|
||||||
request.Perform(this);
|
request.Fail(new InvalidOperationException($@"{nameof(DummyAPIAccess)} cannot process this request."));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
|
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
|
||||||
|
@ -15,7 +15,6 @@ using osu.Game.Online.API;
|
|||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays.Chat.Listing;
|
using osu.Game.Overlays.Chat.Listing;
|
||||||
using osu.Game.Overlays.Chat.Tabs;
|
|
||||||
|
|
||||||
namespace osu.Game.Online.Chat
|
namespace osu.Game.Online.Chat
|
||||||
{
|
{
|
||||||
@ -134,7 +133,7 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
private void currentChannelChanged(ValueChangedEvent<Channel> e)
|
private void currentChannelChanged(ValueChangedEvent<Channel> e)
|
||||||
{
|
{
|
||||||
bool isSelectorChannel = e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel || e.NewValue is ChannelListing.ChannelListingChannel;
|
bool isSelectorChannel = e.NewValue is ChannelListing.ChannelListingChannel;
|
||||||
|
|
||||||
if (!isSelectorChannel)
|
if (!isSelectorChannel)
|
||||||
JoinChannel(e.NewValue);
|
JoinChannel(e.NewValue);
|
||||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Online.Chat
|
|||||||
private INotificationOverlay notifications { get; set; }
|
private INotificationOverlay notifications { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private ChatOverlayV2 chatOverlay { get; set; }
|
private ChatOverlay chatOverlay { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private ChannelManager channelManager { get; set; }
|
private ChannelManager channelManager { get; set; }
|
||||||
@ -170,7 +170,7 @@ namespace osu.Game.Online.Chat
|
|||||||
public override bool IsImportant => false;
|
public override bool IsImportant => false;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours, ChatOverlayV2 chatOverlay, INotificationOverlay notificationOverlay)
|
private void load(OsuColour colours, ChatOverlay chatOverlay, INotificationOverlay notificationOverlay)
|
||||||
{
|
{
|
||||||
IconBackground.Colour = colours.PurpleDark;
|
IconBackground.Colour = colours.PurpleDark;
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ namespace osu.Game
|
|||||||
|
|
||||||
public Toolbar Toolbar;
|
public Toolbar Toolbar;
|
||||||
|
|
||||||
private ChatOverlayV2 chatOverlay;
|
private ChatOverlay chatOverlay;
|
||||||
|
|
||||||
private ChannelManager channelManager;
|
private ChannelManager channelManager;
|
||||||
|
|
||||||
@ -848,7 +848,7 @@ namespace osu.Game
|
|||||||
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
|
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
|
||||||
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
|
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
|
||||||
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
|
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
|
||||||
loadComponentSingleFile(chatOverlay = new ChatOverlayV2(), overlayContent.Add, true);
|
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
|
||||||
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
|
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
|
||||||
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
|
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
|
||||||
loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true);
|
loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true);
|
||||||
|
@ -26,15 +26,20 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
public Action<Channel>? OnRequestSelect;
|
public Action<Channel>? OnRequestSelect;
|
||||||
public Action<Channel>? OnRequestLeave;
|
public Action<Channel>? OnRequestLeave;
|
||||||
|
|
||||||
public IEnumerable<Channel> Channels => publicChannelFlow.Channels.Concat(privateChannelFlow.Channels);
|
public IEnumerable<Channel> Channels => groupFlow.Children
|
||||||
|
.OfType<ChannelGroup>()
|
||||||
|
.SelectMany(channelGroup => channelGroup.ItemFlow)
|
||||||
|
.Select(item => item.Channel);
|
||||||
|
|
||||||
public readonly ChannelListing.ChannelListingChannel ChannelListingChannel = new ChannelListing.ChannelListingChannel();
|
public readonly ChannelListing.ChannelListingChannel ChannelListingChannel = new ChannelListing.ChannelListingChannel();
|
||||||
|
|
||||||
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
|
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
|
||||||
|
|
||||||
private OsuScrollContainer scroll = null!;
|
private OsuScrollContainer scroll = null!;
|
||||||
private ChannelListItemFlow publicChannelFlow = null!;
|
private FillFlowContainer groupFlow = null!;
|
||||||
private ChannelListItemFlow privateChannelFlow = null!;
|
private ChannelGroup announceChannelGroup = null!;
|
||||||
|
private ChannelGroup publicChannelGroup = null!;
|
||||||
|
private ChannelGroup privateChannelGroup = null!;
|
||||||
private ChannelListItem selector = null!;
|
private ChannelListItem selector = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -49,25 +54,20 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
},
|
},
|
||||||
scroll = new OsuScrollContainer
|
scroll = new OsuScrollContainer
|
||||||
{
|
{
|
||||||
Padding = new MarginPadding { Vertical = 7 },
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
ScrollbarAnchor = Anchor.TopRight,
|
ScrollbarAnchor = Anchor.TopRight,
|
||||||
ScrollDistance = 35f,
|
ScrollDistance = 35f,
|
||||||
Child = new FillFlowContainer
|
Child = groupFlow = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new ChannelListLabel(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()),
|
announceChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitleANNOUNCE.ToUpper()),
|
||||||
publicChannelFlow = new ChannelListItemFlow(),
|
publicChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePUBLIC.ToUpper()),
|
||||||
selector = new ChannelListItem(ChannelListingChannel)
|
selector = new ChannelListItem(ChannelListingChannel),
|
||||||
{
|
privateChannelGroup = new ChannelGroup(ChatStrings.ChannelsListTitlePM.ToUpper()),
|
||||||
Margin = new MarginPadding { Bottom = 10 },
|
|
||||||
},
|
|
||||||
new ChannelListLabel(ChatStrings.ChannelsListTitlePM.ToUpper()),
|
|
||||||
privateChannelFlow = new ChannelListItemFlow(),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -85,9 +85,11 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
|
item.OnRequestSelect += chan => OnRequestSelect?.Invoke(chan);
|
||||||
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
|
item.OnRequestLeave += chan => OnRequestLeave?.Invoke(chan);
|
||||||
|
|
||||||
ChannelListItemFlow flow = getFlowForChannel(channel);
|
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel);
|
||||||
channelMap.Add(channel, item);
|
channelMap.Add(channel, item);
|
||||||
flow.Add(item);
|
flow.Add(item);
|
||||||
|
|
||||||
|
updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveChannel(Channel channel)
|
public void RemoveChannel(Channel channel)
|
||||||
@ -96,10 +98,12 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
ChannelListItem item = channelMap[channel];
|
ChannelListItem item = channelMap[channel];
|
||||||
ChannelListItemFlow flow = getFlowForChannel(channel);
|
FillFlowContainer<ChannelListItem> flow = getFlowForChannel(channel);
|
||||||
|
|
||||||
channelMap.Remove(channel);
|
channelMap.Remove(channel);
|
||||||
flow.Remove(item);
|
flow.Remove(item);
|
||||||
|
|
||||||
|
updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChannelListItem GetItem(Channel channel)
|
public ChannelListItem GetItem(Channel channel)
|
||||||
@ -112,40 +116,58 @@ namespace osu.Game.Overlays.Chat.ChannelList
|
|||||||
|
|
||||||
public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel));
|
public void ScrollChannelIntoView(Channel channel) => scroll.ScrollIntoView(GetItem(channel));
|
||||||
|
|
||||||
private ChannelListItemFlow getFlowForChannel(Channel channel)
|
private FillFlowContainer<ChannelListItem> getFlowForChannel(Channel channel)
|
||||||
{
|
{
|
||||||
switch (channel.Type)
|
switch (channel.Type)
|
||||||
{
|
{
|
||||||
case ChannelType.Public:
|
case ChannelType.Public:
|
||||||
return publicChannelFlow;
|
return publicChannelGroup.ItemFlow;
|
||||||
|
|
||||||
case ChannelType.PM:
|
case ChannelType.PM:
|
||||||
return privateChannelFlow;
|
return privateChannelGroup.ItemFlow;
|
||||||
|
|
||||||
|
case ChannelType.Announce:
|
||||||
|
return announceChannelGroup.ItemFlow;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return publicChannelFlow;
|
return publicChannelGroup.ItemFlow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ChannelListLabel : OsuSpriteText
|
private void updateVisibility()
|
||||||
{
|
{
|
||||||
public ChannelListLabel(LocalisableString label)
|
if (announceChannelGroup.ItemFlow.Children.Count == 0)
|
||||||
{
|
announceChannelGroup.Hide();
|
||||||
Text = label;
|
else
|
||||||
Margin = new MarginPadding { Left = 18, Bottom = 5 };
|
announceChannelGroup.Show();
|
||||||
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ChannelListItemFlow : FillFlowContainer<ChannelListItem>
|
private class ChannelGroup : FillFlowContainer
|
||||||
{
|
{
|
||||||
public IEnumerable<Channel> Channels => Children.Select(c => c.Channel);
|
public readonly FillFlowContainer<ChannelListItem> ItemFlow;
|
||||||
|
|
||||||
public ChannelListItemFlow()
|
public ChannelGroup(LocalisableString label)
|
||||||
{
|
{
|
||||||
Direction = FillDirection.Vertical;
|
Direction = FillDirection.Vertical;
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
|
Padding = new MarginPadding { Top = 8 };
|
||||||
|
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Text = label,
|
||||||
|
Margin = new MarginPadding { Left = 18, Bottom = 5 },
|
||||||
|
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
|
||||||
|
},
|
||||||
|
ItemFlow = new FillFlowContainer<ChannelListItem>
|
||||||
|
{
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,8 +141,8 @@ namespace osu.Game.Overlays.Chat
|
|||||||
|
|
||||||
switch (newChannel?.Type)
|
switch (newChannel?.Type)
|
||||||
{
|
{
|
||||||
case ChannelType.Public:
|
case null:
|
||||||
chattingText.Text = ChatStrings.TalkingIn(newChannel.Name);
|
chattingText.Text = string.Empty;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ChannelType.PM:
|
case ChannelType.PM:
|
||||||
@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Chat
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
chattingText.Text = string.Empty;
|
chattingText.Text = ChatStrings.TalkingIn(newChannel.Name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
@ -1,191 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
|
||||||
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.Events;
|
|
||||||
using osu.Framework.Localisation;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Chat.Selection
|
|
||||||
{
|
|
||||||
public class ChannelListItem : OsuClickableContainer, IFilterable
|
|
||||||
{
|
|
||||||
private const float width_padding = 5;
|
|
||||||
private const float channel_width = 150;
|
|
||||||
private const float text_size = 15;
|
|
||||||
private const float transition_duration = 100;
|
|
||||||
|
|
||||||
public readonly Channel Channel;
|
|
||||||
|
|
||||||
private readonly Bindable<bool> joinedBind = new Bindable<bool>();
|
|
||||||
private readonly OsuSpriteText name;
|
|
||||||
private readonly OsuSpriteText topic;
|
|
||||||
private readonly SpriteIcon joinedCheckmark;
|
|
||||||
|
|
||||||
private Color4 joinedColour;
|
|
||||||
private Color4 topicColour;
|
|
||||||
private Color4 hoverColour;
|
|
||||||
|
|
||||||
public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty };
|
|
||||||
|
|
||||||
public bool MatchingFilter
|
|
||||||
{
|
|
||||||
set => this.FadeTo(value ? 1f : 0f, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool FilteringActive { get; set; }
|
|
||||||
|
|
||||||
public Action<Channel> OnRequestJoin;
|
|
||||||
public Action<Channel> OnRequestLeave;
|
|
||||||
|
|
||||||
public ChannelListItem(Channel channel)
|
|
||||||
{
|
|
||||||
Channel = channel;
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.X;
|
|
||||||
AutoSizeAxes = Axes.Y;
|
|
||||||
|
|
||||||
Action = () => { (channel.Joined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(channel); };
|
|
||||||
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
joinedCheckmark = new SpriteIcon
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopRight,
|
|
||||||
Origin = Anchor.TopRight,
|
|
||||||
Icon = FontAwesome.Solid.CheckCircle,
|
|
||||||
Size = new Vector2(text_size),
|
|
||||||
Shadow = false,
|
|
||||||
Margin = new MarginPadding { Right = 10f },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
Width = channel_width,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
name = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Text = channel.ToString(),
|
|
||||||
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
|
|
||||||
Shadow = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Width = 0.7f,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Margin = new MarginPadding { Left = width_padding },
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
topic = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Text = channel.Topic,
|
|
||||||
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.SemiBold),
|
|
||||||
Shadow = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
Margin = new MarginPadding { Left = width_padding },
|
|
||||||
Spacing = new Vector2(3f, 0f),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new SpriteIcon
|
|
||||||
{
|
|
||||||
Anchor = Anchor.CentreLeft,
|
|
||||||
Origin = Anchor.CentreLeft,
|
|
||||||
Icon = FontAwesome.Solid.User,
|
|
||||||
Size = new Vector2(text_size - 2),
|
|
||||||
Shadow = false,
|
|
||||||
},
|
|
||||||
new OsuSpriteText
|
|
||||||
{
|
|
||||||
Text = @"0",
|
|
||||||
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.SemiBold),
|
|
||||||
Shadow = false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuColour colours)
|
|
||||||
{
|
|
||||||
topicColour = colours.Gray9;
|
|
||||||
joinedColour = colours.Blue;
|
|
||||||
hoverColour = colours.Yellow;
|
|
||||||
|
|
||||||
joinedBind.ValueChanged += joined => updateColour(joined.NewValue);
|
|
||||||
joinedBind.BindTo(Channel.Joined);
|
|
||||||
|
|
||||||
joinedBind.TriggerChange();
|
|
||||||
FinishTransforms(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
|
||||||
{
|
|
||||||
if (!Channel.Joined.Value)
|
|
||||||
name.FadeColour(hoverColour, 50, Easing.OutQuint);
|
|
||||||
|
|
||||||
return base.OnHover(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnHoverLost(HoverLostEvent e)
|
|
||||||
{
|
|
||||||
if (!Channel.Joined.Value)
|
|
||||||
name.FadeColour(Color4.White, transition_duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateColour(bool joined)
|
|
||||||
{
|
|
||||||
if (joined)
|
|
||||||
{
|
|
||||||
name.FadeColour(Color4.White, transition_duration);
|
|
||||||
joinedCheckmark.FadeTo(1f, transition_duration);
|
|
||||||
topic.FadeTo(0.8f, transition_duration);
|
|
||||||
topic.FadeColour(Color4.White, transition_duration);
|
|
||||||
this.FadeColour(joinedColour, transition_duration);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
joinedCheckmark.FadeTo(0f, transition_duration);
|
|
||||||
topic.FadeTo(1f, transition_duration);
|
|
||||||
topic.FadeColour(topicColour, transition_duration);
|
|
||||||
this.FadeColour(Color4.White, transition_duration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Localisation;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Chat.Selection
|
|
||||||
{
|
|
||||||
public class ChannelSection : Container, IHasFilterableChildren
|
|
||||||
{
|
|
||||||
public readonly FillFlowContainer<ChannelListItem> ChannelFlow;
|
|
||||||
|
|
||||||
public IEnumerable<IFilterable> FilterableChildren => ChannelFlow.Children;
|
|
||||||
public IEnumerable<LocalisableString> FilterTerms => Array.Empty<LocalisableString>();
|
|
||||||
|
|
||||||
public bool MatchingFilter
|
|
||||||
{
|
|
||||||
set => this.FadeTo(value ? 1f : 0f, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool FilteringActive { get; set; }
|
|
||||||
|
|
||||||
public IEnumerable<Channel> Channels
|
|
||||||
{
|
|
||||||
set => ChannelFlow.ChildrenEnumerable = value.Select(c => new ChannelListItem(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ChannelSection()
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X;
|
|
||||||
AutoSizeAxes = Axes.Y;
|
|
||||||
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new OsuSpriteText
|
|
||||||
{
|
|
||||||
Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold),
|
|
||||||
Text = "All Channels".ToUpperInvariant()
|
|
||||||
},
|
|
||||||
ChannelFlow = new FillFlowContainer<ChannelListItem>
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Margin = new MarginPadding { Top = 25 },
|
|
||||||
Spacing = new Vector2(0f, 5f),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,194 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Backgrounds;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Chat.Selection
|
|
||||||
{
|
|
||||||
public class ChannelSelectionOverlay : WaveOverlayContainer
|
|
||||||
{
|
|
||||||
public new const float WIDTH_PADDING = 170;
|
|
||||||
|
|
||||||
private const float transition_duration = 500;
|
|
||||||
|
|
||||||
private readonly Box bg;
|
|
||||||
private readonly Triangles triangles;
|
|
||||||
private readonly Box headerBg;
|
|
||||||
private readonly SearchTextBox search;
|
|
||||||
private readonly SearchContainer<ChannelSection> sectionsFlow;
|
|
||||||
|
|
||||||
protected override bool DimMainContent => false;
|
|
||||||
|
|
||||||
public Action<Channel> OnRequestJoin;
|
|
||||||
public Action<Channel> OnRequestLeave;
|
|
||||||
|
|
||||||
public ChannelSelectionOverlay()
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X;
|
|
||||||
|
|
||||||
Waves.FirstWaveColour = Color4Extensions.FromHex("353535");
|
|
||||||
Waves.SecondWaveColour = Color4Extensions.FromHex("434343");
|
|
||||||
Waves.ThirdWaveColour = Color4Extensions.FromHex("515151");
|
|
||||||
Waves.FourthWaveColour = Color4Extensions.FromHex("595959");
|
|
||||||
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Masking = true,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
bg = new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
triangles = new Triangles
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
TriangleScale = 5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Padding = new MarginPadding { Top = 85, Right = WIDTH_PADDING },
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
new OsuScrollContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
sectionsFlow = new SearchContainer<ChannelSection>
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
LayoutDuration = 200,
|
|
||||||
LayoutEasing = Easing.OutQuint,
|
|
||||||
Spacing = new Vector2(0f, 20f),
|
|
||||||
Padding = new MarginPadding { Vertical = 20, Left = WIDTH_PADDING },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
headerBg = new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
Spacing = new Vector2(0f, 10f),
|
|
||||||
Padding = new MarginPadding { Top = 10f, Bottom = 10f, Left = WIDTH_PADDING, Right = WIDTH_PADDING },
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new OsuSpriteText
|
|
||||||
{
|
|
||||||
Text = @"Chat Channels",
|
|
||||||
Font = OsuFont.GetFont(size: 20),
|
|
||||||
Shadow = false,
|
|
||||||
},
|
|
||||||
search = new HeaderSearchTextBox { RelativeSizeAxes = Axes.X },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
search.Current.ValueChanged += term => sectionsFlow.SearchTerm = term.NewValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateAvailableChannels(IEnumerable<Channel> channels)
|
|
||||||
{
|
|
||||||
Scheduler.Add(() =>
|
|
||||||
{
|
|
||||||
sectionsFlow.ChildrenEnumerable = new[]
|
|
||||||
{
|
|
||||||
new ChannelSection { Channels = channels, },
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (ChannelSection s in sectionsFlow.Children)
|
|
||||||
{
|
|
||||||
foreach (ChannelListItem c in s.ChannelFlow.Children)
|
|
||||||
{
|
|
||||||
c.OnRequestJoin = channel => { OnRequestJoin?.Invoke(channel); };
|
|
||||||
c.OnRequestLeave = channel => { OnRequestLeave?.Invoke(channel); };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuColour colours)
|
|
||||||
{
|
|
||||||
bg.Colour = colours.Gray3;
|
|
||||||
triangles.ColourDark = colours.Gray3;
|
|
||||||
triangles.ColourLight = Color4Extensions.FromHex(@"353535");
|
|
||||||
|
|
||||||
headerBg.Colour = colours.Gray2.Opacity(0.75f);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnFocus(FocusEvent e)
|
|
||||||
{
|
|
||||||
search.TakeFocus();
|
|
||||||
base.OnFocus(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void PopIn()
|
|
||||||
{
|
|
||||||
if (Alpha == 0) this.MoveToY(DrawHeight);
|
|
||||||
|
|
||||||
this.FadeIn(transition_duration, Easing.OutQuint);
|
|
||||||
this.MoveToY(0, transition_duration, Easing.OutQuint);
|
|
||||||
|
|
||||||
search.HoldFocus = true;
|
|
||||||
base.PopIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void PopOut()
|
|
||||||
{
|
|
||||||
this.FadeOut(transition_duration, Easing.InSine);
|
|
||||||
this.MoveToY(DrawHeight, transition_duration, Easing.InSine);
|
|
||||||
|
|
||||||
search.HoldFocus = false;
|
|
||||||
base.PopOut();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class HeaderSearchTextBox : BasicSearchTextBox
|
|
||||||
{
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
BackgroundFocused = Color4.Black.Opacity(0.2f);
|
|
||||||
BackgroundUnfocused = Color4.Black.Opacity(0.2f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Chat.Tabs
|
|
||||||
{
|
|
||||||
public class ChannelSelectorTabItem : ChannelTabItem
|
|
||||||
{
|
|
||||||
public override bool IsRemovable => false;
|
|
||||||
|
|
||||||
public override bool IsSwitchable => false;
|
|
||||||
|
|
||||||
protected override bool IsBoldWhenActive => false;
|
|
||||||
|
|
||||||
public ChannelSelectorTabItem()
|
|
||||||
: base(new ChannelSelectorTabChannel())
|
|
||||||
{
|
|
||||||
Depth = float.MaxValue;
|
|
||||||
Width = 45;
|
|
||||||
|
|
||||||
Icon.Alpha = 0;
|
|
||||||
|
|
||||||
Text.Font = Text.Font.With(size: 45);
|
|
||||||
Text.Truncate = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuColour colour)
|
|
||||||
{
|
|
||||||
BackgroundInactive = colour.Gray2;
|
|
||||||
BackgroundActive = colour.Gray3;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ChannelSelectorTabChannel : Channel
|
|
||||||
{
|
|
||||||
public ChannelSelectorTabChannel()
|
|
||||||
{
|
|
||||||
Name = "+";
|
|
||||||
Type = ChannelType.System;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.UserInterface;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osuTK;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Chat.Tabs
|
|
||||||
{
|
|
||||||
public class ChannelTabControl : OsuTabControl<Channel>
|
|
||||||
{
|
|
||||||
public const float SHEAR_WIDTH = 10;
|
|
||||||
|
|
||||||
public Action<Channel> OnRequestLeave;
|
|
||||||
|
|
||||||
public readonly Bindable<bool> ChannelSelectorActive = new Bindable<bool>();
|
|
||||||
|
|
||||||
private readonly ChannelSelectorTabItem selectorTab;
|
|
||||||
|
|
||||||
public ChannelTabControl()
|
|
||||||
{
|
|
||||||
Padding = new MarginPadding { Left = 50 };
|
|
||||||
|
|
||||||
TabContainer.Spacing = new Vector2(-SHEAR_WIDTH, 0);
|
|
||||||
TabContainer.Masking = false;
|
|
||||||
|
|
||||||
AddTabItem(selectorTab = new ChannelSelectorTabItem());
|
|
||||||
|
|
||||||
ChannelSelectorActive.BindTo(selectorTab.Active);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void AddTabItem(TabItem<Channel> item, bool addToDropdown = true)
|
|
||||||
{
|
|
||||||
if (item != selectorTab && TabContainer.GetLayoutPosition(selectorTab) < float.MaxValue)
|
|
||||||
// performTabSort might've made selectorTab's position wonky, fix it
|
|
||||||
TabContainer.SetLayoutPosition(selectorTab, float.MaxValue);
|
|
||||||
|
|
||||||
((ChannelTabItem)item).OnRequestClose += channelItem => OnRequestLeave?.Invoke(channelItem.Value);
|
|
||||||
|
|
||||||
base.AddTabItem(item, addToDropdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override TabItem<Channel> CreateTabItem(Channel value)
|
|
||||||
{
|
|
||||||
switch (value.Type)
|
|
||||||
{
|
|
||||||
default:
|
|
||||||
return new ChannelTabItem(value);
|
|
||||||
|
|
||||||
case ChannelType.PM:
|
|
||||||
return new PrivateChannelTabItem(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a channel to the ChannelTabControl.
|
|
||||||
/// The first channel added will automaticly selected.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel that is going to be added.</param>
|
|
||||||
public void AddChannel(Channel channel)
|
|
||||||
{
|
|
||||||
if (!Items.Contains(channel))
|
|
||||||
AddItem(channel);
|
|
||||||
|
|
||||||
Current.Value ??= channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes a channel from the ChannelTabControl.
|
|
||||||
/// If the selected channel is the one that is being removed, the next available channel will be selected.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel that is going to be removed.</param>
|
|
||||||
public void RemoveChannel(Channel channel)
|
|
||||||
{
|
|
||||||
RemoveItem(channel);
|
|
||||||
|
|
||||||
if (SelectedTab == null)
|
|
||||||
SelectChannelSelectorTab();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SelectChannelSelectorTab() => SelectTab(selectorTab);
|
|
||||||
|
|
||||||
protected override void SelectTab(TabItem<Channel> tab)
|
|
||||||
{
|
|
||||||
if (tab is ChannelSelectorTabItem)
|
|
||||||
{
|
|
||||||
tab.Active.Value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
base.SelectTab(tab);
|
|
||||||
selectorTab.Active.Value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override TabFillFlowContainer CreateTabFlow() => new ChannelTabFillFlowContainer
|
|
||||||
{
|
|
||||||
Direction = FillDirection.Full,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Depth = -1,
|
|
||||||
Masking = true
|
|
||||||
};
|
|
||||||
|
|
||||||
private class ChannelTabFillFlowContainer : TabFillFlowContainer
|
|
||||||
{
|
|
||||||
protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,238 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Audio;
|
|
||||||
using osu.Framework.Audio.Sample;
|
|
||||||
using osu.Framework.Extensions;
|
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Effects;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Framework.Graphics.UserInterface;
|
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
|
||||||
using osuTK.Input;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Chat.Tabs
|
|
||||||
{
|
|
||||||
public class ChannelTabItem : TabItem<Channel>
|
|
||||||
{
|
|
||||||
protected Color4 BackgroundInactive;
|
|
||||||
private Color4 backgroundHover;
|
|
||||||
protected Color4 BackgroundActive;
|
|
||||||
|
|
||||||
public override bool IsRemovable => !Pinned;
|
|
||||||
|
|
||||||
protected readonly SpriteText Text;
|
|
||||||
protected readonly ClickableContainer CloseButton;
|
|
||||||
private readonly Box box;
|
|
||||||
private readonly Box highlightBox;
|
|
||||||
protected readonly SpriteIcon Icon;
|
|
||||||
|
|
||||||
public Action<ChannelTabItem> OnRequestClose;
|
|
||||||
private readonly Container content;
|
|
||||||
|
|
||||||
protected override Container<Drawable> Content => content;
|
|
||||||
|
|
||||||
private Sample sampleTabSwitched;
|
|
||||||
|
|
||||||
public ChannelTabItem(Channel value)
|
|
||||||
: base(value)
|
|
||||||
{
|
|
||||||
Width = 150;
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Y;
|
|
||||||
|
|
||||||
Anchor = Anchor.BottomLeft;
|
|
||||||
Origin = Anchor.BottomLeft;
|
|
||||||
|
|
||||||
Shear = new Vector2(ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0);
|
|
||||||
|
|
||||||
Masking = true;
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
|
||||||
{
|
|
||||||
box = new Box
|
|
||||||
{
|
|
||||||
EdgeSmoothness = new Vector2(1, 0),
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
highlightBox = new Box
|
|
||||||
{
|
|
||||||
Width = 5,
|
|
||||||
Alpha = 0,
|
|
||||||
Anchor = Anchor.BottomRight,
|
|
||||||
Origin = Anchor.BottomRight,
|
|
||||||
EdgeSmoothness = new Vector2(1, 0),
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
},
|
|
||||||
content = new Container
|
|
||||||
{
|
|
||||||
Shear = new Vector2(-ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0),
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
Icon = new SpriteIcon
|
|
||||||
{
|
|
||||||
Icon = DisplayIcon,
|
|
||||||
Anchor = Anchor.CentreLeft,
|
|
||||||
Origin = Anchor.CentreLeft,
|
|
||||||
Colour = Color4.Black,
|
|
||||||
X = -10,
|
|
||||||
Alpha = 0.2f,
|
|
||||||
Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT),
|
|
||||||
},
|
|
||||||
Text = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Origin = Anchor.CentreLeft,
|
|
||||||
Anchor = Anchor.CentreLeft,
|
|
||||||
Text = value.ToString(),
|
|
||||||
Font = OsuFont.GetFont(size: 18),
|
|
||||||
Padding = new MarginPadding(5)
|
|
||||||
{
|
|
||||||
Left = LeftTextPadding,
|
|
||||||
Right = RightTextPadding,
|
|
||||||
},
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Truncate = true,
|
|
||||||
},
|
|
||||||
CloseButton = new TabCloseButton
|
|
||||||
{
|
|
||||||
Alpha = 0,
|
|
||||||
Margin = new MarginPadding { Right = 20 },
|
|
||||||
Origin = Anchor.CentreRight,
|
|
||||||
Anchor = Anchor.CentreRight,
|
|
||||||
Action = delegate
|
|
||||||
{
|
|
||||||
if (IsRemovable) OnRequestClose?.Invoke(this);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
new HoverSounds()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual float LeftTextPadding => 5;
|
|
||||||
|
|
||||||
protected virtual float RightTextPadding => IsRemovable ? 40 : 5;
|
|
||||||
|
|
||||||
protected virtual IconUsage DisplayIcon => FontAwesome.Solid.Hashtag;
|
|
||||||
|
|
||||||
protected virtual bool ShowCloseOnHover => true;
|
|
||||||
|
|
||||||
protected virtual bool IsBoldWhenActive => true;
|
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
|
||||||
{
|
|
||||||
if (IsRemovable && ShowCloseOnHover)
|
|
||||||
CloseButton.FadeIn(200, Easing.OutQuint);
|
|
||||||
|
|
||||||
if (!Active.Value)
|
|
||||||
box.FadeColour(backgroundHover, TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnHoverLost(HoverLostEvent e)
|
|
||||||
{
|
|
||||||
CloseButton.FadeOut(200, Easing.OutQuint);
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnMouseUp(MouseUpEvent e)
|
|
||||||
{
|
|
||||||
switch (e.Button)
|
|
||||||
{
|
|
||||||
case MouseButton.Middle:
|
|
||||||
CloseButton.TriggerClick();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuColour colours, AudioManager audio)
|
|
||||||
{
|
|
||||||
BackgroundActive = colours.ChatBlue;
|
|
||||||
BackgroundInactive = colours.Gray4;
|
|
||||||
backgroundHover = colours.Gray7;
|
|
||||||
sampleTabSwitched = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
|
|
||||||
|
|
||||||
highlightBox.Colour = colours.Yellow;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
updateState();
|
|
||||||
FinishTransforms(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateState()
|
|
||||||
{
|
|
||||||
if (Active.Value)
|
|
||||||
FadeActive();
|
|
||||||
else
|
|
||||||
FadeInactive();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected const float TRANSITION_LENGTH = 400;
|
|
||||||
|
|
||||||
private readonly EdgeEffectParameters activateEdgeEffect = new EdgeEffectParameters
|
|
||||||
{
|
|
||||||
Type = EdgeEffectType.Shadow,
|
|
||||||
Radius = 15,
|
|
||||||
Colour = Color4.Black.Opacity(0.4f),
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly EdgeEffectParameters deactivateEdgeEffect = new EdgeEffectParameters
|
|
||||||
{
|
|
||||||
Type = EdgeEffectType.Shadow,
|
|
||||||
Radius = 10,
|
|
||||||
Colour = Color4.Black.Opacity(0.2f),
|
|
||||||
};
|
|
||||||
|
|
||||||
protected virtual void FadeActive()
|
|
||||||
{
|
|
||||||
this.ResizeHeightTo(1.1f, TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
|
|
||||||
TweenEdgeEffectTo(activateEdgeEffect, TRANSITION_LENGTH);
|
|
||||||
|
|
||||||
box.FadeColour(BackgroundActive, TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
highlightBox.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
|
|
||||||
if (IsBoldWhenActive) Text.Font = Text.Font.With(weight: FontWeight.Bold);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void FadeInactive()
|
|
||||||
{
|
|
||||||
this.ResizeHeightTo(1, TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
|
|
||||||
TweenEdgeEffectTo(deactivateEdgeEffect, TRANSITION_LENGTH);
|
|
||||||
|
|
||||||
box.FadeColour(IsHovered ? backgroundHover : BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
|
|
||||||
Text.Font = Text.Font.With(weight: FontWeight.Medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnActivated()
|
|
||||||
{
|
|
||||||
if (IsLoaded)
|
|
||||||
sampleTabSwitched?.Play();
|
|
||||||
|
|
||||||
updateState();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDeactivated() => updateState();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osu.Game.Users.Drawables;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Chat.Tabs
|
|
||||||
{
|
|
||||||
public class PrivateChannelTabItem : ChannelTabItem
|
|
||||||
{
|
|
||||||
protected override IconUsage DisplayIcon => FontAwesome.Solid.At;
|
|
||||||
|
|
||||||
public PrivateChannelTabItem(Channel value)
|
|
||||||
: base(value)
|
|
||||||
{
|
|
||||||
if (value.Type != ChannelType.PM)
|
|
||||||
throw new ArgumentException("Argument value needs to have the targettype user!");
|
|
||||||
|
|
||||||
DrawableAvatar avatar;
|
|
||||||
|
|
||||||
AddRange(new Drawable[]
|
|
||||||
{
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
AutoSizeAxes = Axes.X,
|
|
||||||
Margin = new MarginPadding
|
|
||||||
{
|
|
||||||
Horizontal = 3
|
|
||||||
},
|
|
||||||
Origin = Anchor.BottomLeft,
|
|
||||||
Anchor = Anchor.BottomLeft,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new CircularContainer
|
|
||||||
{
|
|
||||||
Scale = new Vector2(0.95f),
|
|
||||||
Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT),
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Masking = true,
|
|
||||||
Child = new DelayedLoadWrapper(avatar = new DrawableAvatar(value.Users.First())
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both
|
|
||||||
})
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
avatar.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override float LeftTextPadding => base.LeftTextPadding + ChatOverlay.TAB_AREA_HEIGHT;
|
|
||||||
|
|
||||||
protected override bool ShowCloseOnHover => false;
|
|
||||||
|
|
||||||
protected override void FadeActive()
|
|
||||||
{
|
|
||||||
base.FadeActive();
|
|
||||||
|
|
||||||
this.ResizeWidthTo(200, TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
CloseButton.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void FadeInactive()
|
|
||||||
{
|
|
||||||
base.FadeInactive();
|
|
||||||
|
|
||||||
this.ResizeWidthTo(ChatOverlay.TAB_AREA_HEIGHT + 10, TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
CloseButton.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuColour colours)
|
|
||||||
{
|
|
||||||
var user = Value.Users.First();
|
|
||||||
|
|
||||||
BackgroundActive = user.Colour != null ? Color4Extensions.FromHex(user.Colour) : colours.BlueDark;
|
|
||||||
BackgroundInactive = BackgroundActive.Darken(0.5f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Chat.Tabs
|
|
||||||
{
|
|
||||||
public class TabCloseButton : OsuClickableContainer
|
|
||||||
{
|
|
||||||
private readonly SpriteIcon icon;
|
|
||||||
|
|
||||||
public TabCloseButton()
|
|
||||||
{
|
|
||||||
Size = new Vector2(20);
|
|
||||||
|
|
||||||
Child = icon = new SpriteIcon
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Scale = new Vector2(0.75f),
|
|
||||||
Icon = FontAwesome.Solid.TimesCircle,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
|
||||||
{
|
|
||||||
icon.ScaleTo(0.5f, 1000, Easing.OutQuint);
|
|
||||||
return base.OnMouseDown(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnMouseUp(MouseUpEvent e)
|
|
||||||
{
|
|
||||||
icon.ScaleTo(0.75f, 1000, Easing.OutElastic);
|
|
||||||
base.OnMouseUp(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
|
||||||
{
|
|
||||||
icon.FadeColour(Color4.Red, 200, Easing.OutQuint);
|
|
||||||
return base.OnHover(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnHoverLost(HoverLostEvent e)
|
|
||||||
{
|
|
||||||
icon.FadeColour(Color4.White, 200, Easing.OutQuint);
|
|
||||||
base.OnHoverLost(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +1,29 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osu.Game.Overlays.Chat;
|
|
||||||
using osu.Game.Overlays.Chat.Selection;
|
|
||||||
using osu.Game.Overlays.Chat.Tabs;
|
|
||||||
using osuTK.Input;
|
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Framework.Graphics.Textures;
|
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online.Chat;
|
||||||
|
using osu.Game.Overlays.Chat;
|
||||||
|
using osu.Game.Overlays.Chat.ChannelList;
|
||||||
|
using osu.Game.Overlays.Chat.Listing;
|
||||||
|
|
||||||
namespace osu.Game.Overlays
|
namespace osu.Game.Overlays
|
||||||
{
|
{
|
||||||
@ -39,271 +33,147 @@ namespace osu.Game.Overlays
|
|||||||
public LocalisableString Title => ChatStrings.HeaderTitle;
|
public LocalisableString Title => ChatStrings.HeaderTitle;
|
||||||
public LocalisableString Description => ChatStrings.HeaderDescription;
|
public LocalisableString Description => ChatStrings.HeaderDescription;
|
||||||
|
|
||||||
private const float text_box_height = 60;
|
private ChatOverlayTopBar topBar = null!;
|
||||||
private const float channel_selection_min_height = 0.3f;
|
private ChannelList channelList = null!;
|
||||||
|
private LoadingLayer loading = null!;
|
||||||
|
private ChannelListing channelListing = null!;
|
||||||
|
private ChatTextBar textBar = null!;
|
||||||
|
private Container<ChatOverlayDrawableChannel> currentChannelContainer = null!;
|
||||||
|
|
||||||
[Resolved]
|
private readonly Dictionary<Channel, ChatOverlayDrawableChannel> loadedChannels = new Dictionary<Channel, ChatOverlayDrawableChannel>();
|
||||||
private ChannelManager channelManager { get; set; }
|
|
||||||
|
|
||||||
private Container<DrawableChannel> currentChannelContainer;
|
protected IEnumerable<DrawableChannel> DrawableChannels => loadedChannels.Values;
|
||||||
|
|
||||||
private readonly List<DrawableChannel> loadedChannels = new List<DrawableChannel>();
|
private readonly BindableFloat chatHeight = new BindableFloat();
|
||||||
|
private bool isDraggingTopBar;
|
||||||
private LoadingSpinner loading;
|
private float dragStartChatHeight;
|
||||||
|
|
||||||
private FocusedTextBox textBox;
|
|
||||||
|
|
||||||
private const int transition_length = 500;
|
|
||||||
|
|
||||||
public const float DEFAULT_HEIGHT = 0.4f;
|
public const float DEFAULT_HEIGHT = 0.4f;
|
||||||
|
|
||||||
public const float TAB_AREA_HEIGHT = 50;
|
private const int transition_length = 500;
|
||||||
|
private const float top_bar_height = 40;
|
||||||
|
private const float side_bar_width = 190;
|
||||||
|
private const float chat_bar_height = 60;
|
||||||
|
|
||||||
protected ChannelTabControl ChannelTabControl;
|
[Resolved]
|
||||||
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
protected virtual ChannelTabControl CreateChannelTabControl() => new ChannelTabControl();
|
[Resolved]
|
||||||
|
private ChannelManager channelManager { get; set; } = null!;
|
||||||
|
|
||||||
private Container chatContainer;
|
[Cached]
|
||||||
private TabsArea tabsArea;
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
|
||||||
private Box chatBackground;
|
|
||||||
private Box tabBackground;
|
|
||||||
|
|
||||||
public Bindable<float> ChatHeight { get; set; }
|
[Cached]
|
||||||
|
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
|
||||||
private Container channelSelectionContainer;
|
|
||||||
protected ChannelSelectionOverlay ChannelSelectionOverlay;
|
|
||||||
|
|
||||||
private readonly IBindableList<Channel> availableChannels = new BindableList<Channel>();
|
private readonly IBindableList<Channel> availableChannels = new BindableList<Channel>();
|
||||||
private readonly IBindableList<Channel> joinedChannels = new BindableList<Channel>();
|
private readonly IBindableList<Channel> joinedChannels = new BindableList<Channel>();
|
||||||
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
|
|
||||||
|
|
||||||
public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos)
|
|
||||||
|| (ChannelSelectionOverlay.State.Value == Visibility.Visible && ChannelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos));
|
|
||||||
|
|
||||||
public ChatOverlay()
|
public ChatOverlay()
|
||||||
{
|
{
|
||||||
|
Height = DEFAULT_HEIGHT;
|
||||||
|
|
||||||
|
Masking = true;
|
||||||
|
|
||||||
|
const float corner_radius = 7f;
|
||||||
|
|
||||||
|
CornerRadius = corner_radius;
|
||||||
|
|
||||||
|
// Hack to hide the bottom edge corner radius off-screen.
|
||||||
|
Margin = new MarginPadding { Bottom = -corner_radius };
|
||||||
|
Padding = new MarginPadding { Bottom = corner_radius };
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
RelativePositionAxes = Axes.Both;
|
Anchor = Anchor.BottomCentre;
|
||||||
Anchor = Anchor.BottomLeft;
|
Origin = Anchor.BottomCentre;
|
||||||
Origin = Anchor.BottomLeft;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config, OsuColour colours, TextureStore textures)
|
private void load()
|
||||||
{
|
{
|
||||||
const float padding = 5;
|
// Required for the pop in/out animation
|
||||||
|
RelativePositionAxes = Axes.Both;
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
channelSelectionContainer = new Container
|
topBar = new ChatOverlayTopBar
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Height = top_bar_height,
|
||||||
|
},
|
||||||
|
channelList = new ChannelList
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Width = side_bar_width,
|
||||||
|
Padding = new MarginPadding { Top = top_bar_height },
|
||||||
|
},
|
||||||
|
new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Height = 1f - DEFAULT_HEIGHT,
|
Anchor = Anchor.TopRight,
|
||||||
Masking = true,
|
Origin = Anchor.TopRight,
|
||||||
Children = new[]
|
Padding = new MarginPadding
|
||||||
{
|
{
|
||||||
ChannelSelectionOverlay = new ChannelSelectionOverlay
|
Top = top_bar_height,
|
||||||
|
Left = side_bar_width,
|
||||||
|
Bottom = chat_bar_height,
|
||||||
|
},
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = colourProvider.Background4,
|
||||||
|
},
|
||||||
|
currentChannelContainer = new Container<ChatOverlayDrawableChannel>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
loading = new LoadingLayer(true),
|
||||||
|
channelListing = new ChannelListing
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
chatContainer = new Container
|
textBar = new ChatTextBar
|
||||||
{
|
{
|
||||||
Name = @"chat container",
|
RelativeSizeAxes = Axes.X,
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomRight,
|
||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomRight,
|
||||||
RelativeSizeAxes = Axes.Both,
|
Padding = new MarginPadding { Left = side_bar_width },
|
||||||
Height = DEFAULT_HEIGHT,
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
Name = @"chat area",
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Padding = new MarginPadding { Top = TAB_AREA_HEIGHT },
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
chatBackground = new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
new OnlineViewContainer("Sign in to chat")
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
currentChannelContainer = new Container<DrawableChannel>
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Padding = new MarginPadding
|
|
||||||
{
|
|
||||||
Bottom = text_box_height
|
|
||||||
},
|
|
||||||
},
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
Anchor = Anchor.BottomLeft,
|
|
||||||
Origin = Anchor.BottomLeft,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Height = text_box_height,
|
|
||||||
Padding = new MarginPadding
|
|
||||||
{
|
|
||||||
Top = padding * 2,
|
|
||||||
Bottom = padding * 2,
|
|
||||||
Left = ChatLine.LEFT_PADDING + padding * 2,
|
|
||||||
Right = padding * 2,
|
|
||||||
},
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
textBox = new FocusedTextBox
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Height = 1,
|
|
||||||
PlaceholderText = Resources.Localisation.Web.ChatStrings.InputPlaceholder,
|
|
||||||
ReleaseFocusOnCommit = false,
|
|
||||||
HoldFocus = true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loading = new LoadingSpinner(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
tabsArea = new TabsArea
|
|
||||||
{
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
tabBackground = new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = Color4.Black,
|
|
||||||
},
|
|
||||||
new Sprite
|
|
||||||
{
|
|
||||||
Texture = textures.Get(IconTexture),
|
|
||||||
Anchor = Anchor.CentreLeft,
|
|
||||||
Origin = Anchor.CentreLeft,
|
|
||||||
Size = new Vector2(OverlayTitle.ICON_SIZE),
|
|
||||||
Margin = new MarginPadding { Left = 10 },
|
|
||||||
},
|
|
||||||
ChannelTabControl = CreateChannelTabControl().With(d =>
|
|
||||||
{
|
|
||||||
d.Anchor = Anchor.BottomLeft;
|
|
||||||
d.Origin = Anchor.BottomLeft;
|
|
||||||
d.RelativeSizeAxes = Axes.Both;
|
|
||||||
d.OnRequestLeave = channelManager.LeaveChannel;
|
|
||||||
d.IsSwitchable = true;
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
availableChannels.BindTo(channelManager.AvailableChannels);
|
|
||||||
joinedChannels.BindTo(channelManager.JoinedChannels);
|
|
||||||
currentChannel.BindTo(channelManager.CurrentChannel);
|
|
||||||
|
|
||||||
textBox.OnCommit += postMessage;
|
|
||||||
|
|
||||||
ChannelTabControl.Current.ValueChanged += current => currentChannel.Value = current.NewValue;
|
|
||||||
ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
|
|
||||||
ChannelSelectionOverlay.State.ValueChanged += state =>
|
|
||||||
{
|
|
||||||
// Propagate the visibility state to ChannelSelectorActive
|
|
||||||
ChannelTabControl.ChannelSelectorActive.Value = state.NewValue == Visibility.Visible;
|
|
||||||
|
|
||||||
if (state.NewValue == Visibility.Visible)
|
|
||||||
{
|
|
||||||
textBox.HoldFocus = false;
|
|
||||||
if (1f - ChatHeight.Value < channel_selection_min_height)
|
|
||||||
this.TransformBindableTo(ChatHeight, 1f - channel_selection_min_height, 800, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
textBox.HoldFocus = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
ChannelSelectionOverlay.OnRequestJoin = channel => channelManager.JoinChannel(channel);
|
|
||||||
ChannelSelectionOverlay.OnRequestLeave = channelManager.LeaveChannel;
|
|
||||||
|
|
||||||
ChatHeight = config.GetBindable<float>(OsuSetting.ChatDisplayHeight);
|
|
||||||
ChatHeight.BindValueChanged(height =>
|
|
||||||
{
|
|
||||||
chatContainer.Height = height.NewValue;
|
|
||||||
channelSelectionContainer.Height = 1f - height.NewValue;
|
|
||||||
tabBackground.FadeTo(height.NewValue == 1f ? 1f : 0.8f, 200);
|
|
||||||
}, true);
|
|
||||||
|
|
||||||
chatBackground.Colour = colours.ChatBlue;
|
|
||||||
|
|
||||||
loading.Show();
|
|
||||||
|
|
||||||
// This is a relatively expensive (and blocking) operation.
|
|
||||||
// Scheduling it ensures that it won't be performed unless the user decides to open chat.
|
|
||||||
// TODO: Refactor OsuFocusedOverlayContainer / OverlayContainer to support delayed content loading.
|
|
||||||
Schedule(() =>
|
|
||||||
{
|
|
||||||
// TODO: consider scheduling bindable callbacks to not perform when overlay is not present.
|
|
||||||
joinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
|
|
||||||
availableChannels.BindCollectionChanged(availableChannelsChanged, true);
|
|
||||||
currentChannel.BindValueChanged(currentChannelChanged, true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void currentChannelChanged(ValueChangedEvent<Channel> e)
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
if (e.NewValue == null)
|
base.LoadComplete();
|
||||||
|
|
||||||
|
config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
|
||||||
|
|
||||||
|
chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
|
||||||
|
|
||||||
|
currentChannel.BindTo(channelManager.CurrentChannel);
|
||||||
|
joinedChannels.BindTo(channelManager.JoinedChannels);
|
||||||
|
availableChannels.BindTo(channelManager.AvailableChannels);
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
textBox.Current.Disabled = true;
|
currentChannel.BindValueChanged(currentChannelChanged, true);
|
||||||
currentChannelContainer.Clear(false);
|
joinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
|
||||||
ChannelSelectionOverlay.Show();
|
availableChannels.BindCollectionChanged(availableChannelsChanged, true);
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel)
|
channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = channel;
|
||||||
return;
|
channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
|
||||||
|
|
||||||
textBox.Current.Disabled = e.NewValue.ReadOnly;
|
channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel);
|
||||||
|
channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
|
||||||
|
|
||||||
if (ChannelTabControl.Current.Value != e.NewValue)
|
textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms;
|
||||||
Scheduler.Add(() => ChannelTabControl.Current.Value = e.NewValue);
|
textBar.OnChatMessageCommitted += handleChatMessage;
|
||||||
|
|
||||||
var loaded = loadedChannels.Find(d => d.Channel == e.NewValue);
|
|
||||||
|
|
||||||
if (loaded == null)
|
|
||||||
{
|
|
||||||
currentChannelContainer.FadeOut(500, Easing.OutQuint);
|
|
||||||
loading.Show();
|
|
||||||
|
|
||||||
loaded = new DrawableChannel(e.NewValue);
|
|
||||||
loadedChannels.Add(loaded);
|
|
||||||
LoadComponentAsync(loaded, l =>
|
|
||||||
{
|
|
||||||
if (currentChannel.Value != e.NewValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means).
|
|
||||||
if (!loadedChannels.Contains(loaded))
|
|
||||||
return;
|
|
||||||
|
|
||||||
loading.Hide();
|
|
||||||
|
|
||||||
currentChannelContainer.Clear(false);
|
|
||||||
currentChannelContainer.Add(loaded);
|
|
||||||
currentChannelContainer.FadeIn(500, Easing.OutQuint);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
currentChannelContainer.Clear(false);
|
|
||||||
currentChannelContainer.Add(loaded);
|
|
||||||
}
|
|
||||||
|
|
||||||
// mark channel as read when channel switched
|
|
||||||
if (e.NewValue.Messages.Any())
|
|
||||||
channelManager.MarkChannelAsRead(e.NewValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -320,7 +190,7 @@ namespace osu.Game.Overlays
|
|||||||
if (!channel.Joined.Value)
|
if (!channel.Joined.Value)
|
||||||
channel = channelManager.JoinChannel(channel);
|
channel = channelManager.JoinChannel(channel);
|
||||||
|
|
||||||
currentChannel.Value = channel;
|
channelManager.CurrentChannel.Value = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.HighlightedMessage.Value = message;
|
channel.HighlightedMessage.Value = message;
|
||||||
@ -328,159 +198,172 @@ namespace osu.Game.Overlays
|
|||||||
Show();
|
Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private float startDragChatHeight;
|
|
||||||
private bool isDragging;
|
|
||||||
|
|
||||||
protected override bool OnDragStart(DragStartEvent e)
|
|
||||||
{
|
|
||||||
isDragging = tabsArea.IsHovered;
|
|
||||||
|
|
||||||
if (!isDragging)
|
|
||||||
return base.OnDragStart(e);
|
|
||||||
|
|
||||||
startDragChatHeight = ChatHeight.Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDrag(DragEvent e)
|
|
||||||
{
|
|
||||||
if (isDragging)
|
|
||||||
{
|
|
||||||
float targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
|
|
||||||
|
|
||||||
// If the channel selection screen is shown, mind its minimum height
|
|
||||||
if (ChannelSelectionOverlay.State.Value == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height)
|
|
||||||
targetChatHeight = 1f - channel_selection_min_height;
|
|
||||||
|
|
||||||
ChatHeight.Value = targetChatHeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDragEnd(DragEndEvent e)
|
|
||||||
{
|
|
||||||
isDragging = false;
|
|
||||||
base.OnDragEnd(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void selectTab(int index)
|
|
||||||
{
|
|
||||||
var channel = ChannelTabControl.Items
|
|
||||||
.Where(tab => !(tab is ChannelSelectorTabItem.ChannelSelectorTabChannel))
|
|
||||||
.ElementAtOrDefault(index);
|
|
||||||
if (channel != null)
|
|
||||||
ChannelTabControl.Current.Value = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
|
||||||
{
|
|
||||||
if (e.AltPressed)
|
|
||||||
{
|
|
||||||
switch (e.Key)
|
|
||||||
{
|
|
||||||
case Key.Number1:
|
|
||||||
case Key.Number2:
|
|
||||||
case Key.Number3:
|
|
||||||
case Key.Number4:
|
|
||||||
case Key.Number5:
|
|
||||||
case Key.Number6:
|
|
||||||
case Key.Number7:
|
|
||||||
case Key.Number8:
|
|
||||||
case Key.Number9:
|
|
||||||
selectTab((int)e.Key - (int)Key.Number1);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case Key.Number0:
|
|
||||||
selectTab(9);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.OnKeyDown(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
case PlatformAction.TabNew:
|
case PlatformAction.TabNew:
|
||||||
ChannelTabControl.SelectChannelSelectorTab();
|
currentChannel.Value = channelList.ChannelListingChannel;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case PlatformAction.DocumentClose:
|
||||||
|
channelManager.LeaveChannel(currentChannel.Value);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case PlatformAction.TabRestore:
|
case PlatformAction.TabRestore:
|
||||||
channelManager.JoinLastClosedChannel();
|
channelManager.JoinLastClosedChannel();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case PlatformAction.DocumentClose:
|
case PlatformAction.DocumentPrevious:
|
||||||
channelManager.LeaveChannel(currentChannel.Value);
|
cycleChannel(-1);
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
case PlatformAction.DocumentNext:
|
||||||
|
cycleChannel(1);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool AcceptsFocus => true;
|
protected override bool OnDragStart(DragStartEvent e)
|
||||||
|
|
||||||
protected override void OnFocus(FocusEvent e)
|
|
||||||
{
|
{
|
||||||
// this is necessary as textbox is masked away and therefore can't get focus :(
|
isDraggingTopBar = topBar.IsHovered;
|
||||||
textBox.TakeFocus();
|
|
||||||
base.OnFocus(e);
|
if (!isDraggingTopBar)
|
||||||
|
return base.OnDragStart(e);
|
||||||
|
|
||||||
|
dragStartChatHeight = chatHeight.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDrag(DragEvent e)
|
||||||
|
{
|
||||||
|
if (!isDraggingTopBar)
|
||||||
|
return;
|
||||||
|
|
||||||
|
float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
|
||||||
|
chatHeight.Value = targetChatHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDragEnd(DragEndEvent e)
|
||||||
|
{
|
||||||
|
isDraggingTopBar = false;
|
||||||
|
base.OnDragEnd(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopIn()
|
protected override void PopIn()
|
||||||
{
|
{
|
||||||
|
base.PopIn();
|
||||||
|
|
||||||
this.MoveToY(0, transition_length, Easing.OutQuint);
|
this.MoveToY(0, transition_length, Easing.OutQuint);
|
||||||
this.FadeIn(transition_length, Easing.OutQuint);
|
this.FadeIn(transition_length, Easing.OutQuint);
|
||||||
|
|
||||||
textBox.HoldFocus = true;
|
|
||||||
|
|
||||||
base.PopIn();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void PopOut()
|
protected override void PopOut()
|
||||||
{
|
{
|
||||||
|
base.PopOut();
|
||||||
|
|
||||||
this.MoveToY(Height, transition_length, Easing.InSine);
|
this.MoveToY(Height, transition_length, Easing.InSine);
|
||||||
this.FadeOut(transition_length, Easing.InSine);
|
this.FadeOut(transition_length, Easing.InSine);
|
||||||
|
|
||||||
ChannelSelectionOverlay.Hide();
|
textBar.TextBoxKillFocus();
|
||||||
|
|
||||||
textBox.HoldFocus = false;
|
|
||||||
base.PopOut();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnFocus(FocusEvent e)
|
||||||
|
{
|
||||||
|
textBar.TextBoxTakeFocus();
|
||||||
|
base.OnFocus(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void currentChannelChanged(ValueChangedEvent<Channel> channel)
|
||||||
|
{
|
||||||
|
Channel? newChannel = channel.NewValue;
|
||||||
|
|
||||||
|
// null channel denotes that we should be showing the listing.
|
||||||
|
if (newChannel == null)
|
||||||
|
{
|
||||||
|
currentChannel.Value = channelList.ChannelListingChannel;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newChannel is ChannelListing.ChannelListingChannel)
|
||||||
|
{
|
||||||
|
currentChannelContainer.Clear(false);
|
||||||
|
channelListing.Show();
|
||||||
|
textBar.ShowSearch.Value = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
channelListing.Hide();
|
||||||
|
textBar.ShowSearch.Value = false;
|
||||||
|
|
||||||
|
if (loadedChannels.ContainsKey(newChannel))
|
||||||
|
{
|
||||||
|
currentChannelContainer.Clear(false);
|
||||||
|
currentChannelContainer.Add(loadedChannels[newChannel]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
loading.Show();
|
||||||
|
|
||||||
|
// Ensure the drawable channel is stored before async load to prevent double loading
|
||||||
|
ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
|
||||||
|
loadedChannels.Add(newChannel, drawableChannel);
|
||||||
|
|
||||||
|
LoadComponentAsync(drawableChannel, loadedDrawable =>
|
||||||
|
{
|
||||||
|
// Ensure the current channel hasn't changed by the time the load completes
|
||||||
|
if (currentChannel.Value != loadedDrawable.Channel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Ensure the cached reference hasn't been removed from leaving the channel
|
||||||
|
if (!loadedChannels.ContainsKey(loadedDrawable.Channel))
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentChannelContainer.Clear(false);
|
||||||
|
currentChannelContainer.Add(loadedDrawable);
|
||||||
|
loading.Hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark channel as read when channel switched
|
||||||
|
if (newChannel.Messages.Any())
|
||||||
|
channelManager.MarkChannelAsRead(newChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
|
||||||
|
|
||||||
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
switch (args.Action)
|
switch (args.Action)
|
||||||
{
|
{
|
||||||
case NotifyCollectionChangedAction.Add:
|
case NotifyCollectionChangedAction.Add:
|
||||||
foreach (Channel channel in args.NewItems.Cast<Channel>())
|
IEnumerable<Channel> newChannels = args.NewItems.OfType<Channel>().Where(isChatChannel);
|
||||||
{
|
|
||||||
if (channel.Type != ChannelType.Multiplayer)
|
foreach (var channel in newChannels)
|
||||||
ChannelTabControl.AddChannel(channel);
|
channelList.AddChannel(channel);
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Remove:
|
case NotifyCollectionChangedAction.Remove:
|
||||||
foreach (Channel channel in args.OldItems.Cast<Channel>())
|
IEnumerable<Channel> leftChannels = args.OldItems.OfType<Channel>().Where(isChatChannel);
|
||||||
|
|
||||||
|
foreach (var channel in leftChannels)
|
||||||
{
|
{
|
||||||
if (!ChannelTabControl.Items.Contains(channel))
|
channelList.RemoveChannel(channel);
|
||||||
continue;
|
|
||||||
|
|
||||||
ChannelTabControl.RemoveChannel(channel);
|
if (loadedChannels.ContainsKey(channel))
|
||||||
|
|
||||||
var loaded = loadedChannels.Find(c => c.Channel == channel);
|
|
||||||
|
|
||||||
if (loaded != null)
|
|
||||||
{
|
{
|
||||||
// Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared
|
ChatOverlayDrawableChannel loaded = loadedChannels[channel];
|
||||||
// to ensure that the previous channel doesn't get updated after it's disposed
|
loadedChannels.Remove(channel);
|
||||||
loadedChannels.Remove(loaded);
|
// DrawableChannel removed from cache must be manually disposed
|
||||||
currentChannelContainer.Remove(loaded);
|
|
||||||
loaded.Dispose();
|
loaded.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -490,35 +373,47 @@ namespace osu.Game.Overlays
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||||
{
|
=> channelListing.UpdateAvailableChannels(channelManager.AvailableChannels);
|
||||||
ChannelSelectionOverlay.UpdateAvailableChannels(availableChannels);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void postMessage(TextBox textBox, bool newText)
|
private void handleChatMessage(string message)
|
||||||
{
|
{
|
||||||
string text = textBox.Text.Trim();
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (text[0] == '/')
|
if (message[0] == '/')
|
||||||
channelManager.PostCommand(text.Substring(1));
|
channelManager.PostCommand(message.Substring(1));
|
||||||
else
|
else
|
||||||
channelManager.PostMessage(text);
|
channelManager.PostMessage(message);
|
||||||
|
|
||||||
textBox.Text = string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TabsArea : Container
|
private void cycleChannel(int direction)
|
||||||
{
|
{
|
||||||
// IsHovered is used
|
List<Channel> overlayChannels = channelList.Channels.ToList();
|
||||||
public override bool HandlePositionalInput => true;
|
|
||||||
|
|
||||||
public TabsArea()
|
if (overlayChannels.Count < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int currentIndex = overlayChannels.IndexOf(currentChannel.Value);
|
||||||
|
|
||||||
|
currentChannel.Value = overlayChannels[(currentIndex + direction + overlayChannels.Count) % overlayChannels.Count];
|
||||||
|
|
||||||
|
channelList.ScrollChannelIntoView(currentChannel.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a channel should be displayed in this overlay, based on its type.
|
||||||
|
/// </summary>
|
||||||
|
private static bool isChatChannel(Channel channel)
|
||||||
|
{
|
||||||
|
switch (channel.Type)
|
||||||
{
|
{
|
||||||
Name = @"tabs area";
|
case ChannelType.Multiplayer:
|
||||||
RelativeSizeAxes = Axes.X;
|
case ChannelType.Spectator:
|
||||||
Height = TAB_AREA_HEIGHT;
|
case ChannelType.Temporary:
|
||||||
|
return false;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,420 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Framework.Input;
|
|
||||||
using osu.Framework.Input.Bindings;
|
|
||||||
using osu.Framework.Input.Events;
|
|
||||||
using osu.Framework.Localisation;
|
|
||||||
using osu.Game.Configuration;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Localisation;
|
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osu.Game.Overlays.Chat;
|
|
||||||
using osu.Game.Overlays.Chat.ChannelList;
|
|
||||||
using osu.Game.Overlays.Chat.Listing;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays
|
|
||||||
{
|
|
||||||
public class ChatOverlayV2 : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler<PlatformAction>
|
|
||||||
{
|
|
||||||
public string IconTexture => "Icons/Hexacons/messaging";
|
|
||||||
public LocalisableString Title => ChatStrings.HeaderTitle;
|
|
||||||
public LocalisableString Description => ChatStrings.HeaderDescription;
|
|
||||||
|
|
||||||
private ChatOverlayTopBar topBar = null!;
|
|
||||||
private ChannelList channelList = null!;
|
|
||||||
private LoadingLayer loading = null!;
|
|
||||||
private ChannelListing channelListing = null!;
|
|
||||||
private ChatTextBar textBar = null!;
|
|
||||||
private Container<ChatOverlayDrawableChannel> currentChannelContainer = null!;
|
|
||||||
|
|
||||||
private readonly Dictionary<Channel, ChatOverlayDrawableChannel> loadedChannels = new Dictionary<Channel, ChatOverlayDrawableChannel>();
|
|
||||||
|
|
||||||
protected IEnumerable<DrawableChannel> DrawableChannels => loadedChannels.Values;
|
|
||||||
|
|
||||||
private readonly BindableFloat chatHeight = new BindableFloat();
|
|
||||||
private bool isDraggingTopBar;
|
|
||||||
private float dragStartChatHeight;
|
|
||||||
|
|
||||||
public const float DEFAULT_HEIGHT = 0.4f;
|
|
||||||
|
|
||||||
private const int transition_length = 500;
|
|
||||||
private const float top_bar_height = 40;
|
|
||||||
private const float side_bar_width = 190;
|
|
||||||
private const float chat_bar_height = 60;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private OsuConfigManager config { get; set; } = null!;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private ChannelManager channelManager { get; set; } = null!;
|
|
||||||
|
|
||||||
[Cached]
|
|
||||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
|
|
||||||
|
|
||||||
[Cached]
|
|
||||||
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
|
|
||||||
|
|
||||||
private readonly IBindableList<Channel> availableChannels = new BindableList<Channel>();
|
|
||||||
private readonly IBindableList<Channel> joinedChannels = new BindableList<Channel>();
|
|
||||||
|
|
||||||
public ChatOverlayV2()
|
|
||||||
{
|
|
||||||
Height = DEFAULT_HEIGHT;
|
|
||||||
|
|
||||||
Masking = true;
|
|
||||||
|
|
||||||
const float corner_radius = 7f;
|
|
||||||
|
|
||||||
CornerRadius = corner_radius;
|
|
||||||
|
|
||||||
// Hack to hide the bottom edge corner radius off-screen.
|
|
||||||
Margin = new MarginPadding { Bottom = -corner_radius };
|
|
||||||
Padding = new MarginPadding { Bottom = corner_radius };
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
Anchor = Anchor.BottomCentre;
|
|
||||||
Origin = Anchor.BottomCentre;
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load()
|
|
||||||
{
|
|
||||||
// Required for the pop in/out animation
|
|
||||||
RelativePositionAxes = Axes.Both;
|
|
||||||
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
topBar = new ChatOverlayTopBar
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Height = top_bar_height,
|
|
||||||
},
|
|
||||||
channelList = new ChannelList
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
Width = side_bar_width,
|
|
||||||
Padding = new MarginPadding { Top = top_bar_height },
|
|
||||||
},
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Anchor = Anchor.TopRight,
|
|
||||||
Origin = Anchor.TopRight,
|
|
||||||
Padding = new MarginPadding
|
|
||||||
{
|
|
||||||
Top = top_bar_height,
|
|
||||||
Left = side_bar_width,
|
|
||||||
Bottom = chat_bar_height,
|
|
||||||
},
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Colour = colourProvider.Background4,
|
|
||||||
},
|
|
||||||
currentChannelContainer = new Container<ChatOverlayDrawableChannel>
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
loading = new LoadingLayer(true),
|
|
||||||
channelListing = new ChannelListing
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
textBar = new ChatTextBar
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
Anchor = Anchor.BottomRight,
|
|
||||||
Origin = Anchor.BottomRight,
|
|
||||||
Padding = new MarginPadding { Left = side_bar_width },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
config.BindWith(OsuSetting.ChatDisplayHeight, chatHeight);
|
|
||||||
|
|
||||||
chatHeight.BindValueChanged(height => { Height = height.NewValue; }, true);
|
|
||||||
|
|
||||||
currentChannel.BindTo(channelManager.CurrentChannel);
|
|
||||||
joinedChannels.BindTo(channelManager.JoinedChannels);
|
|
||||||
availableChannels.BindTo(channelManager.AvailableChannels);
|
|
||||||
|
|
||||||
Schedule(() =>
|
|
||||||
{
|
|
||||||
currentChannel.BindValueChanged(currentChannelChanged, true);
|
|
||||||
joinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
|
|
||||||
availableChannels.BindCollectionChanged(availableChannelsChanged, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
channelList.OnRequestSelect += channel => channelManager.CurrentChannel.Value = channel;
|
|
||||||
channelList.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
|
|
||||||
|
|
||||||
channelListing.OnRequestJoin += channel => channelManager.JoinChannel(channel);
|
|
||||||
channelListing.OnRequestLeave += channel => channelManager.LeaveChannel(channel);
|
|
||||||
|
|
||||||
textBar.OnSearchTermsChanged += searchTerms => channelListing.SearchTerm = searchTerms;
|
|
||||||
textBar.OnChatMessageCommitted += handleChatMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Highlights a certain message in the specified channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="message">The message to highlight.</param>
|
|
||||||
/// <param name="channel">The channel containing the message.</param>
|
|
||||||
public void HighlightMessage(Message message, Channel channel)
|
|
||||||
{
|
|
||||||
Debug.Assert(channel.Id == message.ChannelId);
|
|
||||||
|
|
||||||
if (currentChannel.Value?.Id != channel.Id)
|
|
||||||
{
|
|
||||||
if (!channel.Joined.Value)
|
|
||||||
channel = channelManager.JoinChannel(channel);
|
|
||||||
|
|
||||||
channelManager.CurrentChannel.Value = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.HighlightedMessage.Value = message;
|
|
||||||
|
|
||||||
Show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
|
|
||||||
{
|
|
||||||
switch (e.Action)
|
|
||||||
{
|
|
||||||
case PlatformAction.TabNew:
|
|
||||||
currentChannel.Value = channelList.ChannelListingChannel;
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case PlatformAction.DocumentClose:
|
|
||||||
channelManager.LeaveChannel(currentChannel.Value);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case PlatformAction.TabRestore:
|
|
||||||
channelManager.JoinLastClosedChannel();
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case PlatformAction.DocumentPrevious:
|
|
||||||
cycleChannel(-1);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case PlatformAction.DocumentNext:
|
|
||||||
cycleChannel(1);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnReleased(KeyBindingReleaseEvent<PlatformAction> e)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnDragStart(DragStartEvent e)
|
|
||||||
{
|
|
||||||
isDraggingTopBar = topBar.IsHovered;
|
|
||||||
|
|
||||||
if (!isDraggingTopBar)
|
|
||||||
return base.OnDragStart(e);
|
|
||||||
|
|
||||||
dragStartChatHeight = chatHeight.Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDrag(DragEvent e)
|
|
||||||
{
|
|
||||||
if (!isDraggingTopBar)
|
|
||||||
return;
|
|
||||||
|
|
||||||
float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
|
|
||||||
chatHeight.Value = targetChatHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDragEnd(DragEndEvent e)
|
|
||||||
{
|
|
||||||
isDraggingTopBar = false;
|
|
||||||
base.OnDragEnd(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void PopIn()
|
|
||||||
{
|
|
||||||
base.PopIn();
|
|
||||||
|
|
||||||
this.MoveToY(0, transition_length, Easing.OutQuint);
|
|
||||||
this.FadeIn(transition_length, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void PopOut()
|
|
||||||
{
|
|
||||||
base.PopOut();
|
|
||||||
|
|
||||||
this.MoveToY(Height, transition_length, Easing.InSine);
|
|
||||||
this.FadeOut(transition_length, Easing.InSine);
|
|
||||||
|
|
||||||
textBar.TextBoxKillFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnFocus(FocusEvent e)
|
|
||||||
{
|
|
||||||
textBar.TextBoxTakeFocus();
|
|
||||||
base.OnFocus(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void currentChannelChanged(ValueChangedEvent<Channel> channel)
|
|
||||||
{
|
|
||||||
Channel? newChannel = channel.NewValue;
|
|
||||||
|
|
||||||
// null channel denotes that we should be showing the listing.
|
|
||||||
if (newChannel == null)
|
|
||||||
{
|
|
||||||
currentChannel.Value = channelList.ChannelListingChannel;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newChannel is ChannelListing.ChannelListingChannel)
|
|
||||||
{
|
|
||||||
currentChannelContainer.Clear(false);
|
|
||||||
channelListing.Show();
|
|
||||||
textBar.ShowSearch.Value = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
channelListing.Hide();
|
|
||||||
textBar.ShowSearch.Value = false;
|
|
||||||
|
|
||||||
if (loadedChannels.ContainsKey(newChannel))
|
|
||||||
{
|
|
||||||
currentChannelContainer.Clear(false);
|
|
||||||
currentChannelContainer.Add(loadedChannels[newChannel]);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
loading.Show();
|
|
||||||
|
|
||||||
// Ensure the drawable channel is stored before async load to prevent double loading
|
|
||||||
ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
|
|
||||||
loadedChannels.Add(newChannel, drawableChannel);
|
|
||||||
|
|
||||||
LoadComponentAsync(drawableChannel, loadedDrawable =>
|
|
||||||
{
|
|
||||||
// Ensure the current channel hasn't changed by the time the load completes
|
|
||||||
if (currentChannel.Value != loadedDrawable.Channel)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Ensure the cached reference hasn't been removed from leaving the channel
|
|
||||||
if (!loadedChannels.ContainsKey(loadedDrawable.Channel))
|
|
||||||
return;
|
|
||||||
|
|
||||||
currentChannelContainer.Clear(false);
|
|
||||||
currentChannelContainer.Add(loadedDrawable);
|
|
||||||
loading.Hide();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark channel as read when channel switched
|
|
||||||
if (newChannel.Messages.Any())
|
|
||||||
channelManager.MarkChannelAsRead(newChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
|
|
||||||
|
|
||||||
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
|
||||||
{
|
|
||||||
switch (args.Action)
|
|
||||||
{
|
|
||||||
case NotifyCollectionChangedAction.Add:
|
|
||||||
IEnumerable<Channel> newChannels = args.NewItems.OfType<Channel>().Where(isChatChannel);
|
|
||||||
|
|
||||||
foreach (var channel in newChannels)
|
|
||||||
channelList.AddChannel(channel);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Remove:
|
|
||||||
IEnumerable<Channel> leftChannels = args.OldItems.OfType<Channel>().Where(isChatChannel);
|
|
||||||
|
|
||||||
foreach (var channel in leftChannels)
|
|
||||||
{
|
|
||||||
channelList.RemoveChannel(channel);
|
|
||||||
|
|
||||||
if (loadedChannels.ContainsKey(channel))
|
|
||||||
{
|
|
||||||
ChatOverlayDrawableChannel loaded = loadedChannels[channel];
|
|
||||||
loadedChannels.Remove(channel);
|
|
||||||
// DrawableChannel removed from cache must be manually disposed
|
|
||||||
loaded.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
|
||||||
=> channelListing.UpdateAvailableChannels(channelManager.AvailableChannels);
|
|
||||||
|
|
||||||
private void handleChatMessage(string message)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(message))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (message[0] == '/')
|
|
||||||
channelManager.PostCommand(message.Substring(1));
|
|
||||||
else
|
|
||||||
channelManager.PostMessage(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cycleChannel(int direction)
|
|
||||||
{
|
|
||||||
List<Channel> overlayChannels = channelList.Channels.ToList();
|
|
||||||
|
|
||||||
if (overlayChannels.Count < 2)
|
|
||||||
return;
|
|
||||||
|
|
||||||
int currentIndex = overlayChannels.IndexOf(currentChannel.Value);
|
|
||||||
|
|
||||||
currentChannel.Value = overlayChannels[(currentIndex + direction + overlayChannels.Count) % overlayChannels.Count];
|
|
||||||
|
|
||||||
channelList.ScrollChannelIntoView(currentChannel.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether a channel should be displayed in this overlay, based on its type.
|
|
||||||
/// </summary>
|
|
||||||
private static bool isChatChannel(Channel channel)
|
|
||||||
{
|
|
||||||
switch (channel.Type)
|
|
||||||
{
|
|
||||||
case ChannelType.Multiplayer:
|
|
||||||
case ChannelType.Spectator:
|
|
||||||
case ChannelType.Temporary:
|
|
||||||
return false;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -9,11 +10,16 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
using osu.Game.Screens;
|
using osu.Game.Screens;
|
||||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
@ -24,26 +30,62 @@ namespace osu.Game.Overlays.Dashboard
|
|||||||
{
|
{
|
||||||
internal class CurrentlyPlayingDisplay : CompositeDrawable
|
internal class CurrentlyPlayingDisplay : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
private const float search_textbox_height = 40;
|
||||||
|
private const float padding = 10;
|
||||||
|
|
||||||
private readonly IBindableList<int> playingUsers = new BindableList<int>();
|
private readonly IBindableList<int> playingUsers = new BindableList<int>();
|
||||||
|
|
||||||
private FillFlowContainer<PlayingUserPanel> userFlow;
|
private SearchContainer<PlayingUserPanel> userFlow;
|
||||||
|
private BasicSearchTextBox searchTextBox;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private SpectatorClient spectatorClient { get; set; }
|
private SpectatorClient spectatorClient { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(OverlayColourProvider colourProvider)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
|
|
||||||
InternalChild = userFlow = new FillFlowContainer<PlayingUserPanel>
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
new Box
|
||||||
AutoSizeAxes = Axes.Y,
|
{
|
||||||
Padding = new MarginPadding(10),
|
RelativeSizeAxes = Axes.X,
|
||||||
Spacing = new Vector2(10),
|
Height = padding * 2 + search_textbox_height,
|
||||||
|
Colour = colourProvider.Background4,
|
||||||
|
},
|
||||||
|
new Container<BasicSearchTextBox>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Padding = new MarginPadding(padding),
|
||||||
|
Child = searchTextBox = new BasicSearchTextBox
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Height = search_textbox_height,
|
||||||
|
ReleaseFocusOnCommit = false,
|
||||||
|
HoldFocus = true,
|
||||||
|
PlaceholderText = HomeStrings.SearchPlaceholder,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
userFlow = new SearchContainer<PlayingUserPanel>
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(10),
|
||||||
|
Padding = new MarginPadding
|
||||||
|
{
|
||||||
|
Top = padding * 3 + search_textbox_height,
|
||||||
|
Bottom = padding,
|
||||||
|
Right = padding,
|
||||||
|
Left = padding,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -57,6 +99,13 @@ namespace osu.Game.Overlays.Dashboard
|
|||||||
playingUsers.BindCollectionChanged(onPlayingUsersChanged, true);
|
playingUsers.BindCollectionChanged(onPlayingUsersChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnFocus(FocusEvent e)
|
||||||
|
{
|
||||||
|
base.OnFocus(e);
|
||||||
|
|
||||||
|
searchTextBox.TakeFocus();
|
||||||
|
}
|
||||||
|
|
||||||
private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
|
private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
|
||||||
{
|
{
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
@ -102,17 +151,34 @@ namespace osu.Game.Overlays.Dashboard
|
|||||||
panel.Origin = Anchor.TopCentre;
|
panel.Origin = Anchor.TopCentre;
|
||||||
});
|
});
|
||||||
|
|
||||||
private class PlayingUserPanel : CompositeDrawable
|
public class PlayingUserPanel : CompositeDrawable, IFilterable
|
||||||
{
|
{
|
||||||
public readonly APIUser User;
|
public readonly APIUser User;
|
||||||
|
|
||||||
|
public IEnumerable<LocalisableString> FilterTerms { get; }
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private IPerformFromScreenRunner performer { get; set; }
|
private IPerformFromScreenRunner performer { get; set; }
|
||||||
|
|
||||||
|
public bool FilteringActive { set; get; }
|
||||||
|
|
||||||
|
public bool MatchingFilter
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value)
|
||||||
|
Show();
|
||||||
|
else
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public PlayingUserPanel(APIUser user)
|
public PlayingUserPanel(APIUser user)
|
||||||
{
|
{
|
||||||
User = user;
|
User = user;
|
||||||
|
|
||||||
|
FilterTerms = new LocalisableString[] { User.Username };
|
||||||
|
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader();
|
protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader();
|
||||||
|
|
||||||
|
public override bool AcceptsFocus => false;
|
||||||
|
|
||||||
protected override void CreateDisplayToLoad(DashboardOverlayTabs tab)
|
protected override void CreateDisplayToLoad(DashboardOverlayTabs tab)
|
||||||
{
|
{
|
||||||
switch (tab)
|
switch (tab)
|
||||||
|
@ -154,12 +154,15 @@ namespace osu.Game.Overlays.FirstRunSetup
|
|||||||
|
|
||||||
var downloadTracker = tutorialDownloader.DownloadTrackers.First();
|
var downloadTracker = tutorialDownloader.DownloadTrackers.First();
|
||||||
|
|
||||||
|
downloadTracker.State.BindValueChanged(state =>
|
||||||
|
{
|
||||||
|
if (state.NewValue == DownloadState.LocallyAvailable)
|
||||||
|
downloadTutorialButton.Complete();
|
||||||
|
}, true);
|
||||||
|
|
||||||
downloadTracker.Progress.BindValueChanged(progress =>
|
downloadTracker.Progress.BindValueChanged(progress =>
|
||||||
{
|
{
|
||||||
downloadTutorialButton.SetProgress(progress.NewValue, false);
|
downloadTutorialButton.SetProgress(progress.NewValue, false);
|
||||||
|
|
||||||
if (progress.NewValue == 1)
|
|
||||||
downloadTutorialButton.Complete();
|
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
|||||||
private UserProfileOverlay userOverlay { get; set; }
|
private UserProfileOverlay userOverlay { get; set; }
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private ChatOverlayV2 chatOverlay { get; set; }
|
private ChatOverlay chatOverlay { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IAPIProvider apiProvider { get; set; }
|
private IAPIProvider apiProvider { get; set; }
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Platform.Windows;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
@ -34,10 +35,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
private Bindable<Size> sizeFullscreen;
|
private Bindable<Size> sizeFullscreen;
|
||||||
|
|
||||||
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
|
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
|
||||||
|
private readonly IBindable<FullscreenCapability> fullscreenCapability = new Bindable<FullscreenCapability>(FullscreenCapability.Capable);
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuGameBase game { get; set; }
|
private OsuGameBase game { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private GameHost host { get; set; }
|
||||||
|
|
||||||
private SettingsDropdown<Size> resolutionDropdown;
|
private SettingsDropdown<Size> resolutionDropdown;
|
||||||
private SettingsDropdown<Display> displayDropdown;
|
private SettingsDropdown<Display> displayDropdown;
|
||||||
private SettingsDropdown<WindowMode> windowModeDropdown;
|
private SettingsDropdown<WindowMode> windowModeDropdown;
|
||||||
@ -65,6 +70,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
windowModes.BindTo(host.Window.SupportedWindowModes);
|
windowModes.BindTo(host.Window.SupportedWindowModes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (host.Window is WindowsWindow windowsWindow)
|
||||||
|
fullscreenCapability.BindTo(windowsWindow.FullscreenCapability);
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
windowModeDropdown = new SettingsDropdown<WindowMode>
|
windowModeDropdown = new SettingsDropdown<WindowMode>
|
||||||
@ -139,6 +147,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fullscreenCapability.BindValueChanged(_ => Schedule(updateScreenModeWarning), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -150,8 +160,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
windowModeDropdown.Current.BindValueChanged(mode =>
|
windowModeDropdown.Current.BindValueChanged(mode =>
|
||||||
{
|
{
|
||||||
updateDisplayModeDropdowns();
|
updateDisplayModeDropdowns();
|
||||||
|
updateScreenModeWarning();
|
||||||
windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? GraphicsSettingsStrings.NotFullscreenNote : default;
|
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
windowModes.BindCollectionChanged((sender, args) =>
|
windowModes.BindCollectionChanged((sender, args) =>
|
||||||
@ -213,6 +222,38 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateScreenModeWarning()
|
||||||
|
{
|
||||||
|
if (windowModeDropdown.Current.Value != WindowMode.Fullscreen)
|
||||||
|
{
|
||||||
|
windowModeDropdown.SetNoticeText(GraphicsSettingsStrings.NotFullscreenNote, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (host.Window is WindowsWindow)
|
||||||
|
{
|
||||||
|
switch (fullscreenCapability.Value)
|
||||||
|
{
|
||||||
|
case FullscreenCapability.Unknown:
|
||||||
|
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.CheckingForFullscreenCapabilities, true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FullscreenCapability.Capable:
|
||||||
|
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.OsuIsRunningExclusiveFullscreen);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FullscreenCapability.Incapable:
|
||||||
|
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.UnableToRunExclusiveFullscreen, true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// We can only detect exclusive fullscreen status on windows currently.
|
||||||
|
windowModeDropdown.ClearNoticeText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void bindPreviewEvent(Bindable<float> bindable)
|
private void bindPreviewEvent(Bindable<float> bindable)
|
||||||
{
|
{
|
||||||
bindable.ValueChanged += _ =>
|
bindable.ValueChanged += _ =>
|
||||||
|
@ -48,7 +48,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
|
|
||||||
frameLimiterDropdown.Current.BindValueChanged(limit =>
|
frameLimiterDropdown.Current.BindValueChanged(limit =>
|
||||||
{
|
{
|
||||||
frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? GraphicsSettingsStrings.UnlimitedFramesNote : default;
|
switch (limit.NewValue)
|
||||||
|
{
|
||||||
|
case FrameSync.Unlimited:
|
||||||
|
frameLimiterDropdown.SetNoticeText(GraphicsSettingsStrings.UnlimitedFramesNote, true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
frameLimiterDropdown.ClearNoticeText();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,9 +117,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
|
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
|
||||||
{
|
{
|
||||||
if (highPrecision.NewValue)
|
if (highPrecision.NewValue)
|
||||||
highPrecisionMouse.WarningText = MouseSettingsStrings.HighPrecisionPlatformWarning;
|
highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true);
|
||||||
else
|
else
|
||||||
highPrecisionMouse.WarningText = null;
|
highPrecisionMouse.ClearNoticeText();
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Localisation;
|
|||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osu.Game.Localisation;
|
using osu.Game.Localisation;
|
||||||
@ -95,11 +96,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
|||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
Text = TabletSettingsStrings.NoTabletDetected,
|
Text = TabletSettingsStrings.NoTabletDetected,
|
||||||
},
|
},
|
||||||
new SettingsNoticeText(colours)
|
new LinkFlowContainer(cp => cp.Colour = colours.Yellow)
|
||||||
{
|
{
|
||||||
TextAnchor = Anchor.TopCentre,
|
TextAnchor = Anchor.TopCentre,
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
}.With(t =>
|
}.With(t =>
|
||||||
{
|
{
|
||||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux)
|
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux)
|
||||||
|
@ -127,9 +127,12 @@ namespace osu.Game.Overlays.Settings.Sections
|
|||||||
dropdownItems.Add(skin.ToLive(realm));
|
dropdownItems.Add(skin.ToLive(realm));
|
||||||
dropdownItems.Insert(protectedCount, random_skin_info);
|
dropdownItems.Insert(protectedCount, random_skin_info);
|
||||||
|
|
||||||
skinDropdown.Items = dropdownItems;
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
skinDropdown.Items = dropdownItems;
|
||||||
|
|
||||||
updateSelectedSkinFromConfig();
|
updateSelectedSkinFromConfig();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSelectedSkinFromConfig()
|
private void updateSelectedSkinFromConfig()
|
||||||
|
@ -61,7 +61,10 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
|
|||||||
|
|
||||||
user.BindValueChanged(u =>
|
user.BindValueChanged(u =>
|
||||||
{
|
{
|
||||||
backgroundSourceDropdown.WarningText = u.NewValue?.IsSupporter != true ? UserInterfaceStrings.NotSupporterNote : default;
|
if (u.NewValue?.IsSupporter != true)
|
||||||
|
backgroundSourceDropdown.SetNoticeText(UserInterfaceStrings.NotSupporterNote, true);
|
||||||
|
else
|
||||||
|
backgroundSourceDropdown.ClearNoticeText();
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings
|
|||||||
|
|
||||||
private SpriteText labelText;
|
private SpriteText labelText;
|
||||||
|
|
||||||
private OsuTextFlowContainer warningText;
|
private OsuTextFlowContainer noticeText;
|
||||||
|
|
||||||
public bool ShowsDefaultIndicator = true;
|
public bool ShowsDefaultIndicator = true;
|
||||||
private readonly Container defaultValueIndicatorContainer;
|
private readonly Container defaultValueIndicatorContainer;
|
||||||
@ -70,27 +70,32 @@ namespace osu.Game.Overlays.Settings
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
|
/// Clear any warning text.
|
||||||
/// Generally used to recommend the user change their setting as the current one is considered sub-optimal.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public LocalisableString? WarningText
|
public void ClearNoticeText()
|
||||||
{
|
{
|
||||||
set
|
noticeText?.Expire();
|
||||||
|
noticeText = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
|
||||||
|
/// Generally used to provide feedback to a user about a sub-optimal setting.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">The text to display.</param>
|
||||||
|
/// <param name="isWarning">Whether the text is in a warning state. Will decide how this is visually represented.</param>
|
||||||
|
public void SetNoticeText(LocalisableString text, bool isWarning = false)
|
||||||
|
{
|
||||||
|
ClearNoticeText();
|
||||||
|
|
||||||
|
// construct lazily for cases where the label is not needed (may be provided by the Control).
|
||||||
|
FlowContent.Add(noticeText = new LinkFlowContainer(cp => cp.Colour = isWarning ? colours.Yellow : colours.Green)
|
||||||
{
|
{
|
||||||
bool hasValue = value != default;
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
if (warningText == null)
|
Margin = new MarginPadding { Bottom = 5 },
|
||||||
{
|
Text = text,
|
||||||
if (!hasValue)
|
});
|
||||||
return;
|
|
||||||
|
|
||||||
// construct lazily for cases where the label is not needed (may be provided by the Control).
|
|
||||||
FlowContent.Add(warningText = new SettingsNoticeText(colours) { Margin = new MarginPadding { Bottom = 5 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
warningText.Alpha = hasValue ? 1 : 0;
|
|
||||||
warningText.Text = value ?? default;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual Bindable<T> Current
|
public virtual Bindable<T> Current
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Settings
|
|
||||||
{
|
|
||||||
public class SettingsNoticeText : LinkFlowContainer
|
|
||||||
{
|
|
||||||
public SettingsNoticeText(OsuColour colours)
|
|
||||||
: base(s => s.Colour = colours.Yellow)
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X;
|
|
||||||
AutoSizeAxes = Axes.Y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(ChatOverlayV2 chat)
|
private void load(ChatOverlay chat)
|
||||||
{
|
{
|
||||||
StateContainer = chat;
|
StateContainer = chat;
|
||||||
}
|
}
|
||||||
|
@ -117,9 +117,8 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// If the provided replay frame does not have any header information, this will be a noop.
|
/// If the provided replay frame does not have any header information, this will be a noop.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="ruleset">The ruleset to be used for retrieving statistics.</param>
|
|
||||||
/// <param name="frame">The replay frame to read header statistics from.</param>
|
/// <param name="frame">The replay frame to read header statistics from.</param>
|
||||||
public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
|
public virtual void ResetFromReplayFrame(ReplayFrame frame)
|
||||||
{
|
{
|
||||||
if (frame.Header == null)
|
if (frame.Header == null)
|
||||||
return;
|
return;
|
||||||
|
@ -6,11 +6,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -88,17 +90,34 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
private readonly double accuracyPortion;
|
private readonly double accuracyPortion;
|
||||||
private readonly double comboPortion;
|
private readonly double comboPortion;
|
||||||
|
|
||||||
private int maxAchievableCombo;
|
/// <summary>
|
||||||
|
/// Scoring values for a perfect play.
|
||||||
|
/// </summary>
|
||||||
|
public ScoringValues MaximumScoringValues
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!beatmapApplied)
|
||||||
|
throw new InvalidOperationException($"Cannot access maximum scoring values before calling {nameof(ApplyBeatmap)}.");
|
||||||
|
|
||||||
|
return maximumScoringValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ScoringValues maximumScoringValues;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum achievable base score.
|
/// Scoring values for the current play assuming all perfect hits.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private double maxBaseScore;
|
/// <remarks>
|
||||||
|
/// This is only used to determine the accuracy with respect to the current point in time for an ongoing play session.
|
||||||
|
/// </remarks>
|
||||||
|
private ScoringValues currentMaximumScoringValues;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum number of basic (non-tick and non-bonus) hitobjects.
|
/// Scoring values for the current play.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private int maxBasicHitObjects;
|
private ScoringValues currentScoringValues;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
|
/// The maximum <see cref="HitResult"/> of a basic (non-tick and non-bonus) hitobject.
|
||||||
@ -106,9 +125,6 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private HitResult? maxBasicResult;
|
private HitResult? maxBasicResult;
|
||||||
|
|
||||||
private double rollingMaxBaseScore;
|
|
||||||
private double baseScore;
|
|
||||||
private int basicHitObjects;
|
|
||||||
private bool beatmapApplied;
|
private bool beatmapApplied;
|
||||||
|
|
||||||
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
|
private readonly Dictionary<HitResult, int> scoreResultCounts = new Dictionary<HitResult, int>();
|
||||||
@ -163,6 +179,10 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
|
|
||||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
|
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
|
||||||
|
|
||||||
|
// Always update the maximum scoring values.
|
||||||
|
applyResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
|
||||||
|
currentMaximumScoringValues.MaxCombo += result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
|
||||||
|
|
||||||
if (!result.Type.IsScorable())
|
if (!result.Type.IsScorable())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -171,16 +191,8 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
else if (result.Type.BreaksCombo())
|
else if (result.Type.BreaksCombo())
|
||||||
Combo.Value = 0;
|
Combo.Value = 0;
|
||||||
|
|
||||||
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
|
applyResult(result.Type, ref currentScoringValues);
|
||||||
|
currentScoringValues.MaxCombo = HighestCombo.Value;
|
||||||
if (!result.Type.IsBonus())
|
|
||||||
{
|
|
||||||
baseScore += scoreIncrease;
|
|
||||||
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Type.IsBasic())
|
|
||||||
basicHitObjects++;
|
|
||||||
|
|
||||||
hitEvents.Add(CreateHitEvent(result));
|
hitEvents.Add(CreateHitEvent(result));
|
||||||
lastHitObject = result.HitObject;
|
lastHitObject = result.HitObject;
|
||||||
@ -188,6 +200,20 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
updateScore();
|
updateScore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void applyResult(HitResult result, ref ScoringValues scoringValues)
|
||||||
|
{
|
||||||
|
if (!result.IsScorable())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (result.IsBonus())
|
||||||
|
scoringValues.BonusScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||||
|
else
|
||||||
|
scoringValues.BaseScore += result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||||
|
|
||||||
|
if (result.IsBasic())
|
||||||
|
scoringValues.CountBasicHitObjects++;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
|
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -206,19 +232,15 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
|
|
||||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
|
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
|
||||||
|
|
||||||
|
// Always update the maximum scoring values.
|
||||||
|
revertResult(result.Judgement.MaxResult, ref currentMaximumScoringValues);
|
||||||
|
currentMaximumScoringValues.MaxCombo -= result.Judgement.MaxResult.IncreasesCombo() ? 1 : 0;
|
||||||
|
|
||||||
if (!result.Type.IsScorable())
|
if (!result.Type.IsScorable())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
|
revertResult(result.Type, ref currentScoringValues);
|
||||||
|
currentScoringValues.MaxCombo = HighestCombo.Value;
|
||||||
if (!result.Type.IsBonus())
|
|
||||||
{
|
|
||||||
baseScore -= scoreIncrease;
|
|
||||||
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.Type.IsBasic())
|
|
||||||
basicHitObjects--;
|
|
||||||
|
|
||||||
Debug.Assert(hitEvents.Count > 0);
|
Debug.Assert(hitEvents.Count > 0);
|
||||||
lastHitObject = hitEvents[^1].LastHitObject;
|
lastHitObject = hitEvents[^1].LastHitObject;
|
||||||
@ -227,14 +249,24 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
updateScore();
|
updateScore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void revertResult(HitResult result, ref ScoringValues scoringValues)
|
||||||
|
{
|
||||||
|
if (!result.IsScorable())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (result.IsBonus())
|
||||||
|
scoringValues.BonusScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||||
|
else
|
||||||
|
scoringValues.BaseScore -= result.IsHit() ? Judgement.ToNumericResult(result) : 0;
|
||||||
|
|
||||||
|
if (result.IsBasic())
|
||||||
|
scoringValues.CountBasicHitObjects--;
|
||||||
|
}
|
||||||
|
|
||||||
private void updateScore()
|
private void updateScore()
|
||||||
{
|
{
|
||||||
double rollingAccuracyRatio = rollingMaxBaseScore > 0 ? baseScore / rollingMaxBaseScore : 1;
|
Accuracy.Value = currentMaximumScoringValues.BaseScore > 0 ? currentScoringValues.BaseScore / currentMaximumScoringValues.BaseScore : 1;
|
||||||
double accuracyRatio = maxBaseScore > 0 ? baseScore / maxBaseScore : 1;
|
TotalScore.Value = ComputeScore(Mode.Value, currentScoringValues, maximumScoringValues);
|
||||||
double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1;
|
|
||||||
|
|
||||||
Accuracy.Value = rollingAccuracyRatio;
|
|
||||||
TotalScore.Value = ComputeScore(Mode.Value, accuracyRatio, comboRatio, getBonusScore(scoreResultCounts), maxBasicHitObjects);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -246,22 +278,15 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||||
|
[Pure]
|
||||||
public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo)
|
public double ComputeFinalScore(ScoringMode mode, ScoreInfo scoreInfo)
|
||||||
{
|
{
|
||||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||||
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
|
||||||
|
|
||||||
extractFromStatistics(ruleset,
|
ExtractScoringValues(scoreInfo, out var current, out var maximum);
|
||||||
scoreInfo.Statistics,
|
|
||||||
out double extractedBaseScore,
|
|
||||||
out double extractedMaxBaseScore,
|
|
||||||
out int extractedMaxCombo,
|
|
||||||
out int extractedBasicHitObjects);
|
|
||||||
|
|
||||||
double accuracyRatio = extractedMaxBaseScore > 0 ? extractedBaseScore / extractedMaxBaseScore : 1;
|
return ComputeScore(mode, current, maximum);
|
||||||
double comboRatio = extractedMaxCombo > 0 ? (double)scoreInfo.MaxCombo / extractedMaxCombo : 1;
|
|
||||||
|
|
||||||
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), extractedBasicHitObjects);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -273,6 +298,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||||
|
[Pure]
|
||||||
public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo)
|
public double ComputePartialScore(ScoringMode mode, ScoreInfo scoreInfo)
|
||||||
{
|
{
|
||||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||||
@ -281,17 +307,9 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
if (!beatmapApplied)
|
if (!beatmapApplied)
|
||||||
throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}.");
|
throw new InvalidOperationException($"Cannot compute partial score without calling {nameof(ApplyBeatmap)}.");
|
||||||
|
|
||||||
extractFromStatistics(ruleset,
|
ExtractScoringValues(scoreInfo, out var current, out _);
|
||||||
scoreInfo.Statistics,
|
|
||||||
out double extractedBaseScore,
|
|
||||||
out _,
|
|
||||||
out _,
|
|
||||||
out _);
|
|
||||||
|
|
||||||
double accuracyRatio = maxBaseScore > 0 ? extractedBaseScore / maxBaseScore : 1;
|
return ComputeScore(mode, current, MaximumScoringValues);
|
||||||
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
|
|
||||||
|
|
||||||
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), maxBasicHitObjects);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -305,6 +323,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
/// <param name="scoreInfo">The <see cref="ScoreInfo"/> to compute the total score of.</param>
|
||||||
/// <param name="maxAchievableCombo">The maximum achievable combo for the provided beatmap.</param>
|
/// <param name="maxAchievableCombo">The maximum achievable combo for the provided beatmap.</param>
|
||||||
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
/// <returns>The total score in the given <see cref="ScoringMode"/>.</returns>
|
||||||
|
[Pure]
|
||||||
public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo)
|
public double ComputeFinalLegacyScore(ScoringMode mode, ScoreInfo scoreInfo, int maxAchievableCombo)
|
||||||
{
|
{
|
||||||
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
|
||||||
@ -313,26 +332,30 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
double accuracyRatio = scoreInfo.Accuracy;
|
double accuracyRatio = scoreInfo.Accuracy;
|
||||||
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
|
double comboRatio = maxAchievableCombo > 0 ? (double)scoreInfo.MaxCombo / maxAchievableCombo : 1;
|
||||||
|
|
||||||
|
ExtractScoringValues(scoreInfo, out var current, out var maximum);
|
||||||
|
|
||||||
// For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score.
|
// For legacy osu!mania scores, a full-GREAT score has 100% accuracy. If combined with a full-combo, the score becomes indistinguishable from a full-PERFECT score.
|
||||||
// To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score.
|
// To get around this, the accuracy ratio is always recalculated based on the hit statistics rather than trusting the score.
|
||||||
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
|
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
|
||||||
if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3)
|
if (scoreInfo.IsLegacyScore && scoreInfo.Ruleset.OnlineID == 3 && maximum.BaseScore > 0)
|
||||||
{
|
accuracyRatio = current.BaseScore / maximum.BaseScore;
|
||||||
extractFromStatistics(
|
|
||||||
ruleset,
|
|
||||||
scoreInfo.Statistics,
|
|
||||||
out double computedBaseScore,
|
|
||||||
out double computedMaxBaseScore,
|
|
||||||
out _,
|
|
||||||
out _);
|
|
||||||
|
|
||||||
if (computedMaxBaseScore > 0)
|
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
|
||||||
accuracyRatio = computedBaseScore / computedMaxBaseScore;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
int computedBasicHitObjects = scoreInfo.Statistics.Where(kvp => kvp.Key.IsBasic()).Select(kvp => kvp.Value).Sum();
|
/// <summary>
|
||||||
|
/// Computes the total score from scoring values.
|
||||||
return ComputeScore(mode, accuracyRatio, comboRatio, getBonusScore(scoreInfo.Statistics), computedBasicHitObjects);
|
/// </summary>
|
||||||
|
/// <param name="mode">The <see cref="ScoringMode"/> to represent the score as.</param>
|
||||||
|
/// <param name="current">The current scoring values.</param>
|
||||||
|
/// <param name="maximum">The maximum scoring values.</param>
|
||||||
|
/// <returns>The total score computed from the given scoring values.</returns>
|
||||||
|
[Pure]
|
||||||
|
public double ComputeScore(ScoringMode mode, ScoringValues current, ScoringValues maximum)
|
||||||
|
{
|
||||||
|
double accuracyRatio = maximum.BaseScore > 0 ? current.BaseScore / maximum.BaseScore : 1;
|
||||||
|
double comboRatio = maximum.MaxCombo > 0 ? (double)current.MaxCombo / maximum.MaxCombo : 1;
|
||||||
|
return ComputeScore(mode, accuracyRatio, comboRatio, current.BonusScore, maximum.CountBasicHitObjects);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -344,6 +367,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// <param name="bonusScore">The total bonus score.</param>
|
/// <param name="bonusScore">The total bonus score.</param>
|
||||||
/// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param>
|
/// <param name="totalBasicHitObjects">The total number of basic (non-tick and non-bonus) hitobjects in the beatmap.</param>
|
||||||
/// <returns>The total score computed from the given scoring component ratios.</returns>
|
/// <returns>The total score computed from the given scoring component ratios.</returns>
|
||||||
|
[Pure]
|
||||||
public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects)
|
public double ComputeScore(ScoringMode mode, double accuracyRatio, double comboRatio, double bonusScore, int totalBasicHitObjects)
|
||||||
{
|
{
|
||||||
switch (mode)
|
switch (mode)
|
||||||
@ -362,15 +386,6 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the total bonus score from score statistics.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="statistics">The score statistics.</param>
|
|
||||||
/// <returns>The total bonus score.</returns>
|
|
||||||
private double getBonusScore(IReadOnlyDictionary<HitResult, int> statistics)
|
|
||||||
=> statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
|
|
||||||
+ statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
|
|
||||||
|
|
||||||
private ScoreRank rankFrom(double acc)
|
private ScoreRank rankFrom(double acc)
|
||||||
{
|
{
|
||||||
if (acc == 1)
|
if (acc == 1)
|
||||||
@ -402,15 +417,10 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
lastHitObject = null;
|
lastHitObject = null;
|
||||||
|
|
||||||
if (storeResults)
|
if (storeResults)
|
||||||
{
|
maximumScoringValues = currentScoringValues;
|
||||||
maxAchievableCombo = HighestCombo.Value;
|
|
||||||
maxBaseScore = baseScore;
|
|
||||||
maxBasicHitObjects = basicHitObjects;
|
|
||||||
}
|
|
||||||
|
|
||||||
baseScore = 0;
|
currentScoringValues = default;
|
||||||
rollingMaxBaseScore = 0;
|
currentMaximumScoringValues = default;
|
||||||
basicHitObjects = 0;
|
|
||||||
|
|
||||||
TotalScore.Value = 0;
|
TotalScore.Value = 0;
|
||||||
Accuracy.Value = 1;
|
Accuracy.Value = 1;
|
||||||
@ -437,14 +447,19 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
|
score.TotalScore = (long)Math.Round(ComputeFinalScore(ScoringMode.Standardised, score));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
|
public override void ResetFromReplayFrame(ReplayFrame frame)
|
||||||
{
|
{
|
||||||
base.ResetFromReplayFrame(ruleset, frame);
|
base.ResetFromReplayFrame(frame);
|
||||||
|
|
||||||
if (frame.Header == null)
|
if (frame.Header == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
extractFromStatistics(ruleset, frame.Header.Statistics, out baseScore, out rollingMaxBaseScore, out _, out _);
|
extractScoringValues(frame.Header.Statistics, out var current, out var maximum);
|
||||||
|
currentScoringValues.BaseScore = current.BaseScore;
|
||||||
|
currentScoringValues.MaxCombo = frame.Header.MaxCombo;
|
||||||
|
currentMaximumScoringValues.BaseScore = maximum.BaseScore;
|
||||||
|
currentMaximumScoringValues.MaxCombo = maximum.MaxCombo;
|
||||||
|
|
||||||
HighestCombo.Value = frame.Header.MaxCombo;
|
HighestCombo.Value = frame.Header.MaxCombo;
|
||||||
|
|
||||||
scoreResultCounts.Clear();
|
scoreResultCounts.Clear();
|
||||||
@ -455,52 +470,126 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
OnResetFromReplayFrame?.Invoke();
|
OnResetFromReplayFrame?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void extractFromStatistics(Ruleset ruleset, IReadOnlyDictionary<HitResult, int> statistics, out double baseScore, out double maxBaseScore, out int maxCombo,
|
#region ScoringValue extraction
|
||||||
out int basicHitObjects)
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
|
||||||
|
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
|
||||||
|
/// </list>
|
||||||
|
/// Consumers are expected to more accurately fill in the above values through external means.
|
||||||
|
/// <para>
|
||||||
|
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
|
||||||
|
/// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="scoreInfo">The score to extract scoring values from.</param>
|
||||||
|
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
|
||||||
|
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
|
||||||
|
[Pure]
|
||||||
|
internal void ExtractScoringValues(ScoreInfo scoreInfo, out ScoringValues current, out ScoringValues maximum)
|
||||||
{
|
{
|
||||||
baseScore = 0;
|
extractScoringValues(scoreInfo.Statistics, out current, out maximum);
|
||||||
maxBaseScore = 0;
|
current.MaxCombo = scoreInfo.MaxCombo;
|
||||||
maxCombo = 0;
|
}
|
||||||
basicHitObjects = 0;
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
|
||||||
|
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
|
||||||
|
/// </list>
|
||||||
|
/// Consumers are expected to more accurately fill in the above values through external means.
|
||||||
|
/// <para>
|
||||||
|
/// <b>Ensure</b> to fill in the maximum <see cref="ScoringValues.CountBasicHitObjects"/> for use in
|
||||||
|
/// <see cref="ComputeScore(osu.Game.Rulesets.Scoring.ScoringMode,osu.Game.Scoring.ScoringValues,osu.Game.Scoring.ScoringValues)"/>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="header">The replay frame header to extract scoring values from.</param>
|
||||||
|
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
|
||||||
|
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
|
||||||
|
[Pure]
|
||||||
|
internal void ExtractScoringValues(FrameHeader header, out ScoringValues current, out ScoringValues maximum)
|
||||||
|
{
|
||||||
|
extractScoringValues(header.Statistics, out current, out maximum);
|
||||||
|
current.MaxCombo = header.MaxCombo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a best-effort extraction of hit statistics into <see cref="ScoringValues"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is useful in a variety of situations, with a few drawbacks that need to be considered:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>The current <see cref="ScoringValues.MaxCombo"/> will always be 0.</item>
|
||||||
|
/// <item>The maximum <see cref="ScoringValues.BonusScore"/> will always be 0.</item>
|
||||||
|
/// <item>The current and maximum <see cref="ScoringValues.CountBasicHitObjects"/> will always be the same value.</item>
|
||||||
|
/// </list>
|
||||||
|
/// Consumers are expected to more accurately fill in the above values (especially the current <see cref="ScoringValues.MaxCombo"/>) via external means (e.g. <see cref="ScoreInfo"/>).
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="statistics">The hit statistics to extract scoring values from.</param>
|
||||||
|
/// <param name="current">The "current" scoring values, representing the hit statistics as they appear.</param>
|
||||||
|
/// <param name="maximum">The "maximum" scoring values, representing the hit statistics as if the maximum hit result was attained each time.</param>
|
||||||
|
[Pure]
|
||||||
|
private void extractScoringValues(IReadOnlyDictionary<HitResult, int> statistics, out ScoringValues current, out ScoringValues maximum)
|
||||||
|
{
|
||||||
|
current = default;
|
||||||
|
maximum = default;
|
||||||
|
|
||||||
foreach ((HitResult result, int count) in statistics)
|
foreach ((HitResult result, int count) in statistics)
|
||||||
{
|
{
|
||||||
// Bonus scores are counted separately directly from the statistics dictionary later on.
|
if (!result.IsScorable())
|
||||||
if (!result.IsScorable() || result.IsBonus())
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// The maximum result of this judgement if it wasn't a miss.
|
if (result.IsBonus())
|
||||||
// E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
|
current.BonusScore += count * Judgement.ToNumericResult(result);
|
||||||
HitResult maxResult;
|
else
|
||||||
|
|
||||||
switch (result)
|
|
||||||
{
|
{
|
||||||
case HitResult.LargeTickHit:
|
// The maximum result of this judgement if it wasn't a miss.
|
||||||
case HitResult.LargeTickMiss:
|
// E.g. For a GOOD judgement, the max result is either GREAT/PERFECT depending on which one the ruleset uses (osu!: GREAT, osu!mania: PERFECT).
|
||||||
maxResult = HitResult.LargeTickHit;
|
HitResult maxResult;
|
||||||
break;
|
|
||||||
|
|
||||||
case HitResult.SmallTickHit:
|
switch (result)
|
||||||
case HitResult.SmallTickMiss:
|
{
|
||||||
maxResult = HitResult.SmallTickHit;
|
case HitResult.LargeTickHit:
|
||||||
break;
|
case HitResult.LargeTickMiss:
|
||||||
|
maxResult = HitResult.LargeTickHit;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
case HitResult.SmallTickHit:
|
||||||
maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
|
case HitResult.SmallTickMiss:
|
||||||
break;
|
maxResult = HitResult.SmallTickHit;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
maxResult = maxBasicResult ??= ruleset.GetHitResults().OrderByDescending(kvp => Judgement.ToNumericResult(kvp.result)).First().result;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.BaseScore += count * Judgement.ToNumericResult(result);
|
||||||
|
maximum.BaseScore += count * Judgement.ToNumericResult(maxResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
baseScore += count * Judgement.ToNumericResult(result);
|
|
||||||
maxBaseScore += count * Judgement.ToNumericResult(maxResult);
|
|
||||||
|
|
||||||
if (result.AffectsCombo())
|
if (result.AffectsCombo())
|
||||||
maxCombo += count;
|
maximum.MaxCombo += count;
|
||||||
|
|
||||||
if (result.IsBasic())
|
if (result.IsBasic())
|
||||||
basicHitObjects += count;
|
{
|
||||||
|
current.CountBasicHitObjects += count;
|
||||||
|
maximum.CountBasicHitObjects += count;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.UI
|
|||||||
{
|
{
|
||||||
public readonly KeyBindingContainer<T> KeyBindingContainer;
|
public readonly KeyBindingContainer<T> KeyBindingContainer;
|
||||||
|
|
||||||
private readonly Ruleset ruleset;
|
|
||||||
|
|
||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private ScoreProcessor scoreProcessor { get; set; }
|
private ScoreProcessor scoreProcessor { get; set; }
|
||||||
|
|
||||||
@ -57,8 +55,6 @@ namespace osu.Game.Rulesets.UI
|
|||||||
|
|
||||||
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
|
protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
|
||||||
{
|
{
|
||||||
this.ruleset = ruleset.CreateInstance();
|
|
||||||
|
|
||||||
InternalChild = KeyBindingContainer =
|
InternalChild = KeyBindingContainer =
|
||||||
CreateKeyBindingContainer(ruleset, variant, unique)
|
CreateKeyBindingContainer(ruleset, variant, unique)
|
||||||
.WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
|
.WithChild(content = new Container { RelativeSizeAxes = Axes.Both });
|
||||||
@ -85,7 +81,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case ReplayStatisticsFrameEvent statisticsStateChangeEvent:
|
case ReplayStatisticsFrameEvent statisticsStateChangeEvent:
|
||||||
scoreProcessor?.ResetFromReplayFrame(ruleset, statisticsStateChangeEvent.Frame);
|
scoreProcessor?.ResetFromReplayFrame(statisticsStateChangeEvent.Frame);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
41
osu.Game/Scoring/ScoringValues.cs
Normal file
41
osu.Game/Scoring/ScoringValues.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using MessagePack;
|
||||||
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Scoring
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public struct ScoringValues
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The sum of all "basic" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBasic"/> and <see cref="Judgement.ToNumericResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Key(0)]
|
||||||
|
public double BaseScore;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The sum of all "bonus" <see cref="HitObject"/> scoring values. See: <see cref="HitResultExtensions.IsBonus"/> and <see cref="Judgement.ToNumericResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Key(1)]
|
||||||
|
public double BonusScore;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The highest achieved combo.
|
||||||
|
/// </summary>
|
||||||
|
[Key(2)]
|
||||||
|
public int MaxCombo;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The count of "basic" <see cref="HitObject"/>s. See: <see cref="HitResultExtensions.IsBasic"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Key(3)]
|
||||||
|
public int CountBasicHitObjects;
|
||||||
|
}
|
||||||
|
}
|
@ -2,13 +2,16 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Effects;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Screens.Edit.Components;
|
using osu.Game.Screens.Edit.Components;
|
||||||
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit
|
namespace osu.Game.Screens.Edit
|
||||||
{
|
{
|
||||||
@ -26,6 +29,14 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
Height = 60;
|
Height = 60;
|
||||||
|
|
||||||
|
Masking = true;
|
||||||
|
EdgeEffect = new EdgeEffectParameters
|
||||||
|
{
|
||||||
|
Colour = Color4.Black.Opacity(0.2f),
|
||||||
|
Type = EdgeEffectType.Shadow,
|
||||||
|
Radius = 10f,
|
||||||
|
};
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
new Box
|
new Box
|
||||||
|
@ -16,6 +16,7 @@ using osu.Game.Configuration;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||||
{
|
{
|
||||||
@ -273,7 +274,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
if (base.OnMouseDown(e))
|
if (base.OnMouseDown(e))
|
||||||
beginUserDrag();
|
beginUserDrag();
|
||||||
|
|
||||||
return true;
|
// handling right button as well breaks context menus inside the timeline, only handle left button for now.
|
||||||
|
return e.Button == MouseButton.Left;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnMouseUp(MouseUpEvent e)
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
|
@ -135,7 +135,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
|
|
||||||
Vector2 size = Vector2.One;
|
Vector2 size = Vector2.One;
|
||||||
|
|
||||||
if (indexInBar != 1)
|
if (indexInBar != 0)
|
||||||
size = BindableBeatDivisor.GetSize(divisor);
|
size = BindableBeatDivisor.GetSize(divisor);
|
||||||
|
|
||||||
var line = getNextUsableLine();
|
var line = getNextUsableLine();
|
||||||
|
@ -99,6 +99,15 @@ namespace osu.Game.Screens.Edit
|
|||||||
colourSelected = colours.Colour3;
|
colourSelected = colours.Colour3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
// Reduce flicker of rows when offset is being changed rapidly.
|
||||||
|
// Probably need to reconsider this.
|
||||||
|
FinishTransforms(true);
|
||||||
|
}
|
||||||
|
|
||||||
private bool selected;
|
private bool selected;
|
||||||
|
|
||||||
public bool Selected
|
public bool Selected
|
||||||
|
@ -61,6 +61,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
selectedGroup.BindValueChanged(group =>
|
selectedGroup.BindValueChanged(group =>
|
||||||
{
|
{
|
||||||
|
// TODO: This should scroll the selected row into view.
|
||||||
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
|
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
@ -31,18 +31,33 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
kiai.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
omitBarLine.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
scrollSpeedSlider.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
|
||||||
|
void saveChanges()
|
||||||
|
{
|
||||||
|
if (!isRebinding) ChangeHandler?.SaveState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool isRebinding;
|
||||||
|
|
||||||
protected override void OnControlPointChanged(ValueChangedEvent<EffectControlPoint> point)
|
protected override void OnControlPointChanged(ValueChangedEvent<EffectControlPoint> point)
|
||||||
{
|
{
|
||||||
if (point.NewValue != null)
|
if (point.NewValue != null)
|
||||||
{
|
{
|
||||||
|
isRebinding = true;
|
||||||
|
|
||||||
kiai.Current = point.NewValue.KiaiModeBindable;
|
kiai.Current = point.NewValue.KiaiModeBindable;
|
||||||
kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
|
||||||
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable;
|
||||||
omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
|
||||||
scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
|
scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable;
|
||||||
scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
isRebinding = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.LocalisationExtensions;
|
using osu.Framework.Extensions.LocalisationExtensions;
|
||||||
@ -31,12 +33,16 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
private IAdjustableClock metronomeClock;
|
private IAdjustableClock metronomeClock;
|
||||||
|
|
||||||
|
private Sample clunk;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OverlayColourProvider overlayColourProvider { get; set; }
|
private OverlayColourProvider overlayColourProvider { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load(AudioManager audio)
|
||||||
{
|
{
|
||||||
|
clunk = audio.Samples.Get(@"Multiplayer/countdown-tick");
|
||||||
|
|
||||||
const float taper = 25;
|
const float taper = 25;
|
||||||
const float swing_vertical_offset = -23;
|
const float swing_vertical_offset = -23;
|
||||||
const float lower_cover_height = 32;
|
const float lower_cover_height = 32;
|
||||||
@ -269,8 +275,21 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging)
|
if (currentAngle != 0 && Math.Abs(currentAngle - targetAngle) > angle * 1.8f && isSwinging)
|
||||||
{
|
{
|
||||||
using (stick.BeginDelayedSequence(beatLength / 2))
|
using (BeginDelayedSequence(beatLength / 2))
|
||||||
|
{
|
||||||
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
|
stick.FlashColour(overlayColourProvider.Content1, beatLength, Easing.OutQuint);
|
||||||
|
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
var channel = clunk?.GetChannel();
|
||||||
|
|
||||||
|
if (channel != null)
|
||||||
|
{
|
||||||
|
channel.Frequency.Value = RNG.NextDouble(0.98f, 1.02f);
|
||||||
|
channel.Play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -18,6 +19,9 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private EditorClock editorClock { get; set; }
|
private EditorClock editorClock { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap beatmap { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private Bindable<ControlPointGroup> selectedGroup { get; set; }
|
private Bindable<ControlPointGroup> selectedGroup { get; set; }
|
||||||
|
|
||||||
@ -45,6 +49,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
{
|
{
|
||||||
new Dimension(GridSizeMode.Absolute, 200),
|
new Dimension(GridSizeMode.Absolute, 200),
|
||||||
new Dimension(GridSizeMode.Absolute, 60),
|
new Dimension(GridSizeMode.Absolute, 60),
|
||||||
|
new Dimension(GridSizeMode.Absolute, 60),
|
||||||
},
|
},
|
||||||
Content = new[]
|
Content = new[]
|
||||||
{
|
{
|
||||||
@ -77,7 +82,36 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding(10),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new TimingAdjustButton(1)
|
||||||
|
{
|
||||||
|
Text = "Offset",
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.48f,
|
||||||
|
Height = 50,
|
||||||
|
Action = adjustOffset,
|
||||||
|
},
|
||||||
|
new TimingAdjustButton(0.1)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Text = "BPM",
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Width = 0.48f,
|
||||||
|
Height = 50,
|
||||||
|
Action = adjustBpm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
new Drawable[]
|
new Drawable[]
|
||||||
{
|
{
|
||||||
@ -113,6 +147,35 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void adjustOffset(double adjust)
|
||||||
|
{
|
||||||
|
// VERY TEMPORARY
|
||||||
|
var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray();
|
||||||
|
|
||||||
|
beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
|
||||||
|
|
||||||
|
double newOffset = selectedGroup.Value.Time + adjust;
|
||||||
|
|
||||||
|
foreach (var cp in currentGroupItems)
|
||||||
|
beatmap.ControlPointInfo.Add(newOffset, cp);
|
||||||
|
|
||||||
|
// the control point might not necessarily exist yet, if currentGroupItems was empty.
|
||||||
|
selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true);
|
||||||
|
|
||||||
|
if (!editorClock.IsRunning)
|
||||||
|
editorClock.Seek(newOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void adjustBpm(double adjust)
|
||||||
|
{
|
||||||
|
var timing = selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (timing == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
timing.BeatLength = 60000 / (timing.BPM + adjust);
|
||||||
|
}
|
||||||
|
|
||||||
private void tap()
|
private void tap()
|
||||||
{
|
{
|
||||||
editorClock.Seek(selectedGroup.Value.Time);
|
editorClock.Seek(selectedGroup.Value.Time);
|
||||||
|
254
osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs
Normal file
254
osu.Game/Screens/Edit/Timing/TimingAdjustButton.cs
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Timing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A button with variable constant output based on hold position and length.
|
||||||
|
/// </summary>
|
||||||
|
public class TimingAdjustButton : CompositeDrawable
|
||||||
|
{
|
||||||
|
public Action<double> Action;
|
||||||
|
|
||||||
|
private readonly double adjustAmount;
|
||||||
|
private ScheduledDelegate adjustDelegate;
|
||||||
|
|
||||||
|
private const int max_multiplier = 10;
|
||||||
|
|
||||||
|
private const int adjust_levels = 4;
|
||||||
|
|
||||||
|
private const double initial_delay = 300;
|
||||||
|
|
||||||
|
private const double minimum_delay = 80;
|
||||||
|
|
||||||
|
public Container Content { get; set; }
|
||||||
|
|
||||||
|
private double adjustDelay = initial_delay;
|
||||||
|
|
||||||
|
private readonly Box background;
|
||||||
|
|
||||||
|
private readonly OsuSpriteText text;
|
||||||
|
|
||||||
|
private Sample sample;
|
||||||
|
|
||||||
|
public LocalisableString Text
|
||||||
|
{
|
||||||
|
get => text.Text;
|
||||||
|
set => text.Text = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider colourProvider { get; set; }
|
||||||
|
|
||||||
|
public TimingAdjustButton(double adjustAmount)
|
||||||
|
{
|
||||||
|
this.adjustAmount = adjustAmount;
|
||||||
|
|
||||||
|
CornerRadius = 5;
|
||||||
|
Masking = true;
|
||||||
|
|
||||||
|
AddInternal(Content = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
background = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Depth = float.MaxValue
|
||||||
|
},
|
||||||
|
text = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Font = OsuFont.Default.With(weight: FontWeight.SemiBold),
|
||||||
|
Padding = new MarginPadding(5),
|
||||||
|
Depth = float.MinValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
sample = audio.Samples.Get(@"UI/notch-tick");
|
||||||
|
|
||||||
|
background.Colour = colourProvider.Background3;
|
||||||
|
|
||||||
|
for (int i = 1; i <= adjust_levels; i++)
|
||||||
|
{
|
||||||
|
Content.Add(new IncrementBox(i, adjustAmount));
|
||||||
|
Content.Add(new IncrementBox(-i, adjustAmount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
|
{
|
||||||
|
beginRepeat();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
|
{
|
||||||
|
adjustDelegate?.Cancel();
|
||||||
|
base.OnMouseUp(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginRepeat()
|
||||||
|
{
|
||||||
|
adjustDelegate?.Cancel();
|
||||||
|
|
||||||
|
adjustDelay = initial_delay;
|
||||||
|
adjustNext();
|
||||||
|
|
||||||
|
void adjustNext()
|
||||||
|
{
|
||||||
|
var hoveredBox = Content.OfType<IncrementBox>().FirstOrDefault(d => d.IsHovered);
|
||||||
|
|
||||||
|
if (hoveredBox != null)
|
||||||
|
{
|
||||||
|
Action(adjustAmount * hoveredBox.Multiplier);
|
||||||
|
|
||||||
|
adjustDelay = Math.Max(minimum_delay, adjustDelay * 0.9f);
|
||||||
|
|
||||||
|
hoveredBox.Flash();
|
||||||
|
|
||||||
|
var channel = sample?.GetChannel();
|
||||||
|
|
||||||
|
if (channel != null)
|
||||||
|
{
|
||||||
|
double repeatModifier = 0.05f * (Math.Abs(adjustDelay - initial_delay) / minimum_delay);
|
||||||
|
double multiplierModifier = (hoveredBox.Multiplier / max_multiplier) * 0.2f;
|
||||||
|
|
||||||
|
channel.Frequency.Value = 1 + multiplierModifier + repeatModifier;
|
||||||
|
channel.Play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
adjustDelay = initial_delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustDelegate = Scheduler.AddDelayed(adjustNext, adjustDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class IncrementBox : CompositeDrawable
|
||||||
|
{
|
||||||
|
public readonly float Multiplier;
|
||||||
|
|
||||||
|
private readonly Box box;
|
||||||
|
private readonly OsuSpriteText text;
|
||||||
|
|
||||||
|
public IncrementBox(int index, double amount)
|
||||||
|
{
|
||||||
|
Multiplier = Math.Sign(index) * convertMultiplier(index);
|
||||||
|
|
||||||
|
float ratio = (float)index / adjust_levels;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
Width = 0.5f * Math.Abs(ratio);
|
||||||
|
|
||||||
|
Anchor direction = index < 0 ? Anchor.x2 : Anchor.x0;
|
||||||
|
|
||||||
|
Origin |= direction;
|
||||||
|
|
||||||
|
Depth = Math.Abs(index);
|
||||||
|
|
||||||
|
Anchor = Anchor.TopCentre;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
box = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Blending = BlendingParameters.Additive
|
||||||
|
},
|
||||||
|
text = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = direction,
|
||||||
|
Origin = direction,
|
||||||
|
Font = OsuFont.Default.With(size: 10, weight: FontWeight.Bold),
|
||||||
|
Text = $"{(index > 0 ? "+" : "-")}{Math.Abs(Multiplier * amount)}",
|
||||||
|
Padding = new MarginPadding(5),
|
||||||
|
Alpha = 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider colourProvider { get; set; }
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
box.Colour = colourProvider.Background1;
|
||||||
|
box.Alpha = 0.1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float convertMultiplier(int m)
|
||||||
|
{
|
||||||
|
switch (Math.Abs(m))
|
||||||
|
{
|
||||||
|
default: return 1;
|
||||||
|
|
||||||
|
case 2: return 2;
|
||||||
|
|
||||||
|
case 3: return 5;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
return max_multiplier;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnHover(HoverEvent e)
|
||||||
|
{
|
||||||
|
box.Colour = colourProvider.Colour0;
|
||||||
|
|
||||||
|
box.FadeTo(0.2f, 100, Easing.OutQuint);
|
||||||
|
text.FadeIn(100, Easing.OutQuint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
|
{
|
||||||
|
box.Colour = colourProvider.Background1;
|
||||||
|
|
||||||
|
box.FadeTo(0.1f, 500, Easing.OutQuint);
|
||||||
|
text.FadeOut(100, Easing.OutQuint);
|
||||||
|
base.OnHoverLost(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Flash()
|
||||||
|
{
|
||||||
|
box
|
||||||
|
.FadeTo(0.4f, 20, Easing.OutQuint)
|
||||||
|
.Then()
|
||||||
|
.FadeTo(0.2f, 400, Easing.OutQuint);
|
||||||
|
|
||||||
|
text
|
||||||
|
.MoveToY(-5, 20, Easing.OutQuint)
|
||||||
|
.Then()
|
||||||
|
.MoveToY(0, 400, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
public class TimingScreen : EditorScreenWithTimeline
|
public class TimingScreen : EditorScreenWithTimeline
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
|
public readonly Bindable<ControlPointGroup> SelectedGroup = new Bindable<ControlPointGroup>();
|
||||||
|
|
||||||
public TimingScreen()
|
public TimingScreen()
|
||||||
: base(EditorScreenMode.Timing)
|
: base(EditorScreenMode.Timing)
|
||||||
@ -132,6 +132,40 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
trackActivePoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Given the user has selected a control point group, we want to track any group which is
|
||||||
|
/// active at the current point in time which matches the type the user has selected.
|
||||||
|
///
|
||||||
|
/// So if the user is currently looking at a timing point and seeks into the future, a
|
||||||
|
/// future timing point would be automatically selected if it is now the new "current" point.
|
||||||
|
/// </summary>
|
||||||
|
private void trackActivePoint()
|
||||||
|
{
|
||||||
|
// For simplicity only match on the first type of the active control point.
|
||||||
|
var selectedPointType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
|
||||||
|
|
||||||
|
if (selectedPointType != null)
|
||||||
|
{
|
||||||
|
// We don't have an efficient way of looking up groups currently, only individual point types.
|
||||||
|
// To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
|
||||||
|
|
||||||
|
// Find the next group which has the same type as the selected one.
|
||||||
|
var found = Beatmap.ControlPointInfo.Groups
|
||||||
|
.Where(g => g.ControlPoints.Any(cp => cp.GetType() == selectedPointType))
|
||||||
|
.LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
selectedGroup.Value = found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void delete()
|
private void delete()
|
||||||
{
|
{
|
||||||
if (selectedGroup.Value == null)
|
if (selectedGroup.Value == null)
|
||||||
@ -144,7 +178,27 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
private void addNew()
|
private void addNew()
|
||||||
{
|
{
|
||||||
selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
|
bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
|
||||||
|
|
||||||
|
var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
|
||||||
|
|
||||||
|
if (isFirstControlPoint)
|
||||||
|
group.Add(new TimingControlPoint());
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Try and create matching types from the currently selected control point.
|
||||||
|
var selected = selectedGroup.Value;
|
||||||
|
|
||||||
|
if (selected != null && selected != group)
|
||||||
|
{
|
||||||
|
foreach (var controlPoint in selected.ControlPoints)
|
||||||
|
{
|
||||||
|
group.Add(controlPoint.DeepClone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedGroup.Value = group;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,15 +28,31 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
bpmTextEntry.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
timeSignature.Current.BindValueChanged(_ => saveChanges());
|
||||||
|
|
||||||
|
void saveChanges()
|
||||||
|
{
|
||||||
|
if (!isRebinding) ChangeHandler?.SaveState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool isRebinding;
|
||||||
|
|
||||||
protected override void OnControlPointChanged(ValueChangedEvent<TimingControlPoint> point)
|
protected override void OnControlPointChanged(ValueChangedEvent<TimingControlPoint> point)
|
||||||
{
|
{
|
||||||
if (point.NewValue != null)
|
if (point.NewValue != null)
|
||||||
{
|
{
|
||||||
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
|
isRebinding = true;
|
||||||
bpmTextEntry.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
|
||||||
|
bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable;
|
||||||
timeSignature.Current = point.NewValue.TimeSignatureBindable;
|
timeSignature.Current = point.NewValue.TimeSignatureBindable;
|
||||||
timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState());
|
|
||||||
|
isRebinding = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -26,6 +27,8 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
{
|
{
|
||||||
private const int total_waveforms = 8;
|
private const int total_waveforms = 8;
|
||||||
|
|
||||||
|
private const float corner_radius = LabelledDrawable<Drawable>.CORNER_RADIUS;
|
||||||
|
|
||||||
private readonly BindableNumber<double> beatLength = new BindableDouble();
|
private readonly BindableNumber<double> beatLength = new BindableDouble();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -42,18 +45,22 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
|
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
|
||||||
|
|
||||||
private int lastDisplayedBeatIndex;
|
private double displayedTime;
|
||||||
|
|
||||||
private double selectedGroupStartTime;
|
private double selectedGroupStartTime;
|
||||||
private double selectedGroupEndTime;
|
private double selectedGroupEndTime;
|
||||||
|
|
||||||
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
|
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
|
||||||
|
|
||||||
|
private readonly BindableBool displayLocked = new BindableBool();
|
||||||
|
|
||||||
|
private LockedOverlay lockedOverlay = null!;
|
||||||
|
|
||||||
public WaveformComparisonDisplay()
|
public WaveformComparisonDisplay()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
CornerRadius = LabelledDrawable<Drawable>.CORNER_RADIUS;
|
CornerRadius = corner_radius;
|
||||||
Masking = true;
|
Masking = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +70,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
for (int i = 0; i < total_waveforms; i++)
|
for (int i = 0; i < total_waveforms; i++)
|
||||||
{
|
{
|
||||||
AddInternal(new WaveformRow
|
AddInternal(new WaveformRow(i == total_waveforms / 2)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
RelativePositionAxes = Axes.Both,
|
RelativePositionAxes = Axes.Both,
|
||||||
@ -81,72 +88,112 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
Width = 3,
|
Width = 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddInternal(lockedOverlay = new LockedOverlay());
|
||||||
|
|
||||||
selectedGroup.BindValueChanged(_ => updateTimingGroup(), true);
|
selectedGroup.BindValueChanged(_ => updateTimingGroup(), true);
|
||||||
|
|
||||||
controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
|
controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
|
||||||
controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup());
|
controlPointGroups.BindCollectionChanged((_, __) => updateTimingGroup());
|
||||||
|
|
||||||
beatLength.BindValueChanged(_ => showFrom(lastDisplayedBeatIndex), true);
|
beatLength.BindValueChanged(_ => regenerateDisplay(true), true);
|
||||||
|
|
||||||
|
displayLocked.BindValueChanged(locked =>
|
||||||
|
{
|
||||||
|
if (locked.NewValue)
|
||||||
|
lockedOverlay.Show();
|
||||||
|
else
|
||||||
|
lockedOverlay.Hide();
|
||||||
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTimingGroup()
|
private void updateTimingGroup()
|
||||||
{
|
{
|
||||||
beatLength.UnbindBindings();
|
beatLength.UnbindBindings();
|
||||||
|
|
||||||
selectedGroupStartTime = 0;
|
|
||||||
selectedGroupEndTime = beatmap.Value.Track.Length;
|
|
||||||
|
|
||||||
var tcp = selectedGroup.Value?.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
|
var tcp = selectedGroup.Value?.ControlPoints.OfType<TimingControlPoint>().FirstOrDefault();
|
||||||
|
|
||||||
if (tcp == null)
|
if (tcp == null)
|
||||||
{
|
{
|
||||||
timingPoint = new TimingControlPoint();
|
timingPoint = new TimingControlPoint();
|
||||||
|
// During movement of a control point's offset, this clause can be hit momentarily,
|
||||||
|
// as moving a control point is implemented by removing it and inserting it at the new time.
|
||||||
|
// We don't want to reset the `selectedGroupStartTime` here as we rely on having the
|
||||||
|
// last value to update the waveform display below.
|
||||||
|
selectedGroupEndTime = beatmap.Value.Track.Length;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
timingPoint = tcp;
|
timingPoint = tcp;
|
||||||
beatLength.BindTo(timingPoint.BeatLengthBindable);
|
beatLength.BindTo(timingPoint.BeatLengthBindable);
|
||||||
|
|
||||||
selectedGroupStartTime = selectedGroup.Value?.Time ?? 0;
|
double? newStartTime = selectedGroup.Value?.Time;
|
||||||
|
double? offsetChange = newStartTime - selectedGroupStartTime;
|
||||||
|
|
||||||
var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints
|
var nextGroup = editorBeatmap.ControlPointInfo.TimingPoints
|
||||||
.SkipWhile(g => g != tcp)
|
.SkipWhile(g => g != tcp)
|
||||||
.Skip(1)
|
.Skip(1)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
|
|
||||||
if (nextGroup != null)
|
selectedGroupStartTime = newStartTime ?? 0;
|
||||||
selectedGroupEndTime = nextGroup.Time;
|
selectedGroupEndTime = nextGroup?.Time ?? beatmap.Value.Track.Length;
|
||||||
|
|
||||||
|
if (newStartTime.HasValue && offsetChange.HasValue)
|
||||||
|
{
|
||||||
|
// The offset of the selected point may have changed.
|
||||||
|
// This handles the case the user has locked the view and expects the display to update with this change.
|
||||||
|
showFromTime(displayedTime + offsetChange.Value, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e) => true;
|
protected override bool OnHover(HoverEvent e) => true;
|
||||||
|
|
||||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
{
|
{
|
||||||
float trackLength = (float)beatmap.Value.Track.Length;
|
if (!displayLocked.Value)
|
||||||
int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength);
|
{
|
||||||
|
float trackLength = (float)beatmap.Value.Track.Length;
|
||||||
|
int totalBeatsAvailable = (int)(trackLength / timingPoint.BeatLength);
|
||||||
|
|
||||||
Scheduler.AddOnce(showFrom, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable));
|
Scheduler.AddOnce(showFromBeat, (int)(e.MousePosition.X / DrawWidth * totalBeatsAvailable));
|
||||||
|
}
|
||||||
|
|
||||||
return base.OnMouseMove(e);
|
return base.OnMouseMove(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
{
|
||||||
|
displayLocked.Toggle();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (!IsHovered)
|
if (!IsHovered && !displayLocked.Value)
|
||||||
{
|
{
|
||||||
int currentBeat = (int)Math.Floor((editorClock.CurrentTimeAccurate - selectedGroupStartTime) / timingPoint.BeatLength);
|
int currentBeat = (int)Math.Floor((editorClock.CurrentTimeAccurate - selectedGroupStartTime) / timingPoint.BeatLength);
|
||||||
|
|
||||||
showFrom(currentBeat);
|
showFromBeat(currentBeat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showFrom(int beatIndex)
|
private void showFromBeat(int beatIndex) =>
|
||||||
|
showFromTime(selectedGroupStartTime + beatIndex * timingPoint.BeatLength, false);
|
||||||
|
|
||||||
|
private void showFromTime(double time, bool animated)
|
||||||
{
|
{
|
||||||
if (lastDisplayedBeatIndex == beatIndex)
|
if (displayedTime == time)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
displayedTime = time;
|
||||||
|
regenerateDisplay(animated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void regenerateDisplay(bool animated)
|
||||||
|
{
|
||||||
|
double index = (displayedTime - selectedGroupStartTime) / timingPoint.BeatLength;
|
||||||
|
|
||||||
// Chosen as a pretty usable number across all BPMs.
|
// Chosen as a pretty usable number across all BPMs.
|
||||||
// Optimally we'd want this to scale with the BPM in question, but performing
|
// Optimally we'd want this to scale with the BPM in question, but performing
|
||||||
// scaling of the display is both expensive in resampling, and decreases usability
|
// scaling of the display is both expensive in resampling, and decreases usability
|
||||||
@ -156,38 +203,115 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
float trackLength = (float)beatmap.Value.Track.Length;
|
float trackLength = (float)beatmap.Value.Track.Length;
|
||||||
float scale = trackLength / visible_width;
|
float scale = trackLength / visible_width;
|
||||||
|
|
||||||
|
const int start_offset = total_waveforms / 2;
|
||||||
|
|
||||||
// Start displaying from before the current beat
|
// Start displaying from before the current beat
|
||||||
beatIndex -= total_waveforms / 2;
|
index -= start_offset;
|
||||||
|
|
||||||
foreach (var row in InternalChildren.OfType<WaveformRow>())
|
foreach (var row in InternalChildren.OfType<WaveformRow>())
|
||||||
{
|
{
|
||||||
// offset to the required beat index.
|
// offset to the required beat index.
|
||||||
double time = selectedGroupStartTime + beatIndex * timingPoint.BeatLength;
|
double time = selectedGroupStartTime + index * timingPoint.BeatLength;
|
||||||
|
|
||||||
float offset = (float)(time - visible_width / 2) / trackLength * scale;
|
float offset = (float)(time - visible_width / 2) / trackLength * scale;
|
||||||
|
|
||||||
row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1;
|
row.Alpha = time < selectedGroupStartTime || time > selectedGroupEndTime ? 0.2f : 1;
|
||||||
row.WaveformOffset = -offset;
|
row.WaveformOffsetTo(-offset, animated);
|
||||||
row.WaveformScale = new Vector2(scale, 1);
|
row.WaveformScale = new Vector2(scale, 1);
|
||||||
row.BeatIndex = beatIndex++;
|
row.BeatIndex = (int)Math.Floor(index);
|
||||||
|
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class LockedOverlay : CompositeDrawable
|
||||||
|
{
|
||||||
|
private OsuSpriteText text = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
Masking = true;
|
||||||
|
CornerRadius = corner_radius;
|
||||||
|
BorderColour = colours.Red;
|
||||||
|
BorderThickness = 3;
|
||||||
|
Alpha = 0;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
AlwaysPresent = true,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Alpha = 0,
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = colours.Red,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
text = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Colour = colours.GrayF,
|
||||||
|
Text = "Locked",
|
||||||
|
Margin = new MarginPadding(5),
|
||||||
|
Shadow = false,
|
||||||
|
Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
lastDisplayedBeatIndex = beatIndex;
|
public override void Show()
|
||||||
|
{
|
||||||
|
this.FadeIn(100, Easing.OutQuint);
|
||||||
|
|
||||||
|
text
|
||||||
|
.FadeIn().Then().Delay(600)
|
||||||
|
.FadeOut().Then().Delay(600)
|
||||||
|
.Loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Hide()
|
||||||
|
{
|
||||||
|
this.FadeOut(100, Easing.OutQuint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class WaveformRow : CompositeDrawable
|
internal class WaveformRow : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
private readonly bool isMainRow;
|
||||||
private OsuSpriteText beatIndexText = null!;
|
private OsuSpriteText beatIndexText = null!;
|
||||||
private WaveformGraph waveformGraph = null!;
|
private WaveformGraph waveformGraph = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||||
|
|
||||||
|
public WaveformRow(bool isMainRow)
|
||||||
|
{
|
||||||
|
this.isMainRow = isMainRow;
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(IBindable<WorkingBeatmap> beatmap)
|
private void load(IBindable<WorkingBeatmap> beatmap)
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = colourProvider.Background3,
|
||||||
|
Alpha = isMainRow ? 1 : 0,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
waveformGraph = new WaveformGraph
|
waveformGraph = new WaveformGraph
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -212,7 +336,15 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
|
public int BeatIndex { set => beatIndexText.Text = value.ToString(); }
|
||||||
public Vector2 WaveformScale { set => waveformGraph.Scale = value; }
|
public Vector2 WaveformScale { set => waveformGraph.Scale = value; }
|
||||||
public float WaveformOffset { set => waveformGraph.X = value; }
|
|
||||||
|
public void WaveformOffsetTo(float value, bool animated) =>
|
||||||
|
this.TransformTo(nameof(waveformOffset), value, animated ? 300 : 0, Easing.OutQuint);
|
||||||
|
|
||||||
|
private float waveformOffset
|
||||||
|
{
|
||||||
|
get => waveformGraph.X;
|
||||||
|
set => waveformGraph.X = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio.Track;
|
using osu.Framework.Audio.Track;
|
||||||
@ -80,16 +81,13 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, clonedMods, loadCancellationSource.Token)
|
difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, clonedMods, loadCancellationSource.Token)
|
||||||
.ContinueWith(task => Schedule(() =>
|
.ContinueWith(task => Schedule(() =>
|
||||||
{
|
{
|
||||||
if (task.Exception != null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
timedAttributes = task.GetResultSafely();
|
timedAttributes = task.GetResultSafely();
|
||||||
|
|
||||||
IsValid = true;
|
IsValid = true;
|
||||||
|
|
||||||
if (lastJudgement != null)
|
if (lastJudgement != null)
|
||||||
onJudgementChanged(lastJudgement);
|
onJudgementChanged(lastJudgement);
|
||||||
}));
|
}), TaskContinuationOptions.OnlyOnRanToCompletion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
32
osu.Game/Tests/Gameplay/TestGameplayState.cs
Normal file
32
osu.Game/Tests/Gameplay/TestGameplayState.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// 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 enable
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Gameplay
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Static class providing a <see cref="Create"/> convenience method to retrieve a correctly-initialised <see cref="GameplayState"/> instance in testing scenarios.
|
||||||
|
/// </summary>
|
||||||
|
public static class TestGameplayState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a correctly-initialised <see cref="GameplayState"/> instance for use in testing.
|
||||||
|
/// </summary>
|
||||||
|
public static GameplayState Create(Ruleset ruleset, IReadOnlyList<Mod>? mods = null, Score? score = null)
|
||||||
|
{
|
||||||
|
var beatmap = new TestBeatmap(ruleset.RulesetInfo);
|
||||||
|
var workingBeatmap = new TestWorkingBeatmap(beatmap);
|
||||||
|
var playableBeatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
||||||
|
|
||||||
|
return new GameplayState(playableBeatmap, ruleset, mods, score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -67,7 +68,25 @@ namespace osu.Game.Tests.Visual.OnlinePlay
|
|||||||
// To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead.
|
// To get around this, the BeatmapManager is looked up from the dependencies provided to the children of the test scene instead.
|
||||||
var beatmapManager = dependencies.Get<BeatmapManager>();
|
var beatmapManager = dependencies.Get<BeatmapManager>();
|
||||||
|
|
||||||
((DummyAPIAccess)API).HandleRequest = request => handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
|
((DummyAPIAccess)API).HandleRequest = request =>
|
||||||
|
{
|
||||||
|
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
|
// Because some of the handlers use realm, we need to ensure the game is still alive when firing.
|
||||||
|
// If we don't, a stray `PerformAsync` could hit an `ObjectDisposedException` if running too late.
|
||||||
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
bool result = handler.HandleRequest(request, API.LocalUser.Value, beatmapManager);
|
||||||
|
tcs.SetResult(result);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
#pragma warning disable RS0030
|
||||||
|
// We can't GetResultSafely() here (will fail with "Can't use GetResultSafely from inside an async operation."), but Wait is safe enough due to
|
||||||
|
// the task being a TaskCompletionSource.
|
||||||
|
// Importantly, this doesn't deadlock because of the scheduler call above running inline where feasible (see the `false` argument).
|
||||||
|
return tcs.Task.Result;
|
||||||
|
#pragma warning restore RS0030
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -48,9 +48,8 @@ namespace osu.Game.Utils
|
|||||||
|
|
||||||
options.AutoSessionTracking = true;
|
options.AutoSessionTracking = true;
|
||||||
options.IsEnvironmentUser = false;
|
options.IsEnvironmentUser = false;
|
||||||
// The reported release needs to match release tags on github in order for sentry
|
// The reported release needs to match version as reported to Sentry in .github/workflows/sentry-release.yml
|
||||||
// to automatically associate and track against releases.
|
options.Release = $"osu@{game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty)}";
|
||||||
options.Release = game.Version.Replace($@"-{OsuGameBase.BUILD_SUFFIX}", string.Empty);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Logger.NewEntry += processLogEntry;
|
Logger.NewEntry += processLogEntry;
|
||||||
|
@ -29,13 +29,14 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.417.0">
|
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.417.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.11.2" />
|
<PackageReference Include="Realm" Version="10.11.2" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2022.528.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2022.530.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.17.1" />
|
<PackageReference Include="Sentry" Version="3.17.1" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.528.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.530.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
||||||
@ -84,7 +84,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2022.528.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2022.530.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
<PackageReference Include="SharpCompress" Version="0.31.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
|
Loading…
Reference in New Issue
Block a user