mirror of
https://github.com/ppy/osu.git
synced 2025-01-28 18:12:56 +08:00
Merge branch 'master' into skin-blueprint-aspect-lock
This commit is contained in:
commit
0fa90a80d4
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
explosion = new LegacyRollingCounter(skin, LegacyFont.Combo)
|
explosion = new LegacyRollingCounter(LegacyFont.Combo)
|
||||||
{
|
{
|
||||||
Alpha = 0.65f,
|
Alpha = 0.65f,
|
||||||
Blending = BlendingParameters.Additive,
|
Blending = BlendingParameters.Additive,
|
||||||
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Scale = new Vector2(1.5f),
|
Scale = new Vector2(1.5f),
|
||||||
},
|
},
|
||||||
counter = new LegacyRollingCounter(skin, LegacyFont.Combo)
|
counter = new LegacyRollingCounter(LegacyFont.Combo)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
||||||
|
|
||||||
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.2);
|
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime, 0.5);
|
||||||
|
|
||||||
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ using JetBrains.Annotations;
|
|||||||
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.Game.Audio;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.UI.Scrolling;
|
using osu.Game.Rulesets.UI.Scrolling;
|
||||||
using osu.Game.Rulesets.Mania.UI;
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
@ -24,6 +25,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
|||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private ManiaPlayfield playfield { get; set; }
|
private ManiaPlayfield playfield { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the samples that are played by this object during gameplay.
|
||||||
|
/// </summary>
|
||||||
|
public ISampleInfo[] GetGameplaySamples() => Samples.Samples;
|
||||||
|
|
||||||
protected override float SamplePlaybackPosition
|
protected override float SamplePlaybackPosition
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
public const float COLUMN_WIDTH = 80;
|
public const float COLUMN_WIDTH = 80;
|
||||||
public const float SPECIAL_COLUMN_WIDTH = 70;
|
public const float SPECIAL_COLUMN_WIDTH = 70;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For hitsounds played by this <see cref="Column"/> (i.e. not as a result of hitting a hitobject),
|
||||||
|
/// a certain number of samples are allowed to be played concurrently so that it feels better when spam-pressing the key.
|
||||||
|
/// </summary>
|
||||||
|
private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The index of this column as part of the whole playfield.
|
/// The index of this column as part of the whole playfield.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -38,6 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
internal readonly Container TopLevelContainer;
|
internal readonly Container TopLevelContainer;
|
||||||
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
|
private readonly DrawablePool<PoolableHitExplosion> hitExplosionPool;
|
||||||
private readonly OrderedHitPolicy hitPolicy;
|
private readonly OrderedHitPolicy hitPolicy;
|
||||||
|
private readonly Container<SkinnableSound> hitSounds;
|
||||||
|
|
||||||
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
public Container UnderlayElements => HitObjectArea.UnderlayElements;
|
||||||
|
|
||||||
@ -64,6 +71,12 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
RelativeSizeAxes = Axes.Both
|
RelativeSizeAxes = Axes.Both
|
||||||
},
|
},
|
||||||
background,
|
background,
|
||||||
|
hitSounds = new Container<SkinnableSound>
|
||||||
|
{
|
||||||
|
Name = "Column samples pool",
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new SkinnableSound()).ToArray()
|
||||||
|
},
|
||||||
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
|
TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both }
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -120,6 +133,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
|
HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int nextHitSoundIndex;
|
||||||
|
|
||||||
public bool OnPressed(ManiaAction action)
|
public bool OnPressed(ManiaAction action)
|
||||||
{
|
{
|
||||||
if (action != Action.Value)
|
if (action != Action.Value)
|
||||||
@ -131,7 +146,15 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
|
HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
|
||||||
HitObjectContainer.Objects.LastOrDefault();
|
HitObjectContainer.Objects.LastOrDefault();
|
||||||
|
|
||||||
nextObject?.PlaySamples();
|
if (nextObject is DrawableManiaHitObject maniaObject)
|
||||||
|
{
|
||||||
|
var hitSound = hitSounds[nextHitSoundIndex];
|
||||||
|
|
||||||
|
hitSound.Samples = maniaObject.GetGameplaySamples();
|
||||||
|
hitSound.Play();
|
||||||
|
|
||||||
|
nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
Child = new SkinProvidingContainer(new DefaultSkin())
|
Child = new SkinProvidingContainer(new DefaultSkin(null))
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = drawableHitCircle = new DrawableHitCircle(hitCircle)
|
Child = drawableHitCircle = new DrawableHitCircle(hitCircle)
|
||||||
|
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
Scale = new Vector2(SPRITE_SCALE),
|
Scale = new Vector2(SPRITE_SCALE),
|
||||||
Y = SPINNER_TOP_OFFSET + 115,
|
Y = SPINNER_TOP_OFFSET + 115,
|
||||||
},
|
},
|
||||||
bonusCounter = new LegacySpriteText(source, LegacyFont.Score)
|
bonusCounter = new LegacySpriteText(LegacyFont.Score)
|
||||||
{
|
{
|
||||||
Alpha = 0f,
|
Alpha = 0f,
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
Scale = new Vector2(SPRITE_SCALE),
|
Scale = new Vector2(SPRITE_SCALE),
|
||||||
Position = new Vector2(-87, 445 + spm_hide_offset),
|
Position = new Vector2(-87, 445 + spm_hide_offset),
|
||||||
},
|
},
|
||||||
spmCounter = new LegacySpriteText(source, LegacyFont.Score)
|
spmCounter = new LegacySpriteText(LegacyFont.Score)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
|
@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
if (!this.HasFont(LegacyFont.HitCircle))
|
if (!this.HasFont(LegacyFont.HitCircle))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new LegacySpriteText(Source, LegacyFont.HitCircle)
|
return new LegacySpriteText(LegacyFont.HitCircle)
|
||||||
{
|
{
|
||||||
// stable applies a blanket 0.8x scale to hitcircle fonts
|
// stable applies a blanket 0.8x scale to hitcircle fonts
|
||||||
Scale = new Vector2(0.8f),
|
Scale = new Vector2(0.8f),
|
||||||
|
@ -53,9 +53,9 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
|||||||
Client.RoomSetupAction = room =>
|
Client.RoomSetupAction = room =>
|
||||||
{
|
{
|
||||||
room.State = MultiplayerRoomState.Playing;
|
room.State = MultiplayerRoomState.Playing;
|
||||||
room.Users.Add(new MultiplayerRoomUser(55)
|
room.Users.Add(new MultiplayerRoomUser(PLAYER_1_ID)
|
||||||
{
|
{
|
||||||
User = new User { Id = 55 },
|
User = new User { Id = PLAYER_1_ID },
|
||||||
State = MultiplayerUserState.Playing
|
State = MultiplayerUserState.Playing
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
223
osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
Normal file
223
osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.OnlinePlay
|
||||||
|
{
|
||||||
|
[HeadlessTest]
|
||||||
|
public class TestSceneCatchUpSyncManager : OsuTestScene
|
||||||
|
{
|
||||||
|
private TestManualClock master;
|
||||||
|
private CatchUpSyncManager syncManager;
|
||||||
|
|
||||||
|
private TestSpectatorPlayerClock player1;
|
||||||
|
private TestSpectatorPlayerClock player2;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
syncManager = new CatchUpSyncManager(master = new TestManualClock());
|
||||||
|
syncManager.AddPlayerClock(player1 = new TestSpectatorPlayerClock(1));
|
||||||
|
syncManager.AddPlayerClock(player2 = new TestSpectatorPlayerClock(2));
|
||||||
|
|
||||||
|
Schedule(() => Child = syncManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMasterClockStartsWhenAllPlayerClocksHaveFrames()
|
||||||
|
{
|
||||||
|
setWaiting(() => player1, false);
|
||||||
|
assertMasterState(false);
|
||||||
|
assertPlayerClockState(() => player1, false);
|
||||||
|
assertPlayerClockState(() => player2, false);
|
||||||
|
|
||||||
|
setWaiting(() => player2, false);
|
||||||
|
assertMasterState(true);
|
||||||
|
assertPlayerClockState(() => player1, true);
|
||||||
|
assertPlayerClockState(() => player2, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMasterClockDoesNotStartWhenNoneReadyForMaximumDelayTime()
|
||||||
|
{
|
||||||
|
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
|
||||||
|
assertMasterState(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMasterClockStartsWhenAnyReadyForMaximumDelayTime()
|
||||||
|
{
|
||||||
|
setWaiting(() => player1, false);
|
||||||
|
AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
|
||||||
|
assertMasterState(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayerClockDoesNotCatchUpWhenSlightlyOutOfSync()
|
||||||
|
{
|
||||||
|
setAllWaiting(false);
|
||||||
|
|
||||||
|
setMasterTime(CatchUpSyncManager.SYNC_TARGET + 1);
|
||||||
|
assertCatchingUp(() => player1, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayerClockStartsCatchingUpWhenTooFarBehind()
|
||||||
|
{
|
||||||
|
setAllWaiting(false);
|
||||||
|
|
||||||
|
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1);
|
||||||
|
assertCatchingUp(() => player1, true);
|
||||||
|
assertCatchingUp(() => player2, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayerClockKeepsCatchingUpWhenSlightlyOutOfSync()
|
||||||
|
{
|
||||||
|
setAllWaiting(false);
|
||||||
|
|
||||||
|
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1);
|
||||||
|
setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET + 1);
|
||||||
|
assertCatchingUp(() => player1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayerClockStopsCatchingUpWhenInSync()
|
||||||
|
{
|
||||||
|
setAllWaiting(false);
|
||||||
|
|
||||||
|
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 2);
|
||||||
|
setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET);
|
||||||
|
assertCatchingUp(() => player1, false);
|
||||||
|
assertCatchingUp(() => player2, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayerClockDoesNotStopWhenSlightlyAhead()
|
||||||
|
{
|
||||||
|
setAllWaiting(false);
|
||||||
|
|
||||||
|
setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET);
|
||||||
|
assertCatchingUp(() => player1, false);
|
||||||
|
assertPlayerClockState(() => player1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayerClockStopsWhenTooFarAheadAndStartsWhenBackInSync()
|
||||||
|
{
|
||||||
|
setAllWaiting(false);
|
||||||
|
|
||||||
|
setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET - 1);
|
||||||
|
|
||||||
|
// This is a silent catchup, where IsCatchingUp = false but IsRunning = false also.
|
||||||
|
assertCatchingUp(() => player1, false);
|
||||||
|
assertPlayerClockState(() => player1, false);
|
||||||
|
|
||||||
|
setMasterTime(1);
|
||||||
|
assertCatchingUp(() => player1, false);
|
||||||
|
assertPlayerClockState(() => player1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInSyncPlayerClockDoesNotStartIfWaitingOnFrames()
|
||||||
|
{
|
||||||
|
setAllWaiting(false);
|
||||||
|
|
||||||
|
assertPlayerClockState(() => player1, true);
|
||||||
|
setWaiting(() => player1, true);
|
||||||
|
assertPlayerClockState(() => player1, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setWaiting(Func<TestSpectatorPlayerClock> playerClock, bool waiting)
|
||||||
|
=> AddStep($"set player clock {playerClock().Id} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting);
|
||||||
|
|
||||||
|
private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () =>
|
||||||
|
{
|
||||||
|
player1.WaitingOnFrames.Value = waiting;
|
||||||
|
player2.WaitingOnFrames.Value = waiting;
|
||||||
|
});
|
||||||
|
|
||||||
|
private void setMasterTime(double time)
|
||||||
|
=> AddStep($"set master = {time}", () => master.Seek(time));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// clock.Time = master.Time - offsetFromMaster
|
||||||
|
/// </summary>
|
||||||
|
private void setPlayerClockTime(Func<TestSpectatorPlayerClock> playerClock, double offsetFromMaster)
|
||||||
|
=> AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
|
||||||
|
|
||||||
|
private void assertMasterState(bool running)
|
||||||
|
=> AddAssert($"master clock {(running ? "is" : "is not")} running", () => master.IsRunning == running);
|
||||||
|
|
||||||
|
private void assertCatchingUp(Func<TestSpectatorPlayerClock> playerClock, bool catchingUp) =>
|
||||||
|
AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
|
||||||
|
|
||||||
|
private void assertPlayerClockState(Func<TestSpectatorPlayerClock> playerClock, bool running)
|
||||||
|
=> AddAssert($"player clock {playerClock().Id} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
|
||||||
|
|
||||||
|
private class TestSpectatorPlayerClock : TestManualClock, ISpectatorPlayerClock
|
||||||
|
{
|
||||||
|
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
|
||||||
|
|
||||||
|
public bool IsCatchingUp { get; set; }
|
||||||
|
|
||||||
|
public IFrameBasedClock Source
|
||||||
|
{
|
||||||
|
set => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly int Id;
|
||||||
|
|
||||||
|
public TestSpectatorPlayerClock(int id)
|
||||||
|
{
|
||||||
|
Id = id;
|
||||||
|
|
||||||
|
WaitingOnFrames.BindValueChanged(waiting =>
|
||||||
|
{
|
||||||
|
if (waiting.NewValue)
|
||||||
|
Stop();
|
||||||
|
else
|
||||||
|
Start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessFrame()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ElapsedFrameTime => 0;
|
||||||
|
|
||||||
|
public double FramesPerSecond => 0;
|
||||||
|
|
||||||
|
public FrameTimeInfo TimeInfo => default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestManualClock : ManualClock, IAdjustableClock
|
||||||
|
{
|
||||||
|
public void Start() => IsRunning = true;
|
||||||
|
|
||||||
|
public void Stop() => IsRunning = false;
|
||||||
|
|
||||||
|
public bool Seek(double position)
|
||||||
|
{
|
||||||
|
CurrentTime = position;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetSpeedAdjustments()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -167,5 +167,21 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox, new Vector2(20)));
|
AddStep("move mouse away", () => InputManager.MoveMouseTo(selectionBox, new Vector2(20)));
|
||||||
AddUntilStep("rotation handle hidden", () => rotationHandle.Alpha == 0);
|
AddUntilStep("rotation handle hidden", () => rotationHandle.Alpha == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests that hovering over two handles instantaneously from one to another does not crash or cause issues to the visibility state.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestHoverOverTwoHandlesInstantaneously()
|
||||||
|
{
|
||||||
|
AddStep("hover over top-left scale handle", () =>
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == Anchor.TopLeft)));
|
||||||
|
AddStep("hover over top-right scale handle", () =>
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<SelectionBoxScaleHandle>().Single(s => s.Anchor == Anchor.TopRight)));
|
||||||
|
AddUntilStep("top-left rotation handle hidden", () =>
|
||||||
|
this.ChildrenOfType<SelectionBoxRotationHandle>().Single(r => r.Anchor == Anchor.TopLeft).Alpha == 0);
|
||||||
|
AddUntilStep("top-right rotation handle shown", () =>
|
||||||
|
this.ChildrenOfType<SelectionBoxRotationHandle>().Single(r => r.Anchor == Anchor.TopRight).Alpha == 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +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.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
public class TestSceneComboCounter : SkinnableTestScene
|
public class TestSceneComboCounter : SkinnableTestScene
|
||||||
{
|
{
|
||||||
private IEnumerable<SkinnableComboCounter> comboCounters => CreatedDrawables.OfType<SkinnableComboCounter>();
|
|
||||||
|
|
||||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
@ -25,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("Create combo counters", () => SetContents(() => new SkinnableComboCounter()));
|
AddStep("Create combo counters", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.ComboCounter))));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
// 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 NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Skinning.Editor;
|
using osu.Game.Skinning.Editor;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -14,12 +16,17 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
private SkinEditor skinEditor;
|
private SkinEditor skinEditor;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SkinManager skinManager { get; set; }
|
||||||
|
|
||||||
|
protected override bool Autoplay => true;
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public override void SetUpSteps()
|
public override void SetUpSteps()
|
||||||
{
|
{
|
||||||
base.SetUpSteps();
|
base.SetUpSteps();
|
||||||
|
|
||||||
AddStep("add editor overlay", () =>
|
AddStep("reload skin editor", () =>
|
||||||
{
|
{
|
||||||
skinEditor?.Expire();
|
skinEditor?.Expire();
|
||||||
Player.ScaleTo(SkinEditorOverlay.VISIBLE_TARGET_SCALE);
|
Player.ScaleTo(SkinEditorOverlay.VISIBLE_TARGET_SCALE);
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
// 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 osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.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.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
@ -32,12 +30,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
SetContents(() =>
|
SetContents(() =>
|
||||||
{
|
{
|
||||||
var ruleset = new OsuRuleset();
|
var ruleset = new OsuRuleset();
|
||||||
|
var mods = new[] { ruleset.GetAutoplayMod() };
|
||||||
var working = CreateWorkingBeatmap(ruleset.RulesetInfo);
|
var working = CreateWorkingBeatmap(ruleset.RulesetInfo);
|
||||||
var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo);
|
var beatmap = working.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
||||||
|
|
||||||
var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap);
|
var drawableRuleset = ruleset.CreateDrawableRulesetWith(beatmap, mods);
|
||||||
|
|
||||||
var hudOverlay = new HUDOverlay(drawableRuleset, Array.Empty<Mod>())
|
var hudOverlay = new HUDOverlay(drawableRuleset, mods)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -7,7 +7,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("Set initial accuracy", () => scoreProcessor.Accuracy.Value = 1);
|
AddStep("Set initial accuracy", () => scoreProcessor.Accuracy.Value = 1);
|
||||||
AddStep("Create accuracy counters", () => SetContents(() => new SkinnableAccuracyCounter()));
|
AddStep("Create accuracy counters", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter))));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
@ -12,14 +10,12 @@ using osu.Game.Rulesets.Osu;
|
|||||||
using osu.Game.Rulesets.Osu.Judgements;
|
using osu.Game.Rulesets.Osu.Judgements;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
public class TestSceneSkinnableHealthDisplay : SkinnableTestScene
|
public class TestSceneSkinnableHealthDisplay : SkinnableTestScene
|
||||||
{
|
{
|
||||||
private IEnumerable<SkinnableHealthDisplay> healthDisplays => CreatedDrawables.OfType<SkinnableHealthDisplay>();
|
|
||||||
|
|
||||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||||
|
|
||||||
[Cached(typeof(HealthProcessor))]
|
[Cached(typeof(HealthProcessor))]
|
||||||
@ -28,10 +24,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("Create health displays", () =>
|
AddStep("Create health displays", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.HealthDisplay))));
|
||||||
{
|
|
||||||
SetContents(() => new SkinnableHealthDisplay());
|
|
||||||
});
|
|
||||||
AddStep(@"Reset all", delegate
|
AddStep(@"Reset all", delegate
|
||||||
{
|
{
|
||||||
healthProcessor.Health.Value = 1;
|
healthProcessor.Health.Value = 1;
|
||||||
|
@ -1,23 +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.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
public class TestSceneSkinnableScoreCounter : SkinnableTestScene
|
public class TestSceneSkinnableScoreCounter : SkinnableTestScene
|
||||||
{
|
{
|
||||||
private IEnumerable<SkinnableScoreCounter> scoreCounters => CreatedDrawables.OfType<SkinnableScoreCounter>();
|
|
||||||
|
|
||||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
@ -26,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("Create score counters", () => SetContents(() => new SkinnableScoreCounter()));
|
AddStep("Create score counters", () => SetContents(() => new SkinnableDrawable(new HUDSkinComponent(HUDSkinComponents.ScoreCounter))));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -40,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestVeryLargeScore()
|
public void TestVeryLargeScore()
|
||||||
{
|
{
|
||||||
AddStep("set large score", () => scoreCounters.ForEach(counter => scoreProcessor.TotalScore.Value = 1_000_000_000));
|
AddStep("set large score", () => scoreProcessor.TotalScore.Value = 1_000_000_000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,32 @@
|
|||||||
// 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.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
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.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online;
|
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Replays.Legacy;
|
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Replays;
|
using osu.Game.Rulesets.Osu.Replays;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
|
using osu.Game.Tests.Visual.Multiplayer;
|
||||||
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
public class TestSceneSpectator : ScreenTestScene
|
public class TestSceneSpectator : ScreenTestScene
|
||||||
{
|
{
|
||||||
|
private readonly User streamingUser = new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Test user" };
|
||||||
|
|
||||||
[Cached(typeof(SpectatorStreamingClient))]
|
[Cached(typeof(SpectatorStreamingClient))]
|
||||||
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
|
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
|
||||||
|
|
||||||
@ -214,9 +212,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
|
private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player);
|
||||||
|
|
||||||
private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(beatmapId ?? importedBeatmapId));
|
private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
|
||||||
|
|
||||||
private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(beatmapId ?? importedBeatmapId));
|
private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
|
||||||
|
|
||||||
private void checkPaused(bool state) =>
|
private void checkPaused(bool state) =>
|
||||||
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
||||||
@ -225,89 +223,17 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
AddStep("send frames", () =>
|
AddStep("send frames", () =>
|
||||||
{
|
{
|
||||||
testSpectatorStreamingClient.SendFrames(nextFrame, count);
|
testSpectatorStreamingClient.SendFrames(streamingUser.Id, nextFrame, count);
|
||||||
nextFrame += count;
|
nextFrame += count;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadSpectatingScreen()
|
private void loadSpectatingScreen()
|
||||||
{
|
{
|
||||||
AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(testSpectatorStreamingClient.StreamingUser)));
|
AddStep("load screen", () => LoadScreen(spectatorScreen = new SoloSpectator(streamingUser)));
|
||||||
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
|
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestSpectatorStreamingClient : SpectatorStreamingClient
|
|
||||||
{
|
|
||||||
public readonly User StreamingUser = new User { Id = 55, Username = "Test user" };
|
|
||||||
|
|
||||||
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
|
|
||||||
|
|
||||||
private int beatmapId;
|
|
||||||
|
|
||||||
public TestSpectatorStreamingClient()
|
|
||||||
: base(new DevelopmentEndpointConfiguration())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartPlay(int beatmapId)
|
|
||||||
{
|
|
||||||
this.beatmapId = beatmapId;
|
|
||||||
sendState(beatmapId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EndPlay(int beatmapId)
|
|
||||||
{
|
|
||||||
((ISpectatorClient)this).UserFinishedPlaying(StreamingUser.Id, new SpectatorState
|
|
||||||
{
|
|
||||||
BeatmapID = beatmapId,
|
|
||||||
RulesetID = 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
sentState = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool sentState;
|
|
||||||
|
|
||||||
public void SendFrames(int index, int count)
|
|
||||||
{
|
|
||||||
var frames = new List<LegacyReplayFrame>();
|
|
||||||
|
|
||||||
for (int i = index; i < index + count; i++)
|
|
||||||
{
|
|
||||||
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
|
|
||||||
|
|
||||||
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
|
|
||||||
}
|
|
||||||
|
|
||||||
var bundle = new FrameDataBundle(new ScoreInfo(), frames);
|
|
||||||
((ISpectatorClient)this).UserSentFrames(StreamingUser.Id, bundle);
|
|
||||||
|
|
||||||
if (!sentState)
|
|
||||||
sendState(beatmapId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void WatchUser(int userId)
|
|
||||||
{
|
|
||||||
if (!PlayingUsers.Contains(userId) && sentState)
|
|
||||||
{
|
|
||||||
// usually the server would do this.
|
|
||||||
sendState(beatmapId);
|
|
||||||
}
|
|
||||||
|
|
||||||
base.WatchUser(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendState(int beatmapId)
|
|
||||||
{
|
|
||||||
sentState = true;
|
|
||||||
((ISpectatorClient)this).UserBeganPlaying(StreamingUser.Id, new SpectatorState
|
|
||||||
{
|
|
||||||
BeatmapID = beatmapId,
|
|
||||||
RulesetID = 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class TestUserLookupCache : UserLookupCache
|
internal class TestUserLookupCache : UserLookupCache
|
||||||
{
|
{
|
||||||
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User
|
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User
|
||||||
|
@ -11,20 +11,17 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online;
|
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Replays.Legacy;
|
|
||||||
using osu.Game.Rulesets.Osu.Scoring;
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
using osu.Game.Scoring;
|
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
public class TestSceneMultiplayerSpectatorLeaderboard : MultiplayerTestScene
|
public class TestSceneMultiSpectatorLeaderboard : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
[Cached(typeof(SpectatorStreamingClient))]
|
[Cached(typeof(SpectatorStreamingClient))]
|
||||||
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
|
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
|
||||||
@ -37,11 +34,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private readonly Dictionary<int, ManualClock> clocks = new Dictionary<int, ManualClock>
|
private readonly Dictionary<int, ManualClock> clocks = new Dictionary<int, ManualClock>
|
||||||
{
|
{
|
||||||
{ 55, new ManualClock() },
|
{ PLAYER_1_ID, new ManualClock() },
|
||||||
{ 56, new ManualClock() }
|
{ PLAYER_2_ID, new ManualClock() }
|
||||||
};
|
};
|
||||||
|
|
||||||
public TestSceneMultiplayerSpectatorLeaderboard()
|
public TestSceneMultiSpectatorLeaderboard()
|
||||||
{
|
{
|
||||||
base.Content.AddRange(new Drawable[]
|
base.Content.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
@ -54,7 +51,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public new void SetUpSteps()
|
public new void SetUpSteps()
|
||||||
{
|
{
|
||||||
MultiplayerSpectatorLeaderboard leaderboard = null;
|
MultiSpectatorLeaderboard leaderboard = null;
|
||||||
|
|
||||||
AddStep("reset", () =>
|
AddStep("reset", () =>
|
||||||
{
|
{
|
||||||
@ -78,7 +75,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
var scoreProcessor = new OsuScoreProcessor();
|
var scoreProcessor = new OsuScoreProcessor();
|
||||||
scoreProcessor.ApplyBeatmap(playable);
|
scoreProcessor.ApplyBeatmap(playable);
|
||||||
|
|
||||||
LoadComponentAsync(leaderboard = new MultiplayerSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add);
|
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
|
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
|
||||||
@ -95,46 +92,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
AddStep("send frames", () =>
|
AddStep("send frames", () =>
|
||||||
{
|
{
|
||||||
// For user 55, send frames in sets of 1.
|
// For player 1, send frames in sets of 1.
|
||||||
// For user 56, send frames in sets of 10.
|
// For player 2, send frames in sets of 10.
|
||||||
for (int i = 0; i < 100; i++)
|
for (int i = 0; i < 100; i++)
|
||||||
{
|
{
|
||||||
streamingClient.SendFrames(55, i, 1);
|
streamingClient.SendFrames(PLAYER_1_ID, i, 1);
|
||||||
|
|
||||||
if (i % 10 == 0)
|
if (i % 10 == 0)
|
||||||
streamingClient.SendFrames(56, i, 10);
|
streamingClient.SendFrames(PLAYER_2_ID, i, 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
assertCombo(55, 1);
|
assertCombo(PLAYER_1_ID, 1);
|
||||||
assertCombo(56, 10);
|
assertCombo(PLAYER_2_ID, 10);
|
||||||
|
|
||||||
// Advance to a point where only user 55's frame changes.
|
// Advance to a point where only user player 1's frame changes.
|
||||||
setTime(500);
|
setTime(500);
|
||||||
assertCombo(55, 5);
|
assertCombo(PLAYER_1_ID, 5);
|
||||||
assertCombo(56, 10);
|
assertCombo(PLAYER_2_ID, 10);
|
||||||
|
|
||||||
// Advance to a point where both user's frame changes.
|
// Advance to a point where both user's frame changes.
|
||||||
setTime(1100);
|
setTime(1100);
|
||||||
assertCombo(55, 11);
|
assertCombo(PLAYER_1_ID, 11);
|
||||||
assertCombo(56, 20);
|
assertCombo(PLAYER_2_ID, 20);
|
||||||
|
|
||||||
// Advance user 56 only to a point where its frame changes.
|
// Advance user player 2 only to a point where its frame changes.
|
||||||
setTime(56, 2100);
|
setTime(PLAYER_2_ID, 2100);
|
||||||
assertCombo(55, 11);
|
assertCombo(PLAYER_1_ID, 11);
|
||||||
assertCombo(56, 30);
|
assertCombo(PLAYER_2_ID, 30);
|
||||||
|
|
||||||
// Advance both users beyond their last frame
|
// Advance both users beyond their last frame
|
||||||
setTime(101 * 100);
|
setTime(101 * 100);
|
||||||
assertCombo(55, 100);
|
assertCombo(PLAYER_1_ID, 100);
|
||||||
assertCombo(56, 100);
|
assertCombo(PLAYER_2_ID, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNoFrames()
|
public void TestNoFrames()
|
||||||
{
|
{
|
||||||
assertCombo(55, 0);
|
assertCombo(PLAYER_1_ID, 0);
|
||||||
assertCombo(56, 0);
|
assertCombo(PLAYER_2_ID, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTime(double time) => AddStep($"set time {time}", () =>
|
private void setTime(double time) => AddStep($"set time {time}", () =>
|
||||||
@ -149,71 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
private void assertCombo(int userId, int expectedCombo)
|
private void assertCombo(int userId, int expectedCombo)
|
||||||
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
|
=> AddUntilStep($"player {userId} has {expectedCombo} combo", () => this.ChildrenOfType<GameplayLeaderboardScore>().Single(s => s.User?.Id == userId).Combo.Value == expectedCombo);
|
||||||
|
|
||||||
private class TestSpectatorStreamingClient : SpectatorStreamingClient
|
|
||||||
{
|
|
||||||
private readonly Dictionary<int, int> userBeatmapDictionary = new Dictionary<int, int>();
|
|
||||||
private readonly Dictionary<int, bool> userSentStateDictionary = new Dictionary<int, bool>();
|
|
||||||
|
|
||||||
public TestSpectatorStreamingClient()
|
|
||||||
: base(new DevelopmentEndpointConfiguration())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StartPlay(int userId, int beatmapId)
|
|
||||||
{
|
|
||||||
userBeatmapDictionary[userId] = beatmapId;
|
|
||||||
userSentStateDictionary[userId] = false;
|
|
||||||
sendState(userId, beatmapId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EndPlay(int userId, int beatmapId)
|
|
||||||
{
|
|
||||||
((ISpectatorClient)this).UserFinishedPlaying(userId, new SpectatorState
|
|
||||||
{
|
|
||||||
BeatmapID = beatmapId,
|
|
||||||
RulesetID = 0,
|
|
||||||
});
|
|
||||||
userSentStateDictionary[userId] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendFrames(int userId, int index, int count)
|
|
||||||
{
|
|
||||||
var frames = new List<LegacyReplayFrame>();
|
|
||||||
|
|
||||||
for (int i = index; i < index + count; i++)
|
|
||||||
{
|
|
||||||
var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1;
|
|
||||||
frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
|
|
||||||
}
|
|
||||||
|
|
||||||
var bundle = new FrameDataBundle(new ScoreInfo { Combo = index + count }, frames);
|
|
||||||
((ISpectatorClient)this).UserSentFrames(userId, bundle);
|
|
||||||
if (!userSentStateDictionary[userId])
|
|
||||||
sendState(userId, userBeatmapDictionary[userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void WatchUser(int userId)
|
|
||||||
{
|
|
||||||
if (userSentStateDictionary[userId])
|
|
||||||
{
|
|
||||||
// usually the server would do this.
|
|
||||||
sendState(userId, userBeatmapDictionary[userId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
base.WatchUser(userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendState(int userId, int beatmapId)
|
|
||||||
{
|
|
||||||
((ISpectatorClient)this).UserBeganPlaying(userId, new SpectatorState
|
|
||||||
{
|
|
||||||
BeatmapID = beatmapId,
|
|
||||||
RulesetID = 0,
|
|
||||||
});
|
|
||||||
userSentStateDictionary[userId] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestUserLookupCache : UserLookupCache
|
private class TestUserLookupCache : UserLookupCache
|
||||||
{
|
{
|
||||||
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
|
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
|
@ -0,0 +1,313 @@
|
|||||||
|
// 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 System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Online.Spectator;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
|
{
|
||||||
|
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
[Cached(typeof(SpectatorStreamingClient))]
|
||||||
|
private TestSpectatorStreamingClient streamingClient = new TestSpectatorStreamingClient();
|
||||||
|
|
||||||
|
[Cached(typeof(UserLookupCache))]
|
||||||
|
private UserLookupCache lookupCache = new TestUserLookupCache();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuGameBase game { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmapManager { get; set; }
|
||||||
|
|
||||||
|
private MultiSpectatorScreen spectatorScreen;
|
||||||
|
|
||||||
|
private readonly List<int> playingUserIds = new List<int>();
|
||||||
|
private readonly Dictionary<int, int> nextFrame = new Dictionary<int, int>();
|
||||||
|
|
||||||
|
private BeatmapSetInfo importedSet;
|
||||||
|
private BeatmapInfo importedBeatmap;
|
||||||
|
private int importedBeatmapId;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
importedSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result;
|
||||||
|
importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
|
||||||
|
importedBeatmapId = importedBeatmap.OnlineBeatmapID ?? -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetUpSteps()
|
||||||
|
{
|
||||||
|
base.SetUpSteps();
|
||||||
|
|
||||||
|
AddStep("reset sent frames", () => nextFrame.Clear());
|
||||||
|
|
||||||
|
AddStep("add streaming client", () =>
|
||||||
|
{
|
||||||
|
Remove(streamingClient);
|
||||||
|
Add(streamingClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("finish previous gameplay", () =>
|
||||||
|
{
|
||||||
|
foreach (var id in playingUserIds)
|
||||||
|
streamingClient.EndPlay(id, importedBeatmapId);
|
||||||
|
playingUserIds.Clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDelayedStart()
|
||||||
|
{
|
||||||
|
AddStep("start players silently", () =>
|
||||||
|
{
|
||||||
|
Client.CurrentMatchPlayingUserIds.Add(PLAYER_1_ID);
|
||||||
|
Client.CurrentMatchPlayingUserIds.Add(PLAYER_2_ID);
|
||||||
|
playingUserIds.Add(PLAYER_1_ID);
|
||||||
|
playingUserIds.Add(PLAYER_2_ID);
|
||||||
|
nextFrame[PLAYER_1_ID] = 0;
|
||||||
|
nextFrame[PLAYER_2_ID] = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
loadSpectateScreen(false);
|
||||||
|
|
||||||
|
AddWaitStep("wait a bit", 10);
|
||||||
|
AddStep("load player first_player_id", () => streamingClient.StartPlay(PLAYER_1_ID, importedBeatmapId));
|
||||||
|
AddUntilStep("one player added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 1);
|
||||||
|
|
||||||
|
AddWaitStep("wait a bit", 10);
|
||||||
|
AddStep("load player second_player_id", () => streamingClient.StartPlay(PLAYER_2_ID, importedBeatmapId));
|
||||||
|
AddUntilStep("two players added", () => spectatorScreen.ChildrenOfType<Player>().Count() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestGeneral()
|
||||||
|
{
|
||||||
|
int[] userIds = Enumerable.Range(0, 4).Select(i => PLAYER_1_ID + i).ToArray();
|
||||||
|
|
||||||
|
start(userIds);
|
||||||
|
loadSpectateScreen();
|
||||||
|
|
||||||
|
sendFrames(userIds, 1000);
|
||||||
|
AddWaitStep("wait a bit", 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayersMustStartSimultaneously()
|
||||||
|
{
|
||||||
|
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
|
||||||
|
loadSpectateScreen();
|
||||||
|
|
||||||
|
// Send frames for one player only, both should remain paused.
|
||||||
|
sendFrames(PLAYER_1_ID, 20);
|
||||||
|
checkPausedInstant(PLAYER_1_ID, true);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, true);
|
||||||
|
|
||||||
|
// Send frames for the other player, both should now start playing.
|
||||||
|
sendFrames(PLAYER_2_ID, 20);
|
||||||
|
checkPausedInstant(PLAYER_1_ID, false);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayersDoNotStartSimultaneouslyIfBufferingForMaximumStartDelay()
|
||||||
|
{
|
||||||
|
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
|
||||||
|
loadSpectateScreen();
|
||||||
|
|
||||||
|
// Send frames for one player only, both should remain paused.
|
||||||
|
sendFrames(PLAYER_1_ID, 1000);
|
||||||
|
checkPausedInstant(PLAYER_1_ID, true);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, true);
|
||||||
|
|
||||||
|
// Wait for the start delay seconds...
|
||||||
|
AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction));
|
||||||
|
|
||||||
|
// Player 1 should start playing by itself, player 2 should remain paused.
|
||||||
|
checkPausedInstant(PLAYER_1_ID, false);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayersContinueWhileOthersBuffer()
|
||||||
|
{
|
||||||
|
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
|
||||||
|
loadSpectateScreen();
|
||||||
|
|
||||||
|
// Send initial frames for both players. A few more for player 1.
|
||||||
|
sendFrames(PLAYER_1_ID, 20);
|
||||||
|
sendFrames(PLAYER_2_ID, 10);
|
||||||
|
checkPausedInstant(PLAYER_1_ID, false);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, false);
|
||||||
|
|
||||||
|
// Eventually player 2 will pause, player 1 must remain running.
|
||||||
|
checkPaused(PLAYER_2_ID, true);
|
||||||
|
checkPausedInstant(PLAYER_1_ID, false);
|
||||||
|
|
||||||
|
// Eventually both players will run out of frames and should pause.
|
||||||
|
checkPaused(PLAYER_1_ID, true);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, true);
|
||||||
|
|
||||||
|
// Send more frames for the first player only. Player 1 should start playing with player 2 remaining paused.
|
||||||
|
sendFrames(PLAYER_1_ID, 20);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, true);
|
||||||
|
checkPausedInstant(PLAYER_1_ID, false);
|
||||||
|
|
||||||
|
// Send more frames for the second player. Both should be playing
|
||||||
|
sendFrames(PLAYER_2_ID, 20);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, false);
|
||||||
|
checkPausedInstant(PLAYER_1_ID, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPlayersCatchUpAfterFallingBehind()
|
||||||
|
{
|
||||||
|
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
|
||||||
|
loadSpectateScreen();
|
||||||
|
|
||||||
|
// Send initial frames for both players. A few more for player 1.
|
||||||
|
sendFrames(PLAYER_1_ID, 1000);
|
||||||
|
sendFrames(PLAYER_2_ID, 10);
|
||||||
|
checkPausedInstant(PLAYER_1_ID, false);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, false);
|
||||||
|
|
||||||
|
// Eventually player 2 will run out of frames and should pause.
|
||||||
|
checkPaused(PLAYER_2_ID, true);
|
||||||
|
AddWaitStep("wait a few more frames", 10);
|
||||||
|
|
||||||
|
// Send more frames for player 2. It should unpause.
|
||||||
|
sendFrames(PLAYER_2_ID, 1000);
|
||||||
|
checkPausedInstant(PLAYER_2_ID, false);
|
||||||
|
|
||||||
|
// Player 2 should catch up to player 1 after unpausing.
|
||||||
|
waitForCatchup(PLAYER_2_ID);
|
||||||
|
AddWaitStep("wait a bit", 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMostInSyncUserIsAudioSource()
|
||||||
|
{
|
||||||
|
start(new[] { PLAYER_1_ID, PLAYER_2_ID });
|
||||||
|
loadSpectateScreen();
|
||||||
|
|
||||||
|
assertMuted(PLAYER_1_ID, true);
|
||||||
|
assertMuted(PLAYER_2_ID, true);
|
||||||
|
|
||||||
|
sendFrames(PLAYER_1_ID, 10);
|
||||||
|
sendFrames(PLAYER_2_ID, 20);
|
||||||
|
assertMuted(PLAYER_1_ID, false);
|
||||||
|
assertMuted(PLAYER_2_ID, true);
|
||||||
|
|
||||||
|
checkPaused(PLAYER_1_ID, true);
|
||||||
|
assertMuted(PLAYER_1_ID, true);
|
||||||
|
assertMuted(PLAYER_2_ID, false);
|
||||||
|
|
||||||
|
sendFrames(PLAYER_1_ID, 100);
|
||||||
|
waitForCatchup(PLAYER_1_ID);
|
||||||
|
checkPaused(PLAYER_2_ID, true);
|
||||||
|
assertMuted(PLAYER_1_ID, false);
|
||||||
|
assertMuted(PLAYER_2_ID, true);
|
||||||
|
|
||||||
|
sendFrames(PLAYER_2_ID, 100);
|
||||||
|
waitForCatchup(PLAYER_2_ID);
|
||||||
|
assertMuted(PLAYER_1_ID, false);
|
||||||
|
assertMuted(PLAYER_2_ID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadSpectateScreen(bool waitForPlayerLoad = true)
|
||||||
|
{
|
||||||
|
AddStep("load screen", () =>
|
||||||
|
{
|
||||||
|
Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap);
|
||||||
|
Ruleset.Value = importedBeatmap.Ruleset;
|
||||||
|
|
||||||
|
LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUserIds.ToArray()));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId);
|
||||||
|
|
||||||
|
private void start(int[] userIds, int? beatmapId = null)
|
||||||
|
{
|
||||||
|
AddStep("start play", () =>
|
||||||
|
{
|
||||||
|
foreach (int id in userIds)
|
||||||
|
{
|
||||||
|
Client.CurrentMatchPlayingUserIds.Add(id);
|
||||||
|
streamingClient.StartPlay(id, beatmapId ?? importedBeatmapId);
|
||||||
|
playingUserIds.Add(id);
|
||||||
|
nextFrame[id] = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finish(int userId, int? beatmapId = null)
|
||||||
|
{
|
||||||
|
AddStep("end play", () =>
|
||||||
|
{
|
||||||
|
streamingClient.EndPlay(userId, beatmapId ?? importedBeatmapId);
|
||||||
|
playingUserIds.Remove(userId);
|
||||||
|
nextFrame.Remove(userId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendFrames(int userId, int count = 10) => sendFrames(new[] { userId }, count);
|
||||||
|
|
||||||
|
private void sendFrames(int[] userIds, int count = 10)
|
||||||
|
{
|
||||||
|
AddStep("send frames", () =>
|
||||||
|
{
|
||||||
|
foreach (int id in userIds)
|
||||||
|
{
|
||||||
|
streamingClient.SendFrames(id, nextFrame[id], count);
|
||||||
|
nextFrame[id] += count;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkPaused(int userId, bool state)
|
||||||
|
=> AddUntilStep($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
|
||||||
|
|
||||||
|
private void checkPausedInstant(int userId, bool state)
|
||||||
|
=> AddAssert($"{userId} is {(state ? "paused" : "playing")}", () => getPlayer(userId).ChildrenOfType<GameplayClockContainer>().First().GameplayClock.IsRunning != state);
|
||||||
|
|
||||||
|
private void assertMuted(int userId, bool muted)
|
||||||
|
=> AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted);
|
||||||
|
|
||||||
|
private void waitForCatchup(int userId)
|
||||||
|
=> AddUntilStep($"{userId} not catching up", () => !getInstance(userId).GameplayClock.IsCatchingUp);
|
||||||
|
|
||||||
|
private Player getPlayer(int userId) => getInstance(userId).ChildrenOfType<Player>().Single();
|
||||||
|
|
||||||
|
private PlayerArea getInstance(int userId) => spectatorScreen.ChildrenOfType<PlayerArea>().Single(p => p.UserId == userId);
|
||||||
|
|
||||||
|
internal class TestUserLookupCache : UserLookupCache
|
||||||
|
{
|
||||||
|
protected override Task<User> ComputeValueAsync(int lookup, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new User
|
||||||
|
{
|
||||||
|
Id = lookup,
|
||||||
|
Username = $"User {lookup}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,25 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Platform;
|
||||||
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.OnlinePlay.Components;
|
using osu.Game.Screens.OnlinePlay.Components;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
@ -11,7 +27,158 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
private TestMultiplayer multiplayerScreen;
|
private TestMultiplayer multiplayerScreen;
|
||||||
|
|
||||||
|
private BeatmapManager beatmaps;
|
||||||
|
private RulesetStore rulesets;
|
||||||
|
private BeatmapSetInfo importedSet;
|
||||||
|
|
||||||
|
private TestMultiplayerClient client => multiplayerScreen.Client;
|
||||||
|
private Room room => client.APIRoom;
|
||||||
|
|
||||||
public TestSceneMultiplayer()
|
public TestSceneMultiplayer()
|
||||||
|
{
|
||||||
|
loadMultiplayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(GameHost host, AudioManager audio)
|
||||||
|
{
|
||||||
|
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
|
||||||
|
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
|
||||||
|
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestUserSetToIdleWhenBeatmapDeleted()
|
||||||
|
{
|
||||||
|
loadMultiplayer();
|
||||||
|
|
||||||
|
createRoom(() => new Room
|
||||||
|
{
|
||||||
|
Name = { Value = "Test Room" },
|
||||||
|
Playlist =
|
||||||
|
{
|
||||||
|
new PlaylistItem
|
||||||
|
{
|
||||||
|
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
|
||||||
|
Ruleset = { Value = new OsuRuleset().RulesetInfo },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set user ready", () => client.ChangeState(MultiplayerUserState.Ready));
|
||||||
|
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
||||||
|
|
||||||
|
AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLocalPlayDoesNotStartWhileSpectatingWithNoBeatmap()
|
||||||
|
{
|
||||||
|
loadMultiplayer();
|
||||||
|
|
||||||
|
createRoom(() => new Room
|
||||||
|
{
|
||||||
|
Name = { Value = "Test Room" },
|
||||||
|
Playlist =
|
||||||
|
{
|
||||||
|
new PlaylistItem
|
||||||
|
{
|
||||||
|
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
|
||||||
|
Ruleset = { Value = new OsuRuleset().RulesetInfo },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("join other user (ready, host)", () =>
|
||||||
|
{
|
||||||
|
client.AddUser(new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" });
|
||||||
|
client.TransferHost(MultiplayerTestScene.PLAYER_1_ID);
|
||||||
|
client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
||||||
|
|
||||||
|
AddStep("click spectate button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerSpectateButton>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start match externally", () => client.StartMatch());
|
||||||
|
|
||||||
|
AddAssert("play not started", () => multiplayerScreen.IsCurrentScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLocalPlayStartsWhileSpectatingWhenBeatmapBecomesAvailable()
|
||||||
|
{
|
||||||
|
loadMultiplayer();
|
||||||
|
|
||||||
|
createRoom(() => new Room
|
||||||
|
{
|
||||||
|
Name = { Value = "Test Room" },
|
||||||
|
Playlist =
|
||||||
|
{
|
||||||
|
new PlaylistItem
|
||||||
|
{
|
||||||
|
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
|
||||||
|
Ruleset = { Value = new OsuRuleset().RulesetInfo },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
||||||
|
|
||||||
|
AddStep("join other user (ready, host)", () =>
|
||||||
|
{
|
||||||
|
client.AddUser(new User { Id = MultiplayerTestScene.PLAYER_1_ID, Username = "Other" });
|
||||||
|
client.TransferHost(MultiplayerTestScene.PLAYER_1_ID);
|
||||||
|
client.ChangeUserState(MultiplayerTestScene.PLAYER_1_ID, MultiplayerUserState.Ready);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("click spectate button", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerSpectateButton>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start match externally", () => client.StartMatch());
|
||||||
|
|
||||||
|
AddStep("restore beatmap", () =>
|
||||||
|
{
|
||||||
|
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
|
||||||
|
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createRoom(Func<Room> room)
|
||||||
|
{
|
||||||
|
AddStep("open room", () =>
|
||||||
|
{
|
||||||
|
multiplayerScreen.OpenNewRoom(room());
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
|
||||||
|
AddWaitStep("wait for transition", 2);
|
||||||
|
|
||||||
|
AddStep("create room", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for join", () => client.Room != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadMultiplayer()
|
||||||
{
|
{
|
||||||
AddStep("show", () =>
|
AddStep("show", () =>
|
||||||
{
|
{
|
||||||
|
@ -6,14 +6,12 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online;
|
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Replays.Legacy;
|
using osu.Game.Replays.Legacy;
|
||||||
@ -22,6 +20,7 @@ using osu.Game.Rulesets.Scoring;
|
|||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Tests.Visual.Online;
|
using osu.Game.Tests.Visual.Online;
|
||||||
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
{
|
{
|
||||||
@ -30,7 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
private const int users = 16;
|
private const int users = 16;
|
||||||
|
|
||||||
[Cached(typeof(SpectatorStreamingClient))]
|
[Cached(typeof(SpectatorStreamingClient))]
|
||||||
private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(users);
|
private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming();
|
||||||
|
|
||||||
[Cached(typeof(UserLookupCache))]
|
[Cached(typeof(UserLookupCache))]
|
||||||
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
|
private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache();
|
||||||
@ -71,7 +70,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
||||||
|
|
||||||
streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
|
for (int i = 0; i < users; i++)
|
||||||
|
streamingClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
|
||||||
|
|
||||||
Client.CurrentMatchPlayingUserIds.Clear();
|
Client.CurrentMatchPlayingUserIds.Clear();
|
||||||
Client.CurrentMatchPlayingUserIds.AddRange(streamingClient.PlayingUsers);
|
Client.CurrentMatchPlayingUserIds.AddRange(streamingClient.PlayingUsers);
|
||||||
@ -114,30 +114,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
|
AddStep("change to standardised", () => config.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestMultiplayerStreaming : SpectatorStreamingClient
|
public class TestMultiplayerStreaming : TestSpectatorStreamingClient
|
||||||
{
|
{
|
||||||
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
|
|
||||||
|
|
||||||
private readonly int totalUsers;
|
|
||||||
|
|
||||||
public TestMultiplayerStreaming(int totalUsers)
|
|
||||||
: base(new DevelopmentEndpointConfiguration())
|
|
||||||
{
|
|
||||||
this.totalUsers = totalUsers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Start(int beatmapId)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < totalUsers; i++)
|
|
||||||
{
|
|
||||||
((ISpectatorClient)this).UserBeganPlaying(i, new SpectatorState
|
|
||||||
{
|
|
||||||
BeatmapID = beatmapId,
|
|
||||||
RulesetID = 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
|
private readonly Dictionary<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
|
||||||
|
|
||||||
public void RandomlyUpdateState()
|
public void RandomlyUpdateState()
|
||||||
|
@ -119,8 +119,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddStep("join other user (ready)", () =>
|
AddStep("join other user (ready)", () =>
|
||||||
{
|
{
|
||||||
Client.AddUser(new User { Id = 55 });
|
Client.AddUser(new User { Id = PLAYER_1_ID });
|
||||||
Client.ChangeUserState(55, MultiplayerUserState.Ready);
|
Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("click spectate button", () =>
|
AddStep("click spectate button", () =>
|
||||||
|
@ -120,9 +120,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[TestCase(MultiplayerRoomState.Open)]
|
||||||
public void TestEnabledWhenRoomOpen()
|
[TestCase(MultiplayerRoomState.WaitingForLoad)]
|
||||||
|
[TestCase(MultiplayerRoomState.Playing)]
|
||||||
|
public void TestEnabledWhenRoomOpenOrInGameplay(MultiplayerRoomState roomState)
|
||||||
{
|
{
|
||||||
|
AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState));
|
||||||
assertSpectateButtonEnablement(true);
|
assertSpectateButtonEnablement(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,12 +140,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
|
AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(MultiplayerRoomState.WaitingForLoad)]
|
|
||||||
[TestCase(MultiplayerRoomState.Playing)]
|
|
||||||
[TestCase(MultiplayerRoomState.Closed)]
|
[TestCase(MultiplayerRoomState.Closed)]
|
||||||
public void TestDisabledDuringGameplayOrClosed(MultiplayerRoomState roomState)
|
public void TestDisabledWhenClosed(MultiplayerRoomState roomState)
|
||||||
{
|
{
|
||||||
AddStep($"change user to {roomState}", () => Client.ChangeRoomState(roomState));
|
AddStep($"change room to {roomState}", () => Client.ChangeRoomState(roomState));
|
||||||
assertSpectateButtonEnablement(false);
|
assertSpectateButtonEnablement(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,8 +157,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestReadyButtonEnabledWhenHostAndUsersReady()
|
public void TestReadyButtonEnabledWhenHostAndUsersReady()
|
||||||
{
|
{
|
||||||
AddStep("add user", () => Client.AddUser(new User { Id = 55 }));
|
AddStep("add user", () => Client.AddUser(new User { Id = PLAYER_1_ID }));
|
||||||
AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready));
|
AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
|
||||||
|
|
||||||
addClickSpectateButtonStep();
|
addClickSpectateButtonStep();
|
||||||
assertReadyButtonEnablement(true);
|
assertReadyButtonEnablement(true);
|
||||||
@ -168,11 +169,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
AddStep("add user and transfer host", () =>
|
AddStep("add user and transfer host", () =>
|
||||||
{
|
{
|
||||||
Client.AddUser(new User { Id = 55 });
|
Client.AddUser(new User { Id = PLAYER_1_ID });
|
||||||
Client.TransferHost(55);
|
Client.TransferHost(PLAYER_1_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("set user ready", () => Client.ChangeUserState(55, MultiplayerUserState.Ready));
|
AddStep("set user ready", () => Client.ChangeUserState(PLAYER_1_ID, MultiplayerUserState.Ready));
|
||||||
|
|
||||||
addClickSpectateButtonStep();
|
addClickSpectateButtonStep();
|
||||||
assertReadyButtonEnablement(false);
|
assertReadyButtonEnablement(false);
|
||||||
|
@ -12,7 +12,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Overlays.Dashboard;
|
using osu.Game.Overlays.Dashboard;
|
||||||
using osu.Game.Tests.Visual.Gameplay;
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
public class TestSceneCurrentlyPlayingDisplay : OsuTestScene
|
public class TestSceneCurrentlyPlayingDisplay : OsuTestScene
|
||||||
{
|
{
|
||||||
[Cached(typeof(SpectatorStreamingClient))]
|
[Cached(typeof(SpectatorStreamingClient))]
|
||||||
private TestSceneSpectator.TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSceneSpectator.TestSpectatorStreamingClient();
|
private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient();
|
||||||
|
|
||||||
private CurrentlyPlayingDisplay currentlyPlaying;
|
private CurrentlyPlayingDisplay currentlyPlaying;
|
||||||
|
|
||||||
|
@ -324,7 +324,7 @@ namespace osu.Game.Beatmaps
|
|||||||
public bool SkinLoaded => skin.IsResultAvailable;
|
public bool SkinLoaded => skin.IsResultAvailable;
|
||||||
public ISkin Skin => skin.Value;
|
public ISkin Skin => skin.Value;
|
||||||
|
|
||||||
protected virtual ISkin GetSkin() => new DefaultSkin();
|
protected virtual ISkin GetSkin() => new DefaultSkin(null);
|
||||||
private readonly RecyclableLazy<ISkin> skin;
|
private readonly RecyclableLazy<ISkin> skin;
|
||||||
|
|
||||||
public abstract Stream GetStream(string storagePath);
|
public abstract Stream GetStream(string storagePath);
|
||||||
|
@ -472,7 +472,7 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delete new file.
|
/// Delete an existing file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="model">The item to operate on.</param>
|
/// <param name="model">The item to operate on.</param>
|
||||||
/// <param name="file">The existing file to be deleted.</param>
|
/// <param name="file">The existing file to be deleted.</param>
|
||||||
|
@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Extensions
|
namespace osu.Game.Extensions
|
||||||
@ -43,5 +45,23 @@ namespace osu.Game.Extensions
|
|||||||
/// <returns>The delta vector in Parent's coordinates.</returns>
|
/// <returns>The delta vector in Parent's coordinates.</returns>
|
||||||
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
|
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
|
||||||
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
|
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
|
||||||
|
|
||||||
|
public static SkinnableInfo CreateSkinnableInfo(this Drawable component) => new SkinnableInfo(component);
|
||||||
|
|
||||||
|
public static void ApplySkinnableInfo(this Drawable component, SkinnableInfo info)
|
||||||
|
{
|
||||||
|
// todo: can probably make this better via deserialisation directly using a common interface.
|
||||||
|
component.Position = info.Position;
|
||||||
|
component.Rotation = info.Rotation;
|
||||||
|
component.Scale = info.Scale;
|
||||||
|
component.Anchor = info.Anchor;
|
||||||
|
component.Origin = info.Origin;
|
||||||
|
|
||||||
|
if (component is Container container)
|
||||||
|
{
|
||||||
|
foreach (var child in info.Children)
|
||||||
|
container.Add(child.CreateInstance());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
31
osu.Game/Extensions/TypeExtensions.cs
Normal file
31
osu.Game/Extensions/TypeExtensions.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace osu.Game.Extensions
|
||||||
|
{
|
||||||
|
internal static class TypeExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns <paramref name="type"/>'s <see cref="Type.AssemblyQualifiedName"/>
|
||||||
|
/// with the assembly version, culture and public key token values removed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method is usually used in extensibility scenarios (i.e. for custom rulesets or skins)
|
||||||
|
/// when a version-agnostic identifier associated with a C# class - potentially originating from
|
||||||
|
/// an external assembly - is needed.
|
||||||
|
/// Leaving only the type and assembly names in such a scenario allows to preserve compatibility
|
||||||
|
/// across assembly versions.
|
||||||
|
/// </remarks>
|
||||||
|
internal static string GetInvariantInstantiationInfo(this Type type)
|
||||||
|
{
|
||||||
|
string assemblyQualifiedName = type.AssemblyQualifiedName;
|
||||||
|
if (assemblyQualifiedName == null)
|
||||||
|
throw new ArgumentException($"{type}'s assembly-qualified name is null. Ensure that it is a concrete type and not a generic type parameter.", nameof(type));
|
||||||
|
|
||||||
|
return string.Join(',', assemblyQualifiedName.Split(',').Take(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
osu.Game/Graphics/UserInterface/DangerousTriangleButton.cs
Normal file
18
osu.Game/Graphics/UserInterface/DangerousTriangleButton.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics.UserInterface
|
||||||
|
{
|
||||||
|
public class DangerousTriangleButton : TriangleButton
|
||||||
|
{
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuColour colours)
|
||||||
|
{
|
||||||
|
BackgroundColour = colours.PinkDark;
|
||||||
|
Triangles.ColourDark = colours.PinkDarker;
|
||||||
|
Triangles.ColourLight = colours.Pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -48,7 +48,7 @@ namespace osu.Game.Input.Bindings
|
|||||||
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
|
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
|
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
|
||||||
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
|
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
|
||||||
new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.ToggleSkinEditor),
|
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
|
||||||
|
|
||||||
new KeyBinding(InputKey.Escape, GlobalAction.Back),
|
new KeyBinding(InputKey.Escape, GlobalAction.Back),
|
||||||
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
|
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
|
||||||
|
508
osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs
generated
Normal file
508
osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.Designer.cs
generated
Normal file
@ -0,0 +1,508 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using osu.Game.Database;
|
||||||
|
|
||||||
|
namespace osu.Game.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(OsuDbContext))]
|
||||||
|
[Migration("20210511060743_AddSkinInstantiationInfo")]
|
||||||
|
partial class AddSkinInstantiationInfo
|
||||||
|
{
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<float>("ApproachRate");
|
||||||
|
|
||||||
|
b.Property<float>("CircleSize");
|
||||||
|
|
||||||
|
b.Property<float>("DrainRate");
|
||||||
|
|
||||||
|
b.Property<float>("OverallDifficulty");
|
||||||
|
|
||||||
|
b.Property<double>("SliderMultiplier");
|
||||||
|
|
||||||
|
b.Property<double>("SliderTickRate");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.ToTable("BeatmapDifficulty");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<double>("AudioLeadIn");
|
||||||
|
|
||||||
|
b.Property<double>("BPM");
|
||||||
|
|
||||||
|
b.Property<int>("BaseDifficultyID");
|
||||||
|
|
||||||
|
b.Property<int>("BeatDivisor");
|
||||||
|
|
||||||
|
b.Property<int>("BeatmapSetInfoID");
|
||||||
|
|
||||||
|
b.Property<bool>("Countdown");
|
||||||
|
|
||||||
|
b.Property<double>("DistanceSpacing");
|
||||||
|
|
||||||
|
b.Property<bool>("EpilepsyWarning");
|
||||||
|
|
||||||
|
b.Property<int>("GridSize");
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<bool>("Hidden");
|
||||||
|
|
||||||
|
b.Property<double>("Length");
|
||||||
|
|
||||||
|
b.Property<bool>("LetterboxInBreaks");
|
||||||
|
|
||||||
|
b.Property<string>("MD5Hash");
|
||||||
|
|
||||||
|
b.Property<int?>("MetadataID");
|
||||||
|
|
||||||
|
b.Property<int?>("OnlineBeatmapID");
|
||||||
|
|
||||||
|
b.Property<string>("Path");
|
||||||
|
|
||||||
|
b.Property<int>("RulesetID");
|
||||||
|
|
||||||
|
b.Property<bool>("SpecialStyle");
|
||||||
|
|
||||||
|
b.Property<float>("StackLeniency");
|
||||||
|
|
||||||
|
b.Property<double>("StarDifficulty");
|
||||||
|
|
||||||
|
b.Property<int>("Status");
|
||||||
|
|
||||||
|
b.Property<string>("StoredBookmarks");
|
||||||
|
|
||||||
|
b.Property<double>("TimelineZoom");
|
||||||
|
|
||||||
|
b.Property<string>("Version");
|
||||||
|
|
||||||
|
b.Property<bool>("WidescreenStoryboard");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("BaseDifficultyID");
|
||||||
|
|
||||||
|
b.HasIndex("BeatmapSetInfoID");
|
||||||
|
|
||||||
|
b.HasIndex("Hash");
|
||||||
|
|
||||||
|
b.HasIndex("MD5Hash");
|
||||||
|
|
||||||
|
b.HasIndex("MetadataID");
|
||||||
|
|
||||||
|
b.HasIndex("OnlineBeatmapID")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("RulesetID");
|
||||||
|
|
||||||
|
b.ToTable("BeatmapInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Artist");
|
||||||
|
|
||||||
|
b.Property<string>("ArtistUnicode");
|
||||||
|
|
||||||
|
b.Property<string>("AudioFile");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorString")
|
||||||
|
.HasColumnName("Author");
|
||||||
|
|
||||||
|
b.Property<string>("BackgroundFile");
|
||||||
|
|
||||||
|
b.Property<int>("PreviewTime");
|
||||||
|
|
||||||
|
b.Property<string>("Source");
|
||||||
|
|
||||||
|
b.Property<string>("Tags");
|
||||||
|
|
||||||
|
b.Property<string>("Title");
|
||||||
|
|
||||||
|
b.Property<string>("TitleUnicode");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.ToTable("BeatmapMetadata");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("BeatmapSetInfoID");
|
||||||
|
|
||||||
|
b.Property<int>("FileInfoID");
|
||||||
|
|
||||||
|
b.Property<string>("Filename")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("BeatmapSetInfoID");
|
||||||
|
|
||||||
|
b.HasIndex("FileInfoID");
|
||||||
|
|
||||||
|
b.ToTable("BeatmapSetFileInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("DateAdded");
|
||||||
|
|
||||||
|
b.Property<bool>("DeletePending");
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<int?>("MetadataID");
|
||||||
|
|
||||||
|
b.Property<int?>("OnlineBeatmapSetID");
|
||||||
|
|
||||||
|
b.Property<bool>("Protected");
|
||||||
|
|
||||||
|
b.Property<int>("Status");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("DeletePending");
|
||||||
|
|
||||||
|
b.HasIndex("Hash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("MetadataID");
|
||||||
|
|
||||||
|
b.HasIndex("OnlineBeatmapSetID")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("BeatmapSetInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasColumnName("Key");
|
||||||
|
|
||||||
|
b.Property<int?>("RulesetID");
|
||||||
|
|
||||||
|
b.Property<int?>("SkinInfoID");
|
||||||
|
|
||||||
|
b.Property<string>("StringValue")
|
||||||
|
.HasColumnName("Value");
|
||||||
|
|
||||||
|
b.Property<int?>("Variant");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("SkinInfoID");
|
||||||
|
|
||||||
|
b.HasIndex("RulesetID", "Variant");
|
||||||
|
|
||||||
|
b.ToTable("Settings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<int>("ReferenceCount");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Hash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("ReferenceCount");
|
||||||
|
|
||||||
|
b.ToTable("FileInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("IntAction")
|
||||||
|
.HasColumnName("Action");
|
||||||
|
|
||||||
|
b.Property<string>("KeysString")
|
||||||
|
.HasColumnName("Keys");
|
||||||
|
|
||||||
|
b.Property<int?>("RulesetID");
|
||||||
|
|
||||||
|
b.Property<int?>("Variant");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("IntAction");
|
||||||
|
|
||||||
|
b.HasIndex("RulesetID", "Variant");
|
||||||
|
|
||||||
|
b.ToTable("KeyBinding");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int?>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<bool>("Available");
|
||||||
|
|
||||||
|
b.Property<string>("InstantiationInfo");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.Property<string>("ShortName");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("Available");
|
||||||
|
|
||||||
|
b.HasIndex("ShortName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("RulesetInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("FileInfoID");
|
||||||
|
|
||||||
|
b.Property<string>("Filename")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Property<int?>("ScoreInfoID");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("FileInfoID");
|
||||||
|
|
||||||
|
b.HasIndex("ScoreInfoID");
|
||||||
|
|
||||||
|
b.ToTable("ScoreFileInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<double>("Accuracy")
|
||||||
|
.HasColumnType("DECIMAL(1,4)");
|
||||||
|
|
||||||
|
b.Property<int>("BeatmapInfoID");
|
||||||
|
|
||||||
|
b.Property<int>("Combo");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("Date");
|
||||||
|
|
||||||
|
b.Property<bool>("DeletePending");
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<int>("MaxCombo");
|
||||||
|
|
||||||
|
b.Property<string>("ModsJson")
|
||||||
|
.HasColumnName("Mods");
|
||||||
|
|
||||||
|
b.Property<long?>("OnlineScoreID");
|
||||||
|
|
||||||
|
b.Property<double?>("PP");
|
||||||
|
|
||||||
|
b.Property<int>("Rank");
|
||||||
|
|
||||||
|
b.Property<int>("RulesetID");
|
||||||
|
|
||||||
|
b.Property<string>("StatisticsJson")
|
||||||
|
.HasColumnName("Statistics");
|
||||||
|
|
||||||
|
b.Property<long>("TotalScore");
|
||||||
|
|
||||||
|
b.Property<int?>("UserID")
|
||||||
|
.HasColumnName("UserID");
|
||||||
|
|
||||||
|
b.Property<string>("UserString")
|
||||||
|
.HasColumnName("User");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("BeatmapInfoID");
|
||||||
|
|
||||||
|
b.HasIndex("OnlineScoreID")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("RulesetID");
|
||||||
|
|
||||||
|
b.ToTable("ScoreInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<int>("FileInfoID");
|
||||||
|
|
||||||
|
b.Property<string>("Filename")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Property<int>("SkinInfoID");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("FileInfoID");
|
||||||
|
|
||||||
|
b.HasIndex("SkinInfoID");
|
||||||
|
|
||||||
|
b.ToTable("SkinFileInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("ID")
|
||||||
|
.ValueGeneratedOnAdd();
|
||||||
|
|
||||||
|
b.Property<string>("Creator");
|
||||||
|
|
||||||
|
b.Property<bool>("DeletePending");
|
||||||
|
|
||||||
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<string>("InstantiationInfo");
|
||||||
|
|
||||||
|
b.Property<string>("Name");
|
||||||
|
|
||||||
|
b.HasKey("ID");
|
||||||
|
|
||||||
|
b.HasIndex("DeletePending");
|
||||||
|
|
||||||
|
b.HasIndex("Hash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SkinInfo");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("BaseDifficultyID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
|
||||||
|
.WithMany("Beatmaps")
|
||||||
|
.HasForeignKey("BeatmapSetInfoID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
|
||||||
|
.WithMany("Beatmaps")
|
||||||
|
.HasForeignKey("MetadataID");
|
||||||
|
|
||||||
|
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RulesetID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("BeatmapSetInfoID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FileInfoID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
|
||||||
|
.WithMany("BeatmapSets")
|
||||||
|
.HasForeignKey("MetadataID");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("osu.Game.Skinning.SkinInfo")
|
||||||
|
.WithMany("Settings")
|
||||||
|
.HasForeignKey("SkinInfoID");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FileInfoID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("osu.Game.Scoring.ScoreInfo")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("ScoreInfoID");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
|
||||||
|
.WithMany("Scores")
|
||||||
|
.HasForeignKey("BeatmapInfoID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RulesetID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("FileInfoID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("osu.Game.Skinning.SkinInfo")
|
||||||
|
.WithMany("Files")
|
||||||
|
.HasForeignKey("SkinInfoID")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace osu.Game.Migrations
|
||||||
|
{
|
||||||
|
public partial class AddSkinInstantiationInfo : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "InstantiationInfo",
|
||||||
|
table: "SkinInfo",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "InstantiationInfo",
|
||||||
|
table: "SkinInfo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -141,8 +141,6 @@ namespace osu.Game.Migrations
|
|||||||
|
|
||||||
b.Property<string>("TitleUnicode");
|
b.Property<string>("TitleUnicode");
|
||||||
|
|
||||||
b.Property<string>("VideoFile");
|
|
||||||
|
|
||||||
b.HasKey("ID");
|
b.HasKey("ID");
|
||||||
|
|
||||||
b.ToTable("BeatmapMetadata");
|
b.ToTable("BeatmapMetadata");
|
||||||
@ -352,7 +350,7 @@ namespace osu.Game.Migrations
|
|||||||
|
|
||||||
b.Property<long>("TotalScore");
|
b.Property<long>("TotalScore");
|
||||||
|
|
||||||
b.Property<long?>("UserID")
|
b.Property<int?>("UserID")
|
||||||
.HasColumnName("UserID");
|
.HasColumnName("UserID");
|
||||||
|
|
||||||
b.Property<string>("UserString")
|
b.Property<string>("UserString")
|
||||||
@ -402,6 +400,8 @@ namespace osu.Game.Migrations
|
|||||||
|
|
||||||
b.Property<string>("Hash");
|
b.Property<string>("Hash");
|
||||||
|
|
||||||
|
b.Property<string>("InstantiationInfo");
|
||||||
|
|
||||||
b.Property<string>("Name");
|
b.Property<string>("Name");
|
||||||
|
|
||||||
b.HasKey("ID");
|
b.HasKey("ID");
|
||||||
|
@ -96,9 +96,6 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
if (!IsConnected.Value)
|
if (!IsConnected.Value)
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
if (newState == MultiplayerUserState.Spectating)
|
|
||||||
return Task.CompletedTask; // Not supported yet.
|
|
||||||
|
|
||||||
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
|
return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,11 @@ namespace osu.Game.Online.Spectator
|
|||||||
if (!playingUsers.Contains(userId))
|
if (!playingUsers.Contains(userId))
|
||||||
playingUsers.Add(userId);
|
playingUsers.Add(userId);
|
||||||
|
|
||||||
playingUserStates[userId] = state;
|
// UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched.
|
||||||
|
// This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29).
|
||||||
|
// We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations.
|
||||||
|
if (watchingUsers.Contains(userId))
|
||||||
|
playingUserStates[userId] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
OnUserBeganPlaying?.Invoke(userId, state);
|
OnUserBeganPlaying?.Invoke(userId, state);
|
||||||
@ -230,7 +234,7 @@ namespace osu.Game.Online.Spectator
|
|||||||
connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId);
|
connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopWatchingUser(int userId)
|
public virtual void StopWatchingUser(int userId)
|
||||||
{
|
{
|
||||||
lock (userLock)
|
lock (userLock)
|
||||||
{
|
{
|
||||||
|
@ -339,22 +339,13 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ClearButton : TriangleButton
|
public class ClearButton : DangerousTriangleButton
|
||||||
{
|
{
|
||||||
public ClearButton()
|
public ClearButton()
|
||||||
{
|
{
|
||||||
Text = "Clear";
|
Text = "Clear";
|
||||||
Size = new Vector2(80, 20);
|
Size = new Vector2(80, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OsuColour colours)
|
|
||||||
{
|
|
||||||
BackgroundColour = colours.Pink;
|
|
||||||
|
|
||||||
Triangles.ColourDark = colours.PinkDark;
|
|
||||||
Triangles.ColourLight = colours.PinkLight;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class KeyButton : Container
|
public class KeyButton : Container
|
||||||
|
@ -11,7 +11,6 @@ using osu.Game.Input;
|
|||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osu.Game.Graphics;
|
|
||||||
|
|
||||||
namespace osu.Game.Overlays.KeyBinding
|
namespace osu.Game.Overlays.KeyBinding
|
||||||
{
|
{
|
||||||
@ -55,10 +54,10 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ResetButton : TriangleButton
|
public class ResetButton : DangerousTriangleButton
|
||||||
{
|
{
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load()
|
||||||
{
|
{
|
||||||
Text = "Reset all bindings in section";
|
Text = "Reset all bindings in section";
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
@ -66,10 +65,6 @@ namespace osu.Game.Overlays.KeyBinding
|
|||||||
Height = 20;
|
Height = 20;
|
||||||
|
|
||||||
Content.CornerRadius = 5;
|
Content.CornerRadius = 5;
|
||||||
|
|
||||||
BackgroundColour = colours.PinkDark;
|
|
||||||
Triangles.ColourDark = colours.PinkDarker;
|
|
||||||
Triangles.ColourLight = colours.Pink;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ namespace osu.Game.Rulesets.Edit.Checks.Components
|
|||||||
/// <summary> An error occurred and a complete check could not be made. </summary>
|
/// <summary> An error occurred and a complete check could not be made. </summary>
|
||||||
Error,
|
Error,
|
||||||
|
|
||||||
// TODO: Negligible issues should be hidden by default.
|
|
||||||
/// <summary> A possible mistake so minor/unlikely that it can often be safely ignored. </summary>
|
/// <summary> A possible mistake so minor/unlikely that it can often be safely ignored. </summary>
|
||||||
Negligible,
|
Negligible,
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@ using JetBrains.Annotations;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Extensions.EnumExtensions;
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Extensions;
|
||||||
using osu.Game.Rulesets.Filter;
|
using osu.Game.Rulesets.Filter;
|
||||||
using osu.Game.Screens.Ranking.Statistics;
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
|
|
||||||
@ -135,7 +136,7 @@ namespace osu.Game.Rulesets
|
|||||||
Name = Description,
|
Name = Description,
|
||||||
ShortName = ShortName,
|
ShortName = ShortName,
|
||||||
ID = (this as ILegacyRuleset)?.LegacyID,
|
ID = (this as ILegacyRuleset)?.LegacyID,
|
||||||
InstantiationInfo = GetType().AssemblyQualifiedName,
|
InstantiationInfo = GetType().GetInvariantInstantiationInfo(),
|
||||||
Available = true,
|
Available = true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
|
||||||
@ -18,20 +17,7 @@ namespace osu.Game.Rulesets
|
|||||||
|
|
||||||
public string ShortName { get; set; }
|
public string ShortName { get; set; }
|
||||||
|
|
||||||
private string instantiationInfo;
|
public string InstantiationInfo { get; set; }
|
||||||
|
|
||||||
public string InstantiationInfo
|
|
||||||
{
|
|
||||||
get => instantiationInfo;
|
|
||||||
set => instantiationInfo = abbreviateInstantiationInfo(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string abbreviateInstantiationInfo(string value)
|
|
||||||
{
|
|
||||||
// exclude version onwards, matching only on namespace and type.
|
|
||||||
// this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old.
|
|
||||||
return string.Join(',', value.Split(',').Take(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool Available { get; set; }
|
public bool Available { get; set; }
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
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.Primitives;
|
using osu.Framework.Graphics.Primitives;
|
||||||
@ -24,6 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler{T}"/>.
|
/// Includes selection and manipulation support via a <see cref="Components.SelectionHandler{T}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
public abstract class BlueprintContainer<T> : CompositeDrawable, IKeyBindingHandler<PlatformAction>
|
||||||
|
where T : class
|
||||||
{
|
{
|
||||||
protected DragBox DragBox { get; private set; }
|
protected DragBox DragBox { get; private set; }
|
||||||
|
|
||||||
@ -39,6 +42,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
[Resolved(CanBeNull = true)]
|
[Resolved(CanBeNull = true)]
|
||||||
private IEditorChangeHandler changeHandler { get; set; }
|
private IEditorChangeHandler changeHandler { get; set; }
|
||||||
|
|
||||||
|
protected readonly BindableList<T> SelectedItems = new BindableList<T>();
|
||||||
|
|
||||||
protected BlueprintContainer()
|
protected BlueprintContainer()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -47,6 +52,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
|
SelectedItems.CollectionChanged += (selectedObjects, args) =>
|
||||||
|
{
|
||||||
|
switch (args.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
foreach (var o in args.NewItems)
|
||||||
|
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
foreach (var o in args.OldItems)
|
||||||
|
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
SelectionHandler = CreateSelectionHandler();
|
SelectionHandler = CreateSelectionHandler();
|
||||||
SelectionHandler.DeselectAll = deselectAll;
|
SelectionHandler.DeselectAll = deselectAll;
|
||||||
|
|
||||||
|
@ -2,10 +2,8 @@
|
|||||||
// 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.Generic;
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
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.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
@ -24,8 +22,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
|
|
||||||
protected readonly HitObjectComposer Composer;
|
protected readonly HitObjectComposer Composer;
|
||||||
|
|
||||||
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
|
|
||||||
|
|
||||||
protected EditorBlueprintContainer(HitObjectComposer composer)
|
protected EditorBlueprintContainer(HitObjectComposer composer)
|
||||||
{
|
{
|
||||||
Composer = composer;
|
Composer = composer;
|
||||||
@ -34,23 +30,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
|
SelectedItems.BindTo(Beatmap.SelectedHitObjects);
|
||||||
selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
|
|
||||||
{
|
|
||||||
switch (args.Action)
|
|
||||||
{
|
|
||||||
case NotifyCollectionChangedAction.Add:
|
|
||||||
foreach (var o in args.NewItems)
|
|
||||||
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Select();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case NotifyCollectionChangedAction.Remove:
|
|
||||||
foreach (var o in args.OldItems)
|
|
||||||
SelectionBlueprints.FirstOrDefault(b => b.Item == o)?.Deselect();
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
@ -84,8 +84,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
if (activeHandle?.IsHeld == true)
|
if (activeHandle?.IsHeld == true)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
activeHandle = rotationHandles.SingleOrDefault(h => h.IsHeld || h.IsHovered);
|
activeHandle = rotationHandles.FirstOrDefault(h => h.IsHeld || h.IsHovered);
|
||||||
activeHandle ??= allDragHandles.SingleOrDefault(h => h.IsHovered);
|
activeHandle ??= allDragHandles.FirstOrDefault(h => h.IsHovered);
|
||||||
|
|
||||||
if (activeHandle != null)
|
if (activeHandle != null)
|
||||||
{
|
{
|
||||||
|
@ -10,7 +10,7 @@ using osu.Game.Overlays;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.Edit
|
namespace osu.Game.Screens.Edit
|
||||||
{
|
{
|
||||||
public class RoundedContentEditorScreen : EditorScreen
|
public class EditorRoundedScreen : EditorScreen
|
||||||
{
|
{
|
||||||
public const int HORIZONTAL_PADDING = 100;
|
public const int HORIZONTAL_PADDING = 100;
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
protected override Container<Drawable> Content => roundedContent;
|
protected override Container<Drawable> Content => roundedContent;
|
||||||
|
|
||||||
public RoundedContentEditorScreen(EditorScreenMode mode)
|
public EditorRoundedScreen(EditorScreenMode mode)
|
||||||
: base(mode)
|
: base(mode)
|
||||||
{
|
{
|
||||||
ColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
ColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
|
44
osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs
Normal file
44
osu.Game/Screens/Edit/EditorRoundedScreenSettings.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit
|
||||||
|
{
|
||||||
|
public abstract class EditorRoundedScreenSettings : CompositeDrawable
|
||||||
|
{
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OverlayColourProvider colours)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = colours.Background4,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
new OsuScrollContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Children = CreateSections()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract IReadOnlyList<Drawable> CreateSections();
|
||||||
|
}
|
||||||
|
}
|
61
osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs
Normal file
61
osu.Game/Screens/Edit/EditorRoundedScreenSettingsSection.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit
|
||||||
|
{
|
||||||
|
public abstract class EditorRoundedScreenSettingsSection : CompositeDrawable
|
||||||
|
{
|
||||||
|
private const int header_height = 50;
|
||||||
|
|
||||||
|
protected abstract string HeaderText { get; }
|
||||||
|
|
||||||
|
protected FillFlowContainer Flow { get; private set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OverlayColourProvider colours)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X;
|
||||||
|
AutoSizeAxes = Axes.Y;
|
||||||
|
Masking = true;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Height = header_height,
|
||||||
|
Padding = new MarginPadding { Horizontal = 20 },
|
||||||
|
Child = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Text = HeaderText,
|
||||||
|
Font = new FontUsage(size: 25, weight: "bold")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Y = header_height,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Child = Flow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Padding = new MarginPadding { Horizontal = 20 },
|
||||||
|
Spacing = new Vector2(10),
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,7 @@ using osu.Game.Graphics.Containers;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Setup
|
namespace osu.Game.Screens.Edit.Setup
|
||||||
{
|
{
|
||||||
public class SetupScreen : RoundedContentEditorScreen
|
public class SetupScreen : EditorRoundedScreen
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
private SectionsContainer<SetupSection> sections = new SectionsContainer<SetupSection>();
|
private SectionsContainer<SetupSection> sections = new SectionsContainer<SetupSection>();
|
||||||
|
@ -93,7 +93,7 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
|
|
||||||
public SetupScreenTabControl()
|
public SetupScreenTabControl()
|
||||||
{
|
{
|
||||||
TabContainer.Margin = new MarginPadding { Horizontal = RoundedContentEditorScreen.HORIZONTAL_PADDING };
|
TabContainer.Margin = new MarginPadding { Horizontal = EditorRoundedScreen.HORIZONTAL_PADDING };
|
||||||
|
|
||||||
AddInternal(background = new Box
|
AddInternal(background = new Box
|
||||||
{
|
{
|
||||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
Padding = new MarginPadding
|
Padding = new MarginPadding
|
||||||
{
|
{
|
||||||
Vertical = 10,
|
Vertical = 10,
|
||||||
Horizontal = RoundedContentEditorScreen.HORIZONTAL_PADDING
|
Horizontal = EditorRoundedScreen.HORIZONTAL_PADDING
|
||||||
};
|
};
|
||||||
|
|
||||||
InternalChild = new FillFlowContainer
|
InternalChild = new FillFlowContainer
|
||||||
|
@ -2,44 +2,13 @@
|
|||||||
// 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.Generic;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osu.Game.Overlays;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Timing
|
namespace osu.Game.Screens.Edit.Timing
|
||||||
{
|
{
|
||||||
public class ControlPointSettings : CompositeDrawable
|
public class ControlPointSettings : EditorRoundedScreenSettings
|
||||||
{
|
{
|
||||||
[BackgroundDependencyLoader]
|
protected override IReadOnlyList<Drawable> CreateSections() => new Drawable[]
|
||||||
private void load(OverlayColourProvider colours)
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
Colour = colours.Background4,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
new OsuScrollContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Child = new FillFlowContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
Children = createSections()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private IReadOnlyList<Drawable> createSections() => new Drawable[]
|
|
||||||
{
|
{
|
||||||
new GroupSection(),
|
new GroupSection(),
|
||||||
new TimingSection(),
|
new TimingSection(),
|
||||||
|
@ -15,7 +15,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Timing
|
namespace osu.Game.Screens.Edit.Timing
|
||||||
{
|
{
|
||||||
public class TimingScreen : RoundedContentEditorScreen
|
public class TimingScreen : EditorRoundedScreen
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
|
private Bindable<ControlPointGroup> selectedGroup = new Bindable<ControlPointGroup>();
|
||||||
|
27
osu.Game/Screens/Edit/Verify/InterpretationSection.cs
Normal file
27
osu.Game/Screens/Edit/Verify/InterpretationSection.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Verify
|
||||||
|
{
|
||||||
|
internal class InterpretationSection : EditorRoundedScreenSettingsSection
|
||||||
|
{
|
||||||
|
protected override string HeaderText => "Interpretation";
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(VerifyScreen verify)
|
||||||
|
{
|
||||||
|
Flow.Add(new SettingsEnumDropdown<DifficultyRating>
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
TooltipText = "Affects checks that depend on difficulty level",
|
||||||
|
Current = verify.InterpretedDifficulty.GetBoundCopy()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
osu.Game/Screens/Edit/Verify/IssueList.cs
Normal file
111
osu.Game/Screens/Edit/Verify/IssueList.cs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Verify
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
public class IssueList : CompositeDrawable
|
||||||
|
{
|
||||||
|
private IssueTable table;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorClock clock { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IBindable<WorkingBeatmap> workingBeatmap { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap beatmap { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private VerifyScreen verify { get; set; }
|
||||||
|
|
||||||
|
private IBeatmapVerifier rulesetVerifier;
|
||||||
|
private BeatmapVerifier generalVerifier;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OverlayColourProvider colours)
|
||||||
|
{
|
||||||
|
generalVerifier = new BeatmapVerifier();
|
||||||
|
rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier();
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = colours.Background2,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
new OsuScrollContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = table = new IssueTable(),
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.BottomRight,
|
||||||
|
Origin = Anchor.BottomRight,
|
||||||
|
Margin = new MarginPadding(20),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new TriangleButton
|
||||||
|
{
|
||||||
|
Text = "Refresh",
|
||||||
|
Action = refresh,
|
||||||
|
Size = new Vector2(120, 40),
|
||||||
|
Anchor = Anchor.BottomRight,
|
||||||
|
Origin = Anchor.BottomRight,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
verify.InterpretedDifficulty.BindValueChanged(_ => refresh());
|
||||||
|
verify.HiddenIssueTypes.BindCollectionChanged((_, __) => refresh());
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refresh()
|
||||||
|
{
|
||||||
|
var issues = generalVerifier.Run(beatmap, workingBeatmap.Value);
|
||||||
|
|
||||||
|
if (rulesetVerifier != null)
|
||||||
|
issues = issues.Concat(rulesetVerifier.Run(beatmap, workingBeatmap.Value));
|
||||||
|
|
||||||
|
issues = filter(issues);
|
||||||
|
|
||||||
|
table.Issues = issues
|
||||||
|
.OrderBy(issue => issue.Template.Type)
|
||||||
|
.ThenBy(issue => issue.Check.Metadata.Category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Issue> filter(IEnumerable<Issue> issues)
|
||||||
|
{
|
||||||
|
return issues.Where(issue => !verify.HiddenIssueTypes.Contains(issue.Template.Type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,45 +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 System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Verify
|
namespace osu.Game.Screens.Edit.Verify
|
||||||
{
|
{
|
||||||
public class IssueSettings : CompositeDrawable
|
public class IssueSettings : EditorRoundedScreenSettings
|
||||||
{
|
{
|
||||||
[BackgroundDependencyLoader]
|
protected override IReadOnlyList<Drawable> CreateSections() => new Drawable[]
|
||||||
private void load(OsuColour colours)
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
Colour = colours.Gray3,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
new OsuScrollContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Child = new FillFlowContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
Children = createSections()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private IReadOnlyList<Drawable> createSections() => new Drawable[]
|
|
||||||
{
|
{
|
||||||
|
new InterpretationSection(),
|
||||||
|
new VisibilitySection()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ namespace osu.Game.Screens.Edit.Verify
|
|||||||
public class IssueTable : EditorTable
|
public class IssueTable : EditorTable
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private Bindable<Issue> selectedIssue { get; set; }
|
private VerifyScreen verify { get; set; }
|
||||||
|
|
||||||
|
private Bindable<Issue> selectedIssue;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private EditorClock clock { get; set; }
|
private EditorClock clock { get; set; }
|
||||||
@ -71,6 +73,7 @@ namespace osu.Game.Screens.Edit.Verify
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
selectedIssue = verify.SelectedIssue.GetBoundCopy();
|
||||||
selectedIssue.BindValueChanged(issue =>
|
selectedIssue.BindValueChanged(issue =>
|
||||||
{
|
{
|
||||||
foreach (var b in BackgroundFlow) b.Selected = b.Item == issue.NewValue;
|
foreach (var b in BackgroundFlow) b.Selected = b.Item == issue.NewValue;
|
||||||
|
@ -1,26 +1,25 @@
|
|||||||
// 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;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.Containers;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Overlays;
|
|
||||||
using osu.Game.Rulesets.Edit;
|
|
||||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Edit.Verify
|
namespace osu.Game.Screens.Edit.Verify
|
||||||
{
|
{
|
||||||
public class VerifyScreen : RoundedContentEditorScreen
|
[Cached]
|
||||||
|
public class VerifyScreen : EditorRoundedScreen
|
||||||
{
|
{
|
||||||
[Cached]
|
public readonly Bindable<Issue> SelectedIssue = new Bindable<Issue>();
|
||||||
private Bindable<Issue> selectedIssue = new Bindable<Issue>();
|
|
||||||
|
public readonly Bindable<DifficultyRating> InterpretedDifficulty = new Bindable<DifficultyRating>();
|
||||||
|
|
||||||
|
public readonly BindableList<IssueType> HiddenIssueTypes = new BindableList<IssueType> { IssueType.Negligible };
|
||||||
|
|
||||||
|
public IssueList IssueList { get; private set; }
|
||||||
|
|
||||||
public VerifyScreen()
|
public VerifyScreen()
|
||||||
: base(EditorScreenMode.Verify)
|
: base(EditorScreenMode.Verify)
|
||||||
@ -30,6 +29,10 @@ namespace osu.Game.Screens.Edit.Verify
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
|
InterpretedDifficulty.Default = EditorBeatmap.BeatmapInfo.DifficultyRating;
|
||||||
|
InterpretedDifficulty.SetDefault();
|
||||||
|
|
||||||
|
IssueList = new IssueList();
|
||||||
Child = new Container
|
Child = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -45,92 +48,12 @@ namespace osu.Game.Screens.Edit.Verify
|
|||||||
{
|
{
|
||||||
new Drawable[]
|
new Drawable[]
|
||||||
{
|
{
|
||||||
new IssueList(),
|
IssueList,
|
||||||
new IssueSettings(),
|
new IssueSettings(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public class IssueList : CompositeDrawable
|
|
||||||
{
|
|
||||||
private IssueTable table;
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private EditorClock clock { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private IBindable<WorkingBeatmap> workingBeatmap { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private EditorBeatmap beatmap { get; set; }
|
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private Bindable<Issue> selectedIssue { get; set; }
|
|
||||||
|
|
||||||
private IBeatmapVerifier rulesetVerifier;
|
|
||||||
private BeatmapVerifier generalVerifier;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(OverlayColourProvider colours)
|
|
||||||
{
|
|
||||||
generalVerifier = new BeatmapVerifier();
|
|
||||||
rulesetVerifier = beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier();
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
|
||||||
{
|
|
||||||
new Box
|
|
||||||
{
|
|
||||||
Colour = colours.Background2,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
},
|
|
||||||
new OsuScrollContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Child = table = new IssueTable(),
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
AutoSizeAxes = Axes.Both,
|
|
||||||
Anchor = Anchor.BottomRight,
|
|
||||||
Origin = Anchor.BottomRight,
|
|
||||||
Margin = new MarginPadding(20),
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new TriangleButton
|
|
||||||
{
|
|
||||||
Text = "Refresh",
|
|
||||||
Action = refresh,
|
|
||||||
Size = new Vector2(120, 40),
|
|
||||||
Anchor = Anchor.BottomRight,
|
|
||||||
Origin = Anchor.BottomRight,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refresh()
|
|
||||||
{
|
|
||||||
var issues = generalVerifier.Run(beatmap, workingBeatmap.Value);
|
|
||||||
|
|
||||||
if (rulesetVerifier != null)
|
|
||||||
issues = issues.Concat(rulesetVerifier.Run(beatmap, workingBeatmap.Value));
|
|
||||||
|
|
||||||
table.Issues = issues
|
|
||||||
.OrderBy(issue => issue.Template.Type)
|
|
||||||
.ThenBy(issue => issue.Check.Metadata.Category);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
54
osu.Game/Screens/Edit/Verify/VisibilitySection.cs
Normal file
54
osu.Game/Screens/Edit/Verify/VisibilitySection.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
|
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Verify
|
||||||
|
{
|
||||||
|
internal class VisibilitySection : EditorRoundedScreenSettingsSection
|
||||||
|
{
|
||||||
|
private readonly IssueType[] configurableIssueTypes =
|
||||||
|
{
|
||||||
|
IssueType.Warning,
|
||||||
|
IssueType.Error,
|
||||||
|
IssueType.Negligible
|
||||||
|
};
|
||||||
|
|
||||||
|
private BindableList<IssueType> hiddenIssueTypes;
|
||||||
|
|
||||||
|
protected override string HeaderText => "Visibility";
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OverlayColourProvider colours, VerifyScreen verify)
|
||||||
|
{
|
||||||
|
hiddenIssueTypes = verify.HiddenIssueTypes.GetBoundCopy();
|
||||||
|
|
||||||
|
foreach (IssueType issueType in configurableIssueTypes)
|
||||||
|
{
|
||||||
|
var checkbox = new SettingsCheckbox
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
LabelText = issueType.ToString(),
|
||||||
|
Current = { Default = !hiddenIssueTypes.Contains(issueType) }
|
||||||
|
};
|
||||||
|
|
||||||
|
checkbox.Current.SetDefault();
|
||||||
|
checkbox.Current.BindValueChanged(state =>
|
||||||
|
{
|
||||||
|
if (!state.NewValue)
|
||||||
|
hiddenIssueTypes.Add(issueType);
|
||||||
|
else
|
||||||
|
hiddenIssueTypes.Remove(issueType);
|
||||||
|
});
|
||||||
|
|
||||||
|
Flow.Add(checkbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,6 @@ using osu.Game.Online.Rooms;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Screens.Play;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Match
|
namespace osu.Game.Screens.OnlinePlay.Match
|
||||||
{
|
{
|
||||||
@ -148,12 +147,18 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
|||||||
return base.OnExiting(next);
|
return base.OnExiting(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void StartPlay(Func<Player> player)
|
protected void StartPlay()
|
||||||
{
|
{
|
||||||
sampleStart?.Play();
|
sampleStart?.Play();
|
||||||
ParentScreen?.Push(new PlayerLoader(player));
|
ParentScreen?.Push(CreateGameplayScreen());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates the gameplay screen to be entered.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The screen to enter.</returns>
|
||||||
|
protected abstract Screen CreateGameplayScreen();
|
||||||
|
|
||||||
private void selectedItemChanged()
|
private void selectedItemChanged()
|
||||||
{
|
{
|
||||||
updateWorkingBeatmap();
|
updateWorkingBeatmap();
|
||||||
|
@ -81,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
|
button.Enabled.Value = Client.Room?.State != MultiplayerRoomState.Closed && !operationInProgress.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ButtonWithTrianglesExposed : TriangleButton
|
private class ButtonWithTrianglesExposed : TriangleButton
|
||||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
{
|
{
|
||||||
base.OnResuming(last);
|
base.OnResuming(last);
|
||||||
|
|
||||||
if (client.Room != null)
|
if (client.Room != null && client.LocalUser?.State != MultiplayerUserState.Spectating)
|
||||||
client.ChangeState(MultiplayerUserState.Idle);
|
client.ChangeState(MultiplayerUserState.Idle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Screens;
|
|||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Online;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -25,6 +26,7 @@ using osu.Game.Screens.OnlinePlay.Match;
|
|||||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -353,10 +355,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
|
|
||||||
client.ChangeBeatmapAvailability(availability.NewValue);
|
client.ChangeBeatmapAvailability(availability.NewValue);
|
||||||
|
|
||||||
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
|
if (availability.NewValue.State != DownloadState.LocallyAvailable)
|
||||||
if (availability.NewValue != Online.Rooms.BeatmapAvailability.LocallyAvailable()
|
{
|
||||||
&& client.LocalUser?.State == MultiplayerUserState.Ready)
|
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
|
||||||
client.ChangeState(MultiplayerUserState.Idle);
|
if (client.LocalUser?.State == MultiplayerUserState.Ready)
|
||||||
|
client.ChangeState(MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (client.LocalUser?.State == MultiplayerUserState.Spectating && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing))
|
||||||
|
onLoadRequested();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onReadyClick()
|
private void onReadyClick()
|
||||||
@ -407,22 +416,46 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
|
|
||||||
private void onRoomUpdated()
|
private void onRoomUpdated()
|
||||||
{
|
{
|
||||||
// user mods may have changed.
|
|
||||||
Scheduler.AddOnce(UpdateMods);
|
Scheduler.AddOnce(UpdateMods);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onLoadRequested()
|
private void onLoadRequested()
|
||||||
{
|
{
|
||||||
Debug.Assert(client.Room != null);
|
if (BeatmapAvailability.Value.State != DownloadState.LocallyAvailable)
|
||||||
|
return;
|
||||||
|
|
||||||
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
|
// In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session.
|
||||||
|
// For now, we want to game to switch to the new game so need to request exiting from the play screen.
|
||||||
|
if (!ParentScreen.IsCurrentScreen())
|
||||||
|
{
|
||||||
|
ParentScreen.MakeCurrent();
|
||||||
|
|
||||||
StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds));
|
Schedule(onLoadRequested);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartPlay();
|
||||||
|
|
||||||
readyClickOperation?.Dispose();
|
readyClickOperation?.Dispose();
|
||||||
readyClickOperation = null;
|
readyClickOperation = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override Screen CreateGameplayScreen()
|
||||||
|
{
|
||||||
|
Debug.Assert(client.LocalUser != null);
|
||||||
|
|
||||||
|
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
|
||||||
|
|
||||||
|
switch (client.LocalUser.State)
|
||||||
|
{
|
||||||
|
case MultiplayerUserState.Spectating:
|
||||||
|
return new MultiSpectatorScreen(userIds);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return new MultiplayerPlayer(SelectedItem.Value, userIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
// 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;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="ISpectatorPlayerClock"/> which catches up using rate adjustment.
|
||||||
|
/// </summary>
|
||||||
|
public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The catch up rate.
|
||||||
|
/// </summary>
|
||||||
|
public const double CATCHUP_RATE = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The source clock.
|
||||||
|
/// </summary>
|
||||||
|
public IFrameBasedClock? Source { get; set; }
|
||||||
|
|
||||||
|
public double CurrentTime { get; private set; }
|
||||||
|
|
||||||
|
public bool IsRunning { get; private set; }
|
||||||
|
|
||||||
|
public void Reset() => CurrentTime = 0;
|
||||||
|
|
||||||
|
public void Start() => IsRunning = true;
|
||||||
|
|
||||||
|
public void Stop() => IsRunning = false;
|
||||||
|
|
||||||
|
public bool Seek(double position) => true;
|
||||||
|
|
||||||
|
public void ResetSpeedAdjustments()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Rate => IsCatchingUp ? CATCHUP_RATE : 1;
|
||||||
|
|
||||||
|
double IAdjustableClock.Rate
|
||||||
|
{
|
||||||
|
get => Rate;
|
||||||
|
set => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
double IClock.Rate => Rate;
|
||||||
|
|
||||||
|
public void ProcessFrame()
|
||||||
|
{
|
||||||
|
ElapsedFrameTime = 0;
|
||||||
|
FramesPerSecond = 0;
|
||||||
|
|
||||||
|
if (Source == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Source.ProcessFrame();
|
||||||
|
|
||||||
|
if (IsRunning)
|
||||||
|
{
|
||||||
|
double elapsedSource = Source.ElapsedFrameTime;
|
||||||
|
double elapsed = elapsedSource * Rate;
|
||||||
|
|
||||||
|
CurrentTime += elapsed;
|
||||||
|
ElapsedFrameTime = elapsed;
|
||||||
|
FramesPerSecond = Source.FramesPerSecond;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double ElapsedFrameTime { get; private set; }
|
||||||
|
|
||||||
|
public double FramesPerSecond { get; private set; }
|
||||||
|
|
||||||
|
public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
|
||||||
|
|
||||||
|
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
|
||||||
|
|
||||||
|
public bool IsCatchingUp { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,153 @@
|
|||||||
|
// 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.Graphics;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="ISyncManager"/> which synchronises de-synced player clocks through catchup.
|
||||||
|
/// </summary>
|
||||||
|
public class CatchUpSyncManager : Component, ISyncManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The offset from the master clock to which player clocks should remain within to be considered in-sync.
|
||||||
|
/// </summary>
|
||||||
|
public const double SYNC_TARGET = 16;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The offset from the master clock at which player clocks begin resynchronising.
|
||||||
|
/// </summary>
|
||||||
|
public const double MAX_SYNC_OFFSET = 50;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum delay to start gameplay, if any (but not all) player clocks are ready.
|
||||||
|
/// </summary>
|
||||||
|
public const double MAXIMUM_START_DELAY = 15000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The master clock which is used to control the timing of all player clocks clocks.
|
||||||
|
/// </summary>
|
||||||
|
public IAdjustableClock MasterClock { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The player clocks.
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<ISpectatorPlayerClock> playerClocks = new List<ISpectatorPlayerClock>();
|
||||||
|
|
||||||
|
private bool hasStarted;
|
||||||
|
private double? firstStartAttemptTime;
|
||||||
|
|
||||||
|
public CatchUpSyncManager(IAdjustableClock master)
|
||||||
|
{
|
||||||
|
MasterClock = master;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddPlayerClock(ISpectatorPlayerClock clock) => playerClocks.Add(clock);
|
||||||
|
|
||||||
|
public void RemovePlayerClock(ISpectatorPlayerClock clock) => playerClocks.Remove(clock);
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (!attemptStart())
|
||||||
|
{
|
||||||
|
// Ensure all player clocks are stopped until the start succeeds.
|
||||||
|
foreach (var clock in playerClocks)
|
||||||
|
clock.Stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCatchup();
|
||||||
|
updateMasterClock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to start playback. Waits for all player clocks to have available frames for up to <see cref="MAXIMUM_START_DELAY"/> milliseconds.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Whether playback was started and syncing should occur.</returns>
|
||||||
|
private bool attemptStart()
|
||||||
|
{
|
||||||
|
if (hasStarted)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (playerClocks.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value);
|
||||||
|
|
||||||
|
if (readyCount == playerClocks.Count)
|
||||||
|
return hasStarted = true;
|
||||||
|
|
||||||
|
if (readyCount > 0)
|
||||||
|
{
|
||||||
|
firstStartAttemptTime ??= Time.Current;
|
||||||
|
|
||||||
|
if (Time.Current - firstStartAttemptTime > MAXIMUM_START_DELAY)
|
||||||
|
return hasStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the catchup states of all player clocks clocks.
|
||||||
|
/// </summary>
|
||||||
|
private void updateCatchup()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < playerClocks.Count; i++)
|
||||||
|
{
|
||||||
|
var clock = playerClocks[i];
|
||||||
|
|
||||||
|
// How far this player's clock is out of sync, compared to the master clock.
|
||||||
|
// A negative value means the player is running fast (ahead); a positive value means the player is running behind (catching up).
|
||||||
|
double timeDelta = MasterClock.CurrentTime - clock.CurrentTime;
|
||||||
|
|
||||||
|
// Check that the player clock isn't too far ahead.
|
||||||
|
// This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock.
|
||||||
|
if (timeDelta < -SYNC_TARGET)
|
||||||
|
{
|
||||||
|
clock.Stop();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the player clock is running if it can.
|
||||||
|
if (!clock.WaitingOnFrames.Value)
|
||||||
|
clock.Start();
|
||||||
|
|
||||||
|
if (clock.IsCatchingUp)
|
||||||
|
{
|
||||||
|
// Stop the player clock from catching up if it's within the sync target.
|
||||||
|
if (timeDelta <= SYNC_TARGET)
|
||||||
|
clock.IsCatchingUp = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Make the player clock start catching up if it's exceeded the maximum allowable sync offset.
|
||||||
|
if (timeDelta > MAX_SYNC_OFFSET)
|
||||||
|
clock.IsCatchingUp = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the master clock's running state.
|
||||||
|
/// </summary>
|
||||||
|
private void updateMasterClock()
|
||||||
|
{
|
||||||
|
bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp);
|
||||||
|
|
||||||
|
if (MasterClock.IsRunning != anyInSync)
|
||||||
|
{
|
||||||
|
if (anyInSync)
|
||||||
|
MasterClock.Start();
|
||||||
|
else
|
||||||
|
MasterClock.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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.Bindables;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A clock which is used by <see cref="MultiSpectatorPlayer"/>s and managed by an <see cref="ISyncManager"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this clock is waiting on frames to continue playback.
|
||||||
|
/// </summary>
|
||||||
|
Bindable<bool> WaitingOnFrames { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this clock is resynchronising to the master clock.
|
||||||
|
/// </summary>
|
||||||
|
bool IsCatchingUp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The source clock
|
||||||
|
/// </summary>
|
||||||
|
IFrameBasedClock Source { set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
// 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.Timing;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages the synchronisation between one or more <see cref="ISpectatorPlayerClock"/>s in relation to a master clock.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISyncManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The master clock which player clocks should synchronise to.
|
||||||
|
/// </summary>
|
||||||
|
IAdjustableClock MasterClock { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an <see cref="ISpectatorPlayerClock"/> to manage.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to add.</param>
|
||||||
|
void AddPlayerClock(ISpectatorPlayerClock clock);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an <see cref="ISpectatorPlayerClock"/>, stopping it from being managed by this <see cref="ISyncManager"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to remove.</param>
|
||||||
|
void RemovePlayerClock(ISpectatorPlayerClock clock);
|
||||||
|
}
|
||||||
|
}
|
@ -9,9 +9,9 @@ using osu.Game.Screens.Play.HUD;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
{
|
{
|
||||||
public class MultiplayerSpectatorLeaderboard : MultiplayerGameplayLeaderboard
|
public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
|
||||||
{
|
{
|
||||||
public MultiplayerSpectatorLeaderboard(ScoreProcessor scoreProcessor, int[] userIds)
|
public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, int[] userIds)
|
||||||
: base(scoreProcessor, userIds)
|
: base(scoreProcessor, userIds)
|
||||||
{
|
{
|
||||||
}
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A single spectated player within a <see cref="MultiSpectatorScreen"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class MultiSpectatorPlayer : SpectatorPlayer
|
||||||
|
{
|
||||||
|
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
|
||||||
|
private readonly Score score;
|
||||||
|
private readonly ISpectatorPlayerClock spectatorPlayerClock;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="MultiSpectatorPlayer"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="score">The score containing the player's replay.</param>
|
||||||
|
/// <param name="spectatorPlayerClock">The clock controlling the gameplay running state.</param>
|
||||||
|
public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock)
|
||||||
|
: base(score)
|
||||||
|
{
|
||||||
|
this.score = score;
|
||||||
|
this.spectatorPlayerClock = spectatorPlayerClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateAfterChildren()
|
||||||
|
{
|
||||||
|
base.UpdateAfterChildren();
|
||||||
|
|
||||||
|
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame.
|
||||||
|
waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || score.Replay.Frames.Count == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
||||||
|
=> new SpectatorGameplayClockContainer(spectatorPlayerClock);
|
||||||
|
|
||||||
|
private class SpectatorGameplayClockContainer : GameplayClockContainer
|
||||||
|
{
|
||||||
|
public SpectatorGameplayClockContainer([NotNull] IClock sourceClock)
|
||||||
|
: base(sourceClock)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to stop gameplay.
|
||||||
|
if (SourceClock.IsRunning)
|
||||||
|
Start();
|
||||||
|
else
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
base.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override GameplayClock CreateGameplayClock(IFrameBasedClock source) => new GameplayClock(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Menu;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used to load a single <see cref="MultiSpectatorPlayer"/> in a <see cref="MultiSpectatorScreen"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class MultiSpectatorPlayerLoader : SpectatorPlayerLoader
|
||||||
|
{
|
||||||
|
public MultiSpectatorPlayerLoader([NotNull] Score score, [NotNull] Func<MultiSpectatorPlayer> createPlayer)
|
||||||
|
: base(score, createPlayer)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LogoArriving(OsuLogo logo, bool resuming)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LogoExiting(OsuLogo logo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,156 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Spectator;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Screens.Spectate;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="SpectatorScreen"/> that spectates multiple users in a match.
|
||||||
|
/// </summary>
|
||||||
|
public class MultiSpectatorScreen : SpectatorScreen
|
||||||
|
{
|
||||||
|
// Isolates beatmap/ruleset to this screen.
|
||||||
|
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether all spectating players have finished loading.
|
||||||
|
/// </summary>
|
||||||
|
public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true);
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SpectatorStreamingClient spectatorClient { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private StatefulMultiplayerClient multiplayerClient { get; set; }
|
||||||
|
|
||||||
|
private readonly PlayerArea[] instances;
|
||||||
|
private MasterGameplayClockContainer masterClockContainer;
|
||||||
|
private ISyncManager syncManager;
|
||||||
|
private PlayerGrid grid;
|
||||||
|
private MultiSpectatorLeaderboard leaderboard;
|
||||||
|
private PlayerArea currentAudioSource;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new <see cref="MultiSpectatorScreen"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userIds">The players to spectate.</param>
|
||||||
|
public MultiSpectatorScreen(int[] userIds)
|
||||||
|
: base(userIds.Take(PlayerGrid.MAX_PLAYERS).ToArray())
|
||||||
|
{
|
||||||
|
instances = new PlayerArea[UserIds.Count];
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Container leaderboardContainer;
|
||||||
|
masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0);
|
||||||
|
|
||||||
|
InternalChildren = new[]
|
||||||
|
{
|
||||||
|
(Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)),
|
||||||
|
masterClockContainer.WithChild(new GridContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
ColumnDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize)
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
leaderboardContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X
|
||||||
|
},
|
||||||
|
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < UserIds.Count; i++)
|
||||||
|
{
|
||||||
|
grid.Add(instances[i] = new PlayerArea(UserIds[i], masterClockContainer.GameplayClock));
|
||||||
|
syncManager.AddPlayerClock(instances[i].GameplayClock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo: This is not quite correct - it should be per-user to adjust for other mod combinations.
|
||||||
|
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
|
||||||
|
var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor();
|
||||||
|
scoreProcessor.ApplyBeatmap(playableBeatmap);
|
||||||
|
|
||||||
|
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds.ToArray())
|
||||||
|
{
|
||||||
|
Expanded = { Value = true },
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
}, leaderboardContainer.Add);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
masterClockContainer.Stop();
|
||||||
|
masterClockContainer.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (!isCandidateAudioSource(currentAudioSource?.GameplayClock))
|
||||||
|
{
|
||||||
|
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock))
|
||||||
|
.OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.MasterClock.CurrentTime))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
foreach (var instance in instances)
|
||||||
|
instance.Mute = instance != currentAudioSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock)
|
||||||
|
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
|
||||||
|
|
||||||
|
protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void StartGameplay(int userId, GameplayState gameplayState)
|
||||||
|
{
|
||||||
|
var instance = instances.Single(i => i.UserId == userId);
|
||||||
|
|
||||||
|
instance.LoadScore(gameplayState.Score);
|
||||||
|
|
||||||
|
syncManager.AddPlayerClock(instance.GameplayClock);
|
||||||
|
leaderboard.AddClock(instance.UserId, instance.GameplayClock);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void EndGameplay(int userId)
|
||||||
|
{
|
||||||
|
RemoveUser(userId);
|
||||||
|
leaderboard.RemoveClock(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool OnBackButton()
|
||||||
|
{
|
||||||
|
// On a manual exit, set the player state back to idle.
|
||||||
|
multiplayerClient.ChangeState(MultiplayerUserState.Idle);
|
||||||
|
return base.OnBackButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
Normal file
144
osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// 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 JetBrains.Annotations;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Scoring;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides an area for and manages the hierarchy of a spectated player within a <see cref="MultiSpectatorScreen"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class PlayerArea : CompositeDrawable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a <see cref="Player"/> is loaded in the area.
|
||||||
|
/// </summary>
|
||||||
|
public bool PlayerLoaded => stack?.CurrentScreen is Player;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user id this <see cref="PlayerArea"/> corresponds to.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int UserId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="ISpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
|
||||||
|
/// </summary>
|
||||||
|
[NotNull]
|
||||||
|
public readonly ISpectatorPlayerClock GameplayClock = new CatchUpSpectatorPlayerClock();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The currently-loaded score.
|
||||||
|
/// </summary>
|
||||||
|
[CanBeNull]
|
||||||
|
public Score Score { get; private set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private BeatmapManager beatmapManager { get; set; }
|
||||||
|
|
||||||
|
private readonly BindableDouble volumeAdjustment = new BindableDouble();
|
||||||
|
private readonly Container gameplayContent;
|
||||||
|
private readonly LoadingLayer loadingLayer;
|
||||||
|
private OsuScreenStack stack;
|
||||||
|
|
||||||
|
public PlayerArea(int userId, IFrameBasedClock masterClock)
|
||||||
|
{
|
||||||
|
UserId = userId;
|
||||||
|
|
||||||
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
Masking = true;
|
||||||
|
|
||||||
|
AudioContainer audioContainer;
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
audioContainer = new AudioContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = gameplayContent = new DrawSizePreservingFillContainer { RelativeSizeAxes = Axes.Both },
|
||||||
|
},
|
||||||
|
loadingLayer = new LoadingLayer(true) { State = { Value = Visibility.Visible } }
|
||||||
|
};
|
||||||
|
|
||||||
|
audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
|
||||||
|
|
||||||
|
GameplayClock.Source = masterClock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadScore([NotNull] Score score)
|
||||||
|
{
|
||||||
|
if (Score != null)
|
||||||
|
throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score.");
|
||||||
|
|
||||||
|
Score = score;
|
||||||
|
|
||||||
|
gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = stack = new OsuScreenStack()
|
||||||
|
};
|
||||||
|
|
||||||
|
stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock)));
|
||||||
|
loadingLayer.Hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool mute = true;
|
||||||
|
|
||||||
|
public bool Mute
|
||||||
|
{
|
||||||
|
get => mute;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
mute = value;
|
||||||
|
volumeAdjustment.Value = value ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player interferes with global input, so disable input for now.
|
||||||
|
public override bool PropagatePositionalInputSubTree => false;
|
||||||
|
public override bool PropagateNonPositionalInputSubTree => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Isolates each player instance from the game-wide ruleset/beatmap/mods (to allow for different players having different settings).
|
||||||
|
/// </summary>
|
||||||
|
private class PlayerIsolationContainer : Container
|
||||||
|
{
|
||||||
|
[Cached]
|
||||||
|
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly Bindable<IReadOnlyList<Mod>> mods = new Bindable<IReadOnlyList<Mod>>();
|
||||||
|
|
||||||
|
public PlayerIsolationContainer(WorkingBeatmap beatmap, RulesetInfo ruleset, IReadOnlyList<Mod> mods)
|
||||||
|
{
|
||||||
|
this.beatmap.Value = beatmap;
|
||||||
|
this.ruleset.Value = ruleset;
|
||||||
|
this.mods.Value = mods;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
|
{
|
||||||
|
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||||
|
dependencies.CacheAs(ruleset.BeginLease(false));
|
||||||
|
dependencies.CacheAs(beatmap.BeginLease(false));
|
||||||
|
dependencies.CacheAs(mods.BeginLease(false));
|
||||||
|
return dependencies;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class PlayerGrid : CompositeDrawable
|
public partial class PlayerGrid : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A temporary limitation on the number of players, because only layouts up to 16 players are supported for a single screen.
|
||||||
|
/// Todo: Can be removed in the future with scrolling support + performance improvements.
|
||||||
|
/// </summary>
|
||||||
|
public const int MAX_PLAYERS = 16;
|
||||||
|
|
||||||
private const float player_spacing = 5;
|
private const float player_spacing = 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -58,11 +64,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
/// Adds a new cell with content to this grid.
|
/// Adds a new cell with content to this grid.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="content">The content the cell should contain.</param>
|
/// <param name="content">The content the cell should contain.</param>
|
||||||
/// <exception cref="InvalidOperationException">If more than 16 cells are added.</exception>
|
/// <exception cref="InvalidOperationException">If more than <see cref="MAX_PLAYERS"/> cells are added.</exception>
|
||||||
public void Add(Drawable content)
|
public void Add(Drawable content)
|
||||||
{
|
{
|
||||||
if (cellContainer.Count == 16)
|
if (cellContainer.Count == MAX_PLAYERS)
|
||||||
throw new InvalidOperationException("Only 16 cells are supported.");
|
throw new InvalidOperationException($"Only {MAX_PLAYERS} cells are supported.");
|
||||||
|
|
||||||
int index = cellContainer.Count;
|
int index = cellContainer.Count;
|
||||||
|
|
||||||
|
@ -218,10 +218,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
},
|
},
|
||||||
new Drawable[]
|
new Drawable[]
|
||||||
{
|
{
|
||||||
new Footer
|
new Footer { OnStart = StartPlay }
|
||||||
{
|
|
||||||
OnStart = onStart,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RowDimensions = new[]
|
RowDimensions = new[]
|
||||||
@ -274,9 +271,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
|||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onStart() => StartPlay(() => new PlaylistsPlayer(SelectedItem.Value)
|
protected override Screen CreateGameplayScreen() => new PlaylistsPlayer(SelectedItem.Value)
|
||||||
{
|
{
|
||||||
Exited = () => leaderboard.RefreshScores()
|
Exited = () => leaderboard.RefreshScores()
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -12,7 +13,7 @@ namespace osu.Game.Screens.Play
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Encapsulates gameplay timing logic and provides a <see cref="GameplayClock"/> via DI for gameplay components to use.
|
/// Encapsulates gameplay timing logic and provides a <see cref="GameplayClock"/> via DI for gameplay components to use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class GameplayClockContainer : Container
|
public abstract class GameplayClockContainer : Container, IAdjustableClock
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The final clock which is exposed to gameplay components.
|
/// The final clock which is exposed to gameplay components.
|
||||||
@ -157,5 +158,33 @@ namespace osu.Game.Screens.Play
|
|||||||
/// <param name="source">The <see cref="IFrameBasedClock"/> providing the source time.</param>
|
/// <param name="source">The <see cref="IFrameBasedClock"/> providing the source time.</param>
|
||||||
/// <returns>The final <see cref="GameplayClock"/>.</returns>
|
/// <returns>The final <see cref="GameplayClock"/>.</returns>
|
||||||
protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source);
|
protected abstract GameplayClock CreateGameplayClock(IFrameBasedClock source);
|
||||||
|
|
||||||
|
#region IAdjustableClock
|
||||||
|
|
||||||
|
bool IAdjustableClock.Seek(double position)
|
||||||
|
{
|
||||||
|
Seek(position);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void IAdjustableClock.Reset() => Reset();
|
||||||
|
|
||||||
|
public void ResetSpeedAdjustments()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
double IAdjustableClock.Rate
|
||||||
|
{
|
||||||
|
get => GameplayClock.Rate;
|
||||||
|
set => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
double IClock.Rate => GameplayClock.Rate;
|
||||||
|
|
||||||
|
public double CurrentTime => GameplayClock.CurrentTime;
|
||||||
|
|
||||||
|
public bool IsRunning => GameplayClock.IsRunning;
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,23 +2,13 @@
|
|||||||
// 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.Graphics;
|
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
namespace osu.Game.Screens.Play.HUD
|
||||||
{
|
{
|
||||||
public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableComponent
|
public class DefaultAccuracyCounter : GameplayAccuracyCounter, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
private readonly Vector2 offset = new Vector2(-20, 5);
|
|
||||||
|
|
||||||
public DefaultAccuracyCounter()
|
|
||||||
{
|
|
||||||
Origin = Anchor.TopRight;
|
|
||||||
Anchor = Anchor.TopRight;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private HUDOverlay hud { get; set; }
|
private HUDOverlay hud { get; set; }
|
||||||
|
|
||||||
@ -27,17 +17,5 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
Colour = colours.BlueLighter;
|
Colour = colours.BlueLighter;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
|
|
||||||
{
|
|
||||||
// for now align with the score counter. eventually this will be user customisable.
|
|
||||||
Anchor = Anchor.TopLeft;
|
|
||||||
Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopLeft) + offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,14 +9,11 @@ using osu.Game.Graphics.Sprites;
|
|||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
namespace osu.Game.Screens.Play.HUD
|
||||||
{
|
{
|
||||||
public class DefaultComboCounter : RollingCounter<int>, ISkinnableComponent
|
public class DefaultComboCounter : RollingCounter<int>, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
private readonly Vector2 offset = new Vector2(20, 5);
|
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private HUDOverlay hud { get; set; }
|
private HUDOverlay hud { get; set; }
|
||||||
|
|
||||||
@ -32,17 +29,6 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
Current.BindTo(scoreProcessor.Combo);
|
Current.BindTo(scoreProcessor.Combo);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score)
|
|
||||||
{
|
|
||||||
// for now align with the score counter. eventually this will be user customisable.
|
|
||||||
Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopRight) + offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override string FormatCount(int count)
|
protected override string FormatCount(int count)
|
||||||
{
|
{
|
||||||
return $@"{count}x";
|
return $@"{count}x";
|
||||||
|
@ -17,7 +17,7 @@ using osu.Game.Skinning;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
namespace osu.Game.Screens.Play.HUD
|
||||||
{
|
{
|
||||||
public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableComponent
|
public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The base opacity of the glow.
|
/// The base opacity of the glow.
|
||||||
|
@ -8,7 +8,7 @@ using osu.Game.Skinning;
|
|||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
namespace osu.Game.Screens.Play.HUD
|
||||||
{
|
{
|
||||||
public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableComponent
|
public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
public DefaultScoreCounter()
|
public DefaultScoreCounter()
|
||||||
: base(6)
|
: base(6)
|
||||||
@ -24,12 +24,6 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
Colour = colours.BlueLighter;
|
Colour = colours.BlueLighter;
|
||||||
|
|
||||||
// todo: check if default once health display is skinnable
|
|
||||||
hud?.ShowHealthbar.BindValueChanged(healthBar =>
|
|
||||||
{
|
|
||||||
this.MoveToY(healthBar.NewValue ? 30 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING);
|
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
@ -16,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class HealthDisplay : CompositeDrawable
|
public abstract class HealthDisplay : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
private readonly Bindable<bool> showHealthbar = new Bindable<bool>(true);
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
protected HealthProcessor HealthProcessor { get; private set; }
|
protected HealthProcessor HealthProcessor { get; private set; }
|
||||||
|
|
||||||
@ -29,12 +32,21 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[Resolved(canBeNull: true)]
|
||||||
private void load()
|
private HUDOverlay hudOverlay { get; set; }
|
||||||
{
|
|
||||||
Current.BindTo(HealthProcessor.Health);
|
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
Current.BindTo(HealthProcessor.Health);
|
||||||
HealthProcessor.NewJudgement += onNewJudgement;
|
HealthProcessor.NewJudgement += onNewJudgement;
|
||||||
|
|
||||||
|
if (hudOverlay != null)
|
||||||
|
showHealthbar.BindTo(hudOverlay.ShowHealthbar);
|
||||||
|
|
||||||
|
// this probably shouldn't be operating on `this.`
|
||||||
|
showHealthbar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onNewJudgement(JudgementResult judgement)
|
private void onNewJudgement(JudgementResult judgement)
|
||||||
|
@ -13,13 +13,12 @@ using osu.Framework.Graphics.Sprites;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Skinning;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||||
{
|
{
|
||||||
public class BarHitErrorMeter : HitErrorMeter, ISkinnableComponent
|
public class BarHitErrorMeter : HitErrorMeter
|
||||||
{
|
{
|
||||||
private readonly Anchor alignment;
|
private readonly Anchor alignment;
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Uses the 'x' symbol and has a pop-out effect while rolling over.
|
/// Uses the 'x' symbol and has a pop-out effect while rolling over.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LegacyComboCounter : CompositeDrawable, ISkinnableComponent
|
public class LegacyComboCounter : CompositeDrawable, ISkinnableDrawable
|
||||||
{
|
{
|
||||||
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0, };
|
public Bindable<int> Current { get; } = new BindableInt { MinValue = 0, };
|
||||||
|
|
||||||
@ -84,13 +84,13 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
popOutCount = new LegacySpriteText(skin, LegacyFont.Combo)
|
popOutCount = new LegacySpriteText(LegacyFont.Combo)
|
||||||
{
|
{
|
||||||
Alpha = 0,
|
Alpha = 0,
|
||||||
Margin = new MarginPadding(0.05f),
|
Margin = new MarginPadding(0.05f),
|
||||||
Blending = BlendingParameters.Additive,
|
Blending = BlendingParameters.Additive,
|
||||||
},
|
},
|
||||||
displayedCountSpriteText = new LegacySpriteText(skin, LegacyFont.Combo)
|
displayedCountSpriteText = new LegacySpriteText(LegacyFont.Combo)
|
||||||
{
|
{
|
||||||
Alpha = 0,
|
Alpha = 0,
|
||||||
},
|
},
|
||||||
|
@ -1,16 +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.Game.Skinning;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
|
||||||
{
|
|
||||||
public class SkinnableAccuracyCounter : SkinnableDrawable
|
|
||||||
{
|
|
||||||
public SkinnableAccuracyCounter()
|
|
||||||
: base(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter), _ => new DefaultAccuracyCounter())
|
|
||||||
{
|
|
||||||
CentreComponent = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +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.Game.Skinning;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
|
||||||
{
|
|
||||||
public class SkinnableComboCounter : SkinnableDrawable
|
|
||||||
{
|
|
||||||
public SkinnableComboCounter()
|
|
||||||
: base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), skinComponent => new DefaultComboCounter())
|
|
||||||
{
|
|
||||||
CentreComponent = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +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.Game.Skinning;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
|
||||||
{
|
|
||||||
public class SkinnableHealthDisplay : SkinnableDrawable
|
|
||||||
{
|
|
||||||
public SkinnableHealthDisplay()
|
|
||||||
: base(new HUDSkinComponent(HUDSkinComponents.HealthDisplay), _ => new DefaultHealthDisplay())
|
|
||||||
{
|
|
||||||
CentreComponent = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
74
osu.Game/Screens/Play/HUD/SkinnableInfo.cs
Normal file
74
osu.Game/Screens/Play/HUD/SkinnableInfo.cs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.IO.Serialization;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Play.HUD
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serialised information governing custom changes to an <see cref="ISkinnableDrawable"/>.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class SkinnableInfo : IJsonSerializable
|
||||||
|
{
|
||||||
|
public Type Type { get; set; }
|
||||||
|
|
||||||
|
public Vector2 Position { get; set; }
|
||||||
|
|
||||||
|
public float Rotation { get; set; }
|
||||||
|
|
||||||
|
public Vector2 Scale { get; set; }
|
||||||
|
|
||||||
|
public Anchor Anchor { get; set; }
|
||||||
|
|
||||||
|
public Anchor Origin { get; set; }
|
||||||
|
|
||||||
|
public List<SkinnableInfo> Children { get; } = new List<SkinnableInfo>();
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public SkinnableInfo()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construct a new instance populating all attributes from the provided drawable.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="component">The drawable which attributes should be sourced from.</param>
|
||||||
|
public SkinnableInfo(Drawable component)
|
||||||
|
{
|
||||||
|
Type = component.GetType();
|
||||||
|
|
||||||
|
Position = component.Position;
|
||||||
|
Rotation = component.Rotation;
|
||||||
|
Scale = component.Scale;
|
||||||
|
Anchor = component.Anchor;
|
||||||
|
Origin = component.Origin;
|
||||||
|
|
||||||
|
if (component is Container<Drawable> container)
|
||||||
|
{
|
||||||
|
foreach (var child in container.OfType<ISkinnableDrawable>().OfType<Drawable>())
|
||||||
|
Children.Add(child.CreateSkinnableInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Construct an instance of the drawable with all attributes applied.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The new instance.</returns>
|
||||||
|
public Drawable CreateInstance()
|
||||||
|
{
|
||||||
|
Drawable d = (Drawable)Activator.CreateInstance(Type);
|
||||||
|
d.ApplySkinnableInfo(this);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +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.Game.Skinning;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play.HUD
|
|
||||||
{
|
|
||||||
public class SkinnableScoreCounter : SkinnableDrawable
|
|
||||||
{
|
|
||||||
public SkinnableScoreCounter()
|
|
||||||
: base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter())
|
|
||||||
{
|
|
||||||
CentreComponent = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -22,7 +24,7 @@ using osuTK;
|
|||||||
namespace osu.Game.Screens.Play
|
namespace osu.Game.Screens.Play
|
||||||
{
|
{
|
||||||
[Cached]
|
[Cached]
|
||||||
public class HUDOverlay : Container, IKeyBindingHandler<GlobalAction>, IDefaultSkinnableTarget
|
public class HUDOverlay : Container, IKeyBindingHandler<GlobalAction>
|
||||||
{
|
{
|
||||||
public const float FADE_DURATION = 300;
|
public const float FADE_DURATION = 300;
|
||||||
|
|
||||||
@ -34,9 +36,6 @@ namespace osu.Game.Screens.Play
|
|||||||
public float TopScoringElementsHeight { get; private set; }
|
public float TopScoringElementsHeight { get; private set; }
|
||||||
|
|
||||||
public readonly KeyCounterDisplay KeyCounter;
|
public readonly KeyCounterDisplay KeyCounter;
|
||||||
public readonly SkinnableScoreCounter ScoreCounter;
|
|
||||||
public readonly SkinnableAccuracyCounter AccuracyCounter;
|
|
||||||
public readonly SkinnableHealthDisplay HealthDisplay;
|
|
||||||
public readonly SongProgress Progress;
|
public readonly SongProgress Progress;
|
||||||
public readonly ModDisplay ModDisplay;
|
public readonly ModDisplay ModDisplay;
|
||||||
public readonly HoldForMenuButton HoldToQuit;
|
public readonly HoldForMenuButton HoldToQuit;
|
||||||
@ -69,6 +68,8 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
private bool holdingForHUD;
|
private bool holdingForHUD;
|
||||||
|
|
||||||
|
private readonly SkinnableTargetContainer mainComponents;
|
||||||
|
|
||||||
private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements };
|
private IEnumerable<Drawable> hideTargets => new Drawable[] { visibilityContainer, KeyCounter, topRightElements };
|
||||||
|
|
||||||
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
|
public HUDOverlay(DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods)
|
||||||
@ -96,11 +97,19 @@ namespace osu.Game.Screens.Play
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
HealthDisplay = CreateHealthDisplay(),
|
mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
|
||||||
AccuracyCounter = CreateAccuracyCounter(),
|
{
|
||||||
ScoreCounter = CreateScoreCounter(),
|
RelativeSizeAxes = Axes.Both,
|
||||||
CreateComboCounter(),
|
},
|
||||||
CreateHitErrorDisplayOverlay(),
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
// still need to be migrated; a bit more involved.
|
||||||
|
new HitErrorDisplay(this.drawableRuleset?.FirstAvailableHitWindows),
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -185,7 +194,6 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
ShowHealthbar.BindValueChanged(healthBar => HealthDisplay.FadeTo(healthBar.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING), true);
|
|
||||||
ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING)));
|
ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING)));
|
||||||
|
|
||||||
IsBreakTime.BindValueChanged(_ => updateVisibility());
|
IsBreakTime.BindValueChanged(_ => updateVisibility());
|
||||||
@ -198,11 +206,25 @@ namespace osu.Game.Screens.Play
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
// HACK: for now align with the accuracy counter.
|
Vector2 lowestScreenSpace = Vector2.Zero;
|
||||||
// this is done for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
|
|
||||||
// it only works with the default skin due to padding offsetting it *just enough* to coexist.
|
|
||||||
topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y;
|
|
||||||
|
|
||||||
|
// LINQ cast can be removed when IDrawable interface includes Anchor / RelativeSizeAxes.
|
||||||
|
foreach (var element in mainComponents.Components.Cast<Drawable>())
|
||||||
|
{
|
||||||
|
// for now align top-right components with the bottom-edge of the lowest top-anchored hud element.
|
||||||
|
if (!element.Anchor.HasFlagFast(Anchor.TopRight) && !element.RelativeSizeAxes.HasFlagFast(Axes.X))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// health bars are excluded for the sake of hacky legacy skins which extend the health bar to take up the full screen area.
|
||||||
|
if (element is LegacyHealthDisplay)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var bottomRight = element.ScreenSpaceDrawQuad.BottomRight;
|
||||||
|
if (bottomRight.Y > lowestScreenSpace.Y)
|
||||||
|
lowestScreenSpace = bottomRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(lowestScreenSpace).Y;
|
||||||
bottomRightElements.Y = -Progress.Height;
|
bottomRightElements.Y = -Progress.Height;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,48 +285,38 @@ namespace osu.Game.Screens.Play
|
|||||||
Progress.BindDrawableRuleset(drawableRuleset);
|
Progress.BindDrawableRuleset(drawableRuleset);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter();
|
protected FailingLayer CreateFailingLayer() => new FailingLayer
|
||||||
|
|
||||||
protected SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter();
|
|
||||||
|
|
||||||
protected SkinnableComboCounter CreateComboCounter() => new SkinnableComboCounter();
|
|
||||||
|
|
||||||
protected SkinnableHealthDisplay CreateHealthDisplay() => new SkinnableHealthDisplay();
|
|
||||||
|
|
||||||
protected virtual FailingLayer CreateFailingLayer() => new FailingLayer
|
|
||||||
{
|
{
|
||||||
ShowHealth = { BindTarget = ShowHealthbar }
|
ShowHealth = { BindTarget = ShowHealthbar }
|
||||||
};
|
};
|
||||||
|
|
||||||
protected virtual KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay
|
protected KeyCounterDisplay CreateKeyCounter() => new KeyCounterDisplay
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomRight,
|
Anchor = Anchor.BottomRight,
|
||||||
Origin = Anchor.BottomRight,
|
Origin = Anchor.BottomRight,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected virtual SongProgress CreateProgress() => new SongProgress
|
protected SongProgress CreateProgress() => new SongProgress
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected virtual HoldForMenuButton CreateHoldForMenuButton() => new HoldForMenuButton
|
protected HoldForMenuButton CreateHoldForMenuButton() => new HoldForMenuButton
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomRight,
|
Anchor = Anchor.BottomRight,
|
||||||
Origin = Anchor.BottomRight,
|
Origin = Anchor.BottomRight,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected virtual ModDisplay CreateModsContainer() => new ModDisplay
|
protected ModDisplay CreateModsContainer() => new ModDisplay
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopRight,
|
Anchor = Anchor.TopRight,
|
||||||
Origin = Anchor.TopRight,
|
Origin = Anchor.TopRight,
|
||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(drawableRuleset?.FirstAvailableHitWindows);
|
protected PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
|
||||||
|
|
||||||
protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
|
|
||||||
|
|
||||||
public bool OnPressed(GlobalAction action)
|
public bool OnPressed(GlobalAction action)
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,6 @@ using osu.Framework.Timing;
|
|||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Skinning;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play
|
namespace osu.Game.Screens.Play
|
||||||
{
|
{
|
||||||
@ -181,12 +180,12 @@ namespace osu.Game.Screens.Play
|
|||||||
info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In);
|
info.TransformTo(nameof(info.Margin), new MarginPadding { Bottom = finalMargin }, transition_duration, Easing.In);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SongProgressDisplay : Container, ISkinnableComponent
|
public class SongProgressDisplay : Container
|
||||||
{
|
{
|
||||||
public SongProgressDisplay()
|
public SongProgressDisplay()
|
||||||
{
|
{
|
||||||
// TODO: move actual implementation into this.
|
// TODO: move actual implementation into this.
|
||||||
// exists for skin customisation purposes.
|
// exists for skin customisation purposes (interface should be added to this container).
|
||||||
|
|
||||||
Masking = true;
|
Masking = true;
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
@ -12,7 +12,12 @@ namespace osu.Game.Screens.Play
|
|||||||
public readonly ScoreInfo Score;
|
public readonly ScoreInfo Score;
|
||||||
|
|
||||||
public SpectatorPlayerLoader(Score score)
|
public SpectatorPlayerLoader(Score score)
|
||||||
: base(() => new SpectatorPlayer(score))
|
: this(score, () => new SpectatorPlayer(score))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpectatorPlayerLoader(Score score, Func<Player> createPlayer)
|
||||||
|
: base(createPlayer)
|
||||||
{
|
{
|
||||||
if (score.Replay == null)
|
if (score.Replay == null)
|
||||||
throw new ArgumentException($"{nameof(score)} must have a non-null {nameof(score.Replay)}.", nameof(score));
|
throw new ArgumentException($"{nameof(score)} must have a non-null {nameof(score.Replay)}.", nameof(score));
|
||||||
|
@ -87,9 +87,9 @@ namespace osu.Game.Screens
|
|||||||
private static Color4 getColourFor(object type)
|
private static Color4 getColourFor(object type)
|
||||||
{
|
{
|
||||||
int hash = type.GetHashCode();
|
int hash = type.GetHashCode();
|
||||||
byte r = (byte)Math.Clamp(((hash & 0xFF0000) >> 16) * 0.8f, 20, 255);
|
byte r = (byte)Math.Clamp(((hash & 0xFF0000) >> 16) * 2, 128, 255);
|
||||||
byte g = (byte)Math.Clamp(((hash & 0x00FF00) >> 8) * 0.8f, 20, 255);
|
byte g = (byte)Math.Clamp(((hash & 0x00FF00) >> 8) * 2, 128, 255);
|
||||||
byte b = (byte)Math.Clamp((hash & 0x0000FF) * 0.8f, 20, 255);
|
byte b = (byte)Math.Clamp((hash & 0x0000FF) * 2, 128, 255);
|
||||||
return new Color4(r, g, b, 255);
|
return new Color4(r, g, b, 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,10 +109,10 @@ namespace osu.Game.Screens
|
|||||||
|
|
||||||
private readonly Container boxContainer;
|
private readonly Container boxContainer;
|
||||||
|
|
||||||
public UnderConstructionMessage(string name)
|
public UnderConstructionMessage(string name, string description = "is not yet ready for use!")
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
Size = new Vector2(0.3f);
|
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ namespace osu.Game.Screens
|
|||||||
{
|
{
|
||||||
CornerRadius = 20,
|
CornerRadius = 20,
|
||||||
Masking = true,
|
Masking = true,
|
||||||
RelativeSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
@ -133,15 +133,15 @@ namespace osu.Game.Screens
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
|
||||||
Colour = colour,
|
Colour = colour.Darken(0.8f),
|
||||||
Alpha = 0.2f,
|
Alpha = 0.8f,
|
||||||
Blending = BlendingParameters.Additive,
|
|
||||||
},
|
},
|
||||||
TextContainer = new FillFlowContainer
|
TextContainer = new FillFlowContainer
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
Padding = new MarginPadding(20),
|
||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
@ -157,14 +157,14 @@ namespace osu.Game.Screens
|
|||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
Text = name,
|
Text = name,
|
||||||
Colour = colour.Lighten(0.8f),
|
Colour = colour,
|
||||||
Font = OsuFont.GetFont(size: 36),
|
Font = OsuFont.GetFont(size: 36),
|
||||||
},
|
},
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
Text = "is not yet ready for use!",
|
Text = description,
|
||||||
Font = OsuFont.GetFont(size: 20),
|
Font = OsuFont.GetFont(size: 20),
|
||||||
},
|
},
|
||||||
new OsuSpriteText
|
new OsuSpriteText
|
||||||
|
@ -26,7 +26,9 @@ namespace osu.Game.Screens.Spectate
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class SpectatorScreen : OsuScreen
|
public abstract class SpectatorScreen : OsuScreen
|
||||||
{
|
{
|
||||||
private readonly int[] userIds;
|
protected IReadOnlyList<int> UserIds => userIds;
|
||||||
|
|
||||||
|
private readonly List<int> userIds = new List<int>();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapManager beatmaps { get; set; }
|
private BeatmapManager beatmaps { get; set; }
|
||||||
@ -54,7 +56,7 @@ namespace osu.Game.Screens.Spectate
|
|||||||
/// <param name="userIds">The users to spectate.</param>
|
/// <param name="userIds">The users to spectate.</param>
|
||||||
protected SpectatorScreen(params int[] userIds)
|
protected SpectatorScreen(params int[] userIds)
|
||||||
{
|
{
|
||||||
this.userIds = userIds;
|
this.userIds.AddRange(userIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -80,20 +82,18 @@ namespace osu.Game.Screens.Spectate
|
|||||||
|
|
||||||
private Task populateAllUsers()
|
private Task populateAllUsers()
|
||||||
{
|
{
|
||||||
var userLookupTasks = new Task[userIds.Length];
|
var userLookupTasks = new List<Task>();
|
||||||
|
|
||||||
for (int i = 0; i < userIds.Length; i++)
|
foreach (var u in userIds)
|
||||||
{
|
{
|
||||||
var userId = userIds[i];
|
userLookupTasks.Add(userLookupCache.GetUserAsync(u).ContinueWith(task =>
|
||||||
|
|
||||||
userLookupTasks[i] = userLookupCache.GetUserAsync(userId).ContinueWith(task =>
|
|
||||||
{
|
{
|
||||||
if (!task.IsCompletedSuccessfully)
|
if (!task.IsCompletedSuccessfully)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
lock (stateLock)
|
lock (stateLock)
|
||||||
userMap[userId] = task.Result;
|
userMap[u] = task.Result;
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.WhenAll(userLookupTasks);
|
return Task.WhenAll(userLookupTasks);
|
||||||
@ -239,6 +239,23 @@ namespace osu.Game.Screens.Spectate
|
|||||||
/// <param name="userId">The user to end gameplay for.</param>
|
/// <param name="userId">The user to end gameplay for.</param>
|
||||||
protected abstract void EndGameplay(int userId);
|
protected abstract void EndGameplay(int userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops spectating a user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">The user to stop spectating.</param>
|
||||||
|
protected void RemoveUser(int userId)
|
||||||
|
{
|
||||||
|
lock (stateLock)
|
||||||
|
{
|
||||||
|
userFinishedPlaying(userId, null);
|
||||||
|
|
||||||
|
userIds.Remove(userId);
|
||||||
|
userMap.Remove(userId);
|
||||||
|
|
||||||
|
spectatorClient.StopWatchingUser(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
// 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 JetBrains.Annotations;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
|
using osu.Game.Extensions;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -10,7 +12,13 @@ namespace osu.Game.Skinning
|
|||||||
public class DefaultLegacySkin : LegacySkin
|
public class DefaultLegacySkin : LegacySkin
|
||||||
{
|
{
|
||||||
public DefaultLegacySkin(IResourceStore<byte[]> storage, IStorageResourceProvider resources)
|
public DefaultLegacySkin(IResourceStore<byte[]> storage, IStorageResourceProvider resources)
|
||||||
: base(Info, storage, resources, string.Empty)
|
: this(Info, storage, resources)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
|
||||||
|
public DefaultLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, IStorageResourceProvider resources)
|
||||||
|
: base(skin, storage, resources, string.Empty)
|
||||||
{
|
{
|
||||||
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
|
Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
|
||||||
Configuration.AddComboColours(
|
Configuration.AddComboColours(
|
||||||
@ -27,7 +35,8 @@ namespace osu.Game.Skinning
|
|||||||
{
|
{
|
||||||
ID = SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon.
|
ID = SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon.
|
||||||
Name = "osu!classic",
|
Name = "osu!classic",
|
||||||
Creator = "team osu!"
|
Creator = "team osu!",
|
||||||
|
InstantiationInfo = typeof(DefaultLegacySkin).GetInvariantInstantiationInfo()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,30 +2,122 @@
|
|||||||
// 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.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.OpenGL.Textures;
|
using osu.Framework.Graphics.OpenGL.Textures;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.IO;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Skinning
|
namespace osu.Game.Skinning
|
||||||
{
|
{
|
||||||
public class DefaultSkin : Skin
|
public class DefaultSkin : Skin
|
||||||
{
|
{
|
||||||
public DefaultSkin()
|
public DefaultSkin(IStorageResourceProvider resources)
|
||||||
: base(SkinInfo.Default)
|
: this(SkinInfo.Default, resources)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
|
||||||
|
public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources)
|
||||||
|
: base(skin, resources)
|
||||||
{
|
{
|
||||||
Configuration = new DefaultSkinConfiguration();
|
Configuration = new DefaultSkinConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Drawable GetDrawableComponent(ISkinComponent component) => null;
|
|
||||||
|
|
||||||
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
|
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
|
||||||
|
|
||||||
public override ISample GetSample(ISampleInfo sampleInfo) => null;
|
public override ISample GetSample(ISampleInfo sampleInfo) => null;
|
||||||
|
|
||||||
|
public override Drawable GetDrawableComponent(ISkinComponent component)
|
||||||
|
{
|
||||||
|
if (base.GetDrawableComponent(component) is Drawable c)
|
||||||
|
return c;
|
||||||
|
|
||||||
|
switch (component)
|
||||||
|
{
|
||||||
|
case SkinnableTargetComponent target:
|
||||||
|
switch (target.Target)
|
||||||
|
{
|
||||||
|
case SkinnableTarget.MainHUDComponents:
|
||||||
|
var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container =>
|
||||||
|
{
|
||||||
|
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
|
||||||
|
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
|
||||||
|
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (score != null)
|
||||||
|
{
|
||||||
|
score.Anchor = Anchor.TopCentre;
|
||||||
|
score.Origin = Anchor.TopCentre;
|
||||||
|
|
||||||
|
// elements default to beneath the health bar
|
||||||
|
const float vertical_offset = 30;
|
||||||
|
|
||||||
|
const float horizontal_padding = 20;
|
||||||
|
|
||||||
|
score.Position = new Vector2(0, vertical_offset);
|
||||||
|
|
||||||
|
if (accuracy != null)
|
||||||
|
{
|
||||||
|
accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5);
|
||||||
|
accuracy.Origin = Anchor.TopRight;
|
||||||
|
accuracy.Anchor = Anchor.TopCentre;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combo != null)
|
||||||
|
{
|
||||||
|
combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5);
|
||||||
|
combo.Anchor = Anchor.TopCentre;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboCounter)),
|
||||||
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreCounter)),
|
||||||
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)),
|
||||||
|
GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return skinnableTargetWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HUDSkinComponent hudComponent:
|
||||||
|
{
|
||||||
|
switch (hudComponent.Component)
|
||||||
|
{
|
||||||
|
case HUDSkinComponents.ComboCounter:
|
||||||
|
return new DefaultComboCounter();
|
||||||
|
|
||||||
|
case HUDSkinComponents.ScoreCounter:
|
||||||
|
return new DefaultScoreCounter();
|
||||||
|
|
||||||
|
case HUDSkinComponents.AccuracyCounter:
|
||||||
|
return new DefaultAccuracyCounter();
|
||||||
|
|
||||||
|
case HUDSkinComponents.HealthDisplay:
|
||||||
|
return new DefaultHealthDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||||
{
|
{
|
||||||
switch (lookup)
|
switch (lookup)
|
||||||
|
@ -16,7 +16,7 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Skinning.Editor
|
namespace osu.Game.Skinning.Editor
|
||||||
{
|
{
|
||||||
public class SkinBlueprint : SelectionBlueprint<ISkinnableComponent>
|
public class SkinBlueprint : SelectionBlueprint<ISkinnableDrawable>
|
||||||
{
|
{
|
||||||
private Container box;
|
private Container box;
|
||||||
|
|
||||||
@ -24,17 +24,12 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
private Drawable drawable => (Drawable)Item;
|
private Drawable drawable => (Drawable)Item;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the blueprint should be shown even when the <see cref="SelectionBlueprint{T}.Item"/> is not alive.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual bool AlwaysShowWhenSelected => true;
|
|
||||||
|
|
||||||
protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent;
|
protected override bool ShouldBeAlive => drawable.IsAlive && Item.IsPresent;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
public SkinBlueprint(ISkinnableComponent component)
|
public SkinBlueprint(ISkinnableDrawable component)
|
||||||
: base(component)
|
: base(component)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,97 @@
|
|||||||
// 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.Linq;
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
|
using osu.Game.Screens;
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
|
|
||||||
namespace osu.Game.Skinning.Editor
|
namespace osu.Game.Skinning.Editor
|
||||||
{
|
{
|
||||||
public class SkinBlueprintContainer : BlueprintContainer<ISkinnableComponent>
|
public class SkinBlueprintContainer : BlueprintContainer<ISkinnableDrawable>
|
||||||
{
|
{
|
||||||
private readonly Drawable target;
|
private readonly Drawable target;
|
||||||
|
|
||||||
|
private readonly List<BindableList<ISkinnableDrawable>> targetComponents = new List<BindableList<ISkinnableDrawable>>();
|
||||||
|
|
||||||
public SkinBlueprintContainer(Drawable target)
|
public SkinBlueprintContainer(Drawable target)
|
||||||
{
|
{
|
||||||
this.target = target;
|
this.target = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader(true)]
|
||||||
|
private void load(SkinEditor editor)
|
||||||
|
{
|
||||||
|
SelectedItems.BindTo(editor.SelectedComponents);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
checkForComponents();
|
// track each target container on the current screen.
|
||||||
|
var targetContainers = target.ChildrenOfType<ISkinnableTarget>().ToArray();
|
||||||
|
|
||||||
|
if (targetContainers.Length == 0)
|
||||||
|
{
|
||||||
|
var targetScreen = target.ChildrenOfType<Screen>().LastOrDefault()?.GetType().Name ?? "this screen";
|
||||||
|
|
||||||
|
AddInternal(new ScreenWhiteBox.UnderConstructionMessage(targetScreen, "doesn't support skin customisation just yet."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var targetContainer in targetContainers)
|
||||||
|
{
|
||||||
|
var bindableList = new BindableList<ISkinnableDrawable> { BindTarget = targetContainer.Components };
|
||||||
|
bindableList.BindCollectionChanged(componentsChanged, true);
|
||||||
|
|
||||||
|
targetComponents.Add(bindableList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkForComponents()
|
private void componentsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
foreach (var c in target.ChildrenOfType<ISkinnableComponent>().ToArray()) AddBlueprintFor(c);
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>())
|
||||||
|
AddBlueprintFor(item);
|
||||||
|
break;
|
||||||
|
|
||||||
// We'd hope to eventually be running this in a more sensible way, but this handles situations where new drawables become present (ie. during ongoing gameplay)
|
case NotifyCollectionChangedAction.Remove:
|
||||||
// or when drawables in the target are loaded asynchronously and may not be immediately available when this BlueprintContainer is loaded.
|
case NotifyCollectionChangedAction.Reset:
|
||||||
Scheduler.AddDelayed(checkForComponents, 1000);
|
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>())
|
||||||
|
RemoveBlueprintFor(item);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Replace:
|
||||||
|
foreach (var item in e.OldItems.Cast<ISkinnableDrawable>())
|
||||||
|
RemoveBlueprintFor(item);
|
||||||
|
|
||||||
|
foreach (var item in e.NewItems.Cast<ISkinnableDrawable>())
|
||||||
|
AddBlueprintFor(item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SelectionHandler<ISkinnableComponent> CreateSelectionHandler() => new SkinSelectionHandler();
|
protected override void AddBlueprintFor(ISkinnableDrawable item)
|
||||||
|
{
|
||||||
|
if (!item.IsEditable)
|
||||||
|
return;
|
||||||
|
|
||||||
protected override SelectionBlueprint<ISkinnableComponent> CreateBlueprintFor(ISkinnableComponent component)
|
base.AddBlueprintFor(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override SelectionHandler<ISkinnableDrawable> CreateSelectionHandler() => new SkinSelectionHandler();
|
||||||
|
|
||||||
|
protected override SelectionBlueprint<ISkinnableDrawable> CreateBlueprintFor(ISkinnableDrawable component)
|
||||||
=> new SkinBlueprint(component);
|
=> new SkinBlueprint(component);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Skinning.Editor
|
|||||||
Spacing = new Vector2(20)
|
Spacing = new Vector2(20)
|
||||||
};
|
};
|
||||||
|
|
||||||
var skinnableTypes = typeof(OsuGame).Assembly.GetTypes().Where(t => typeof(ISkinnableComponent).IsAssignableFrom(t)).ToArray();
|
var skinnableTypes = typeof(OsuGame).Assembly.GetTypes().Where(t => typeof(ISkinnableDrawable).IsAssignableFrom(t)).ToArray();
|
||||||
|
|
||||||
foreach (var type in skinnableTypes)
|
foreach (var type in skinnableTypes)
|
||||||
{
|
{
|
||||||
@ -78,6 +78,9 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
Debug.Assert(instance != null);
|
Debug.Assert(instance != null);
|
||||||
|
|
||||||
|
if (!((ISkinnableDrawable)instance).IsEditable)
|
||||||
|
return null;
|
||||||
|
|
||||||
return new ToolboxComponentButton(instance);
|
return new ToolboxComponentButton(instance);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
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.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
@ -11,28 +12,43 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Cursor;
|
using osu.Game.Graphics.Cursor;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Skinning.Editor
|
namespace osu.Game.Skinning.Editor
|
||||||
{
|
{
|
||||||
|
[Cached(typeof(SkinEditor))]
|
||||||
public class SkinEditor : FocusedOverlayContainer
|
public class SkinEditor : FocusedOverlayContainer
|
||||||
{
|
{
|
||||||
public const double TRANSITION_DURATION = 500;
|
public const double TRANSITION_DURATION = 500;
|
||||||
|
|
||||||
private readonly Drawable target;
|
public readonly BindableList<ISkinnableDrawable> SelectedComponents = new BindableList<ISkinnableDrawable>();
|
||||||
|
|
||||||
private OsuTextFlowContainer headerText;
|
|
||||||
|
|
||||||
protected override bool StartHidden => true;
|
protected override bool StartHidden => true;
|
||||||
|
|
||||||
public SkinEditor(Drawable target)
|
private readonly Drawable targetScreen;
|
||||||
|
|
||||||
|
private OsuTextFlowContainer headerText;
|
||||||
|
|
||||||
|
private Bindable<Skin> currentSkin;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SkinManager skins { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
private bool hasBegunMutating;
|
||||||
|
|
||||||
|
public SkinEditor(Drawable targetScreen)
|
||||||
{
|
{
|
||||||
this.target = target;
|
this.targetScreen = targetScreen;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load()
|
||||||
{
|
{
|
||||||
InternalChild = new OsuContextMenuContainer
|
InternalChild = new OsuContextMenuContainer
|
||||||
{
|
{
|
||||||
@ -47,37 +63,165 @@ namespace osu.Game.Skinning.Editor
|
|||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
RelativeSizeAxes = Axes.X
|
RelativeSizeAxes = Axes.X
|
||||||
},
|
},
|
||||||
new SkinBlueprintContainer(target),
|
new GridContainer
|
||||||
new SkinComponentToolbox(600)
|
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreLeft,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Origin = Anchor.CentreLeft,
|
ColumnDimensions = new[]
|
||||||
RequestPlacement = placeComponent
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
|
new Dimension()
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new SkinComponentToolbox(600)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
RequestPlacement = placeComponent
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new SkinBlueprintContainer(targetScreen),
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Spacing = new Vector2(5),
|
||||||
|
Padding = new MarginPadding
|
||||||
|
{
|
||||||
|
Top = 10,
|
||||||
|
Left = 10,
|
||||||
|
},
|
||||||
|
Margin = new MarginPadding
|
||||||
|
{
|
||||||
|
Right = 10,
|
||||||
|
Bottom = 10,
|
||||||
|
},
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new TriangleButton
|
||||||
|
{
|
||||||
|
Text = "Save Changes",
|
||||||
|
Width = 140,
|
||||||
|
Action = Save,
|
||||||
|
},
|
||||||
|
new DangerousTriangleButton
|
||||||
|
{
|
||||||
|
Text = "Revert to default",
|
||||||
|
Width = 140,
|
||||||
|
Action = revert,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
headerText.AddParagraph("Skin editor (preview)", cp => cp.Font = OsuFont.Default.With(size: 24));
|
|
||||||
headerText.AddParagraph("This is a preview of what is to come. Changes are lost on changing screens.", cp =>
|
|
||||||
{
|
|
||||||
cp.Font = OsuFont.Default.With(size: 12);
|
|
||||||
cp.Colour = colours.Yellow;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void placeComponent(Type type)
|
|
||||||
{
|
|
||||||
var instance = (Drawable)Activator.CreateInstance(type);
|
|
||||||
|
|
||||||
var targetContainer = target.ChildrenOfType<IDefaultSkinnableTarget>().FirstOrDefault();
|
|
||||||
|
|
||||||
targetContainer?.Add(instance);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Show();
|
Show();
|
||||||
|
|
||||||
|
// as long as the skin editor is loaded, let's make sure we can modify the current skin.
|
||||||
|
currentSkin = skins.CurrentSkin.GetBoundCopy();
|
||||||
|
|
||||||
|
// schedule ensures this only happens when the skin editor is visible.
|
||||||
|
// also avoid some weird endless recursion / bindable feedback loop (something to do with tracking skins across three different bindable types).
|
||||||
|
// probably something which will be factored out in a future database refactor so not too concerning for now.
|
||||||
|
currentSkin.BindValueChanged(skin =>
|
||||||
|
{
|
||||||
|
hasBegunMutating = false;
|
||||||
|
Scheduler.AddOnce(skinChanged);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void skinChanged()
|
||||||
|
{
|
||||||
|
headerText.Clear();
|
||||||
|
|
||||||
|
headerText.AddParagraph("Skin editor", cp => cp.Font = OsuFont.Default.With(size: 24));
|
||||||
|
headerText.NewParagraph();
|
||||||
|
headerText.AddText("Currently editing ", cp =>
|
||||||
|
{
|
||||||
|
cp.Font = OsuFont.Default.With(size: 12);
|
||||||
|
cp.Colour = colours.Yellow;
|
||||||
|
});
|
||||||
|
|
||||||
|
headerText.AddText($"{currentSkin.Value.SkinInfo}", cp =>
|
||||||
|
{
|
||||||
|
cp.Font = OsuFont.Default.With(size: 12, weight: FontWeight.Bold);
|
||||||
|
cp.Colour = colours.Yellow;
|
||||||
|
});
|
||||||
|
|
||||||
|
skins.EnsureMutableSkin();
|
||||||
|
hasBegunMutating = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeComponent(Type type)
|
||||||
|
{
|
||||||
|
var targetContainer = getTarget(SkinnableTarget.MainHUDComponents);
|
||||||
|
|
||||||
|
if (targetContainer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!(Activator.CreateInstance(type) is ISkinnableDrawable component))
|
||||||
|
throw new InvalidOperationException($"Attempted to instantiate a component for placement which was not an {typeof(ISkinnableDrawable)}.");
|
||||||
|
|
||||||
|
var drawableComponent = (Drawable)component;
|
||||||
|
|
||||||
|
// give newly added components a sane starting location.
|
||||||
|
drawableComponent.Origin = Anchor.TopCentre;
|
||||||
|
drawableComponent.Anchor = Anchor.TopCentre;
|
||||||
|
drawableComponent.Y = targetContainer.DrawSize.Y / 2;
|
||||||
|
|
||||||
|
targetContainer.Add(component);
|
||||||
|
|
||||||
|
SelectedComponents.Clear();
|
||||||
|
SelectedComponents.Add(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ISkinnableTarget getTarget(SkinnableTarget target)
|
||||||
|
{
|
||||||
|
return targetScreen.ChildrenOfType<ISkinnableTarget>().FirstOrDefault(c => c.Target == target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void revert()
|
||||||
|
{
|
||||||
|
SkinnableTargetContainer[] targetContainers = targetScreen.ChildrenOfType<SkinnableTargetContainer>().ToArray();
|
||||||
|
|
||||||
|
foreach (var t in targetContainers)
|
||||||
|
{
|
||||||
|
currentSkin.Value.ResetDrawableTarget(t);
|
||||||
|
|
||||||
|
// add back default components
|
||||||
|
getTarget(t.Target).Reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
if (!hasBegunMutating)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SkinnableTargetContainer[] targetContainers = targetScreen.ChildrenOfType<SkinnableTargetContainer>().ToArray();
|
||||||
|
|
||||||
|
foreach (var t in targetContainers)
|
||||||
|
currentSkin.Value.UpdateDrawableTarget(t);
|
||||||
|
|
||||||
|
skins.Save(skins.CurrentSkin.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e) => true;
|
protected override bool OnHover(HoverEvent e) => true;
|
||||||
|
@ -71,7 +71,7 @@ namespace osu.Game.Skinning.Editor
|
|||||||
target.RelativePositionAxes = Axes.Both;
|
target.RelativePositionAxes = Axes.Both;
|
||||||
|
|
||||||
target.ScaleTo(VISIBLE_TARGET_SCALE, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
|
target.ScaleTo(VISIBLE_TARGET_SCALE, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
|
||||||
target.MoveToX(0.1f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
|
target.MoveToX(0.095f, SkinEditor.TRANSITION_DURATION, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -92,8 +92,10 @@ namespace osu.Game.Skinning.Editor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
skinEditor?.Save();
|
||||||
skinEditor?.Hide();
|
skinEditor?.Hide();
|
||||||
skinEditor?.Expire();
|
skinEditor?.Expire();
|
||||||
|
|
||||||
skinEditor = null;
|
skinEditor = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Skinning.Editor
|
namespace osu.Game.Skinning.Editor
|
||||||
{
|
{
|
||||||
public class SkinSelectionHandler : SelectionHandler<ISkinnableComponent>
|
public class SkinSelectionHandler : SelectionHandler<ISkinnableDrawable>
|
||||||
{
|
{
|
||||||
public override bool HandleRotation(float angle)
|
public override bool HandleRotation(float angle)
|
||||||
{
|
{
|
||||||
@ -51,7 +51,7 @@ namespace osu.Game.Skinning.Editor
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool HandleMovement(MoveSelectionEvent<ISkinnableComponent> moveEvent)
|
public override bool HandleMovement(MoveSelectionEvent<ISkinnableDrawable> moveEvent)
|
||||||
{
|
{
|
||||||
foreach (var c in SelectedBlueprints)
|
foreach (var c in SelectedBlueprints)
|
||||||
{
|
{
|
||||||
@ -72,7 +72,7 @@ namespace osu.Game.Skinning.Editor
|
|||||||
SelectionBox.CanReverse = false;
|
SelectionBox.CanReverse = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DeleteItems(IEnumerable<ISkinnableComponent> items)
|
protected override void DeleteItems(IEnumerable<ISkinnableDrawable> items)
|
||||||
{
|
{
|
||||||
foreach (var i in items)
|
foreach (var i in items)
|
||||||
{
|
{
|
||||||
@ -81,17 +81,22 @@ namespace osu.Game.Skinning.Editor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableComponent>> selection)
|
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection)
|
||||||
{
|
{
|
||||||
yield return new OsuMenuItem("Anchor")
|
yield return new OsuMenuItem("Anchor")
|
||||||
{
|
{
|
||||||
Items = createAnchorItems().ToArray()
|
Items = createAnchorItems(d => d.Anchor, applyAnchor).ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
yield return new OsuMenuItem("Origin")
|
||||||
|
{
|
||||||
|
Items = createAnchorItems(d => d.Origin, applyOrigin).ToArray()
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
foreach (var item in base.GetContextMenuItemsForSelection(selection))
|
||||||
yield return item;
|
yield return item;
|
||||||
|
|
||||||
IEnumerable<AnchorMenuItem> createAnchorItems()
|
IEnumerable<AnchorMenuItem> createAnchorItems(Func<Drawable, Anchor> checkFunction, Action<Anchor> applyFunction)
|
||||||
{
|
{
|
||||||
var displayableAnchors = new[]
|
var displayableAnchors = new[]
|
||||||
{
|
{
|
||||||
@ -108,18 +113,36 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
return displayableAnchors.Select(a =>
|
return displayableAnchors.Select(a =>
|
||||||
{
|
{
|
||||||
return new AnchorMenuItem(a, selection, _ => applyAnchor(a))
|
return new AnchorMenuItem(a, selection, _ => applyFunction(a))
|
||||||
{
|
{
|
||||||
State = { Value = GetStateFromSelection(selection, c => ((Drawable)c.Item).Anchor == a) }
|
State = { Value = GetStateFromSelection(selection, c => checkFunction((Drawable)c.Item) == a) }
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyOrigin(Anchor anchor)
|
||||||
|
{
|
||||||
|
foreach (var item in SelectedItems)
|
||||||
|
{
|
||||||
|
var drawable = (Drawable)item;
|
||||||
|
|
||||||
|
var previousOrigin = drawable.OriginPosition;
|
||||||
|
drawable.Origin = anchor;
|
||||||
|
drawable.Position += drawable.OriginPosition - previousOrigin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void applyAnchor(Anchor anchor)
|
private void applyAnchor(Anchor anchor)
|
||||||
{
|
{
|
||||||
foreach (var item in SelectedItems)
|
foreach (var item in SelectedItems)
|
||||||
((Drawable)item).Anchor = anchor;
|
{
|
||||||
|
var drawable = (Drawable)item;
|
||||||
|
|
||||||
|
var previousAnchor = drawable.AnchorPosition;
|
||||||
|
drawable.Anchor = anchor;
|
||||||
|
drawable.Position -= drawable.AnchorPosition - previousAnchor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
|
||||||
@ -144,7 +167,7 @@ namespace osu.Game.Skinning.Editor
|
|||||||
|
|
||||||
public class AnchorMenuItem : TernaryStateMenuItem
|
public class AnchorMenuItem : TernaryStateMenuItem
|
||||||
{
|
{
|
||||||
public AnchorMenuItem(Anchor anchor, IEnumerable<SelectionBlueprint<ISkinnableComponent>> selection, Action<TernaryState> action)
|
public AnchorMenuItem(Anchor anchor, IEnumerable<SelectionBlueprint<ISkinnableDrawable>> selection, Action<TernaryState> action)
|
||||||
: base(anchor.ToString(), getNextState, MenuItemType.Standard, action)
|
: base(anchor.ToString(), getNextState, MenuItemType.Standard, action)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,11 @@ namespace osu.Game.Skinning
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications.
|
/// Denotes a drawable which, as a drawable, can be adjusted via skinning specifications.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISkinnableComponent : IDrawable
|
public interface ISkinnableDrawable : IDrawable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this component should be editable by an end user.
|
||||||
|
/// </summary>
|
||||||
|
bool IsEditable => true;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,15 +1,44 @@
|
|||||||
// 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.Linq;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.Screens.Play.HUD;
|
||||||
|
|
||||||
namespace osu.Game.Skinning
|
namespace osu.Game.Skinning
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Denotes a container which can house <see cref="ISkinnableComponent"/>s.
|
/// Denotes a container which can house <see cref="ISkinnableDrawable"/>s.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISkinnableTarget : IContainerCollection<Drawable>
|
public interface ISkinnableTarget : IDrawable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The definition of this target.
|
||||||
|
/// </summary>
|
||||||
|
SkinnableTarget Target { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A bindable list of components which are being tracked by this skinnable target.
|
||||||
|
/// </summary>
|
||||||
|
IBindableList<ISkinnableDrawable> Components { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serialise all children as <see cref="SkinnableInfo"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The serialised content.</returns>
|
||||||
|
IEnumerable<SkinnableInfo> CreateSkinnableInfo() => Components.Select(d => ((Drawable)d).CreateSkinnableInfo());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reload this target from the current skin.
|
||||||
|
/// </summary>
|
||||||
|
void Reload();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add the provided item to this target.
|
||||||
|
/// </summary>
|
||||||
|
void Add(ISkinnableDrawable drawable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user