1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 14:12:54 +08:00

Merge branch 'master' into scroll-to-center

This commit is contained in:
Dan Balasescu 2020-04-21 10:21:08 +09:00 committed by GitHub
commit 3b3a7bdd85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 906 additions and 137 deletions

View File

@ -0,0 +1,106 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModHidden : ModTestScene
{
public TestSceneOsuModHidden()
: base(new OsuRuleset())
{
}
[Test]
public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
{
Mod = new OsuModHidden(),
Autoplay = true,
PassCondition = checkSomeHit
});
[Test]
public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData
{
Mod = new OsuModHidden(),
Autoplay = true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Spinner
{
Position = new Vector2(256, 192),
EndTime = 1000,
},
new Spinner
{
Position = new Vector2(256, 192),
StartTime = 1200,
EndTime = 2200,
},
new HitCircle
{
Position = new Vector2(300, 192),
StartTime = 3200,
},
new HitCircle
{
Position = new Vector2(384, 192),
StartTime = 4200,
}
}
},
PassCondition = checkSomeHit
});
[Test]
public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData
{
Mod = new OsuModHidden(),
Autoplay = true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Spinner
{
Position = new Vector2(256, 192),
EndTime = 1000,
},
new Spinner
{
Position = new Vector2(256, 192),
StartTime = 1200,
EndTime = 2200,
},
new Slider
{
StartTime = 3200,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
},
new Slider
{
StartTime = 5200,
Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
}
}
},
PassCondition = checkSomeHit
});
private bool checkSomeHit()
{
return Player.ScoreProcessor.JudgedHits >= 4;
}
}
}

View File

@ -296,6 +296,44 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[1], HitResult.Great);
}
[Test]
public void TestHitSliderHeadBeforeHitCircle()
{
const double time_circle = 1000;
const double time_slider = 1200;
Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
new TestSlider
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
}
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
@ -371,6 +409,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows();
HeadCircle.HitWindows.SetDifficulty(0);
TailCircle.HitWindows.SetDifficulty(0);
};
}
}

View File

@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;
protected override bool IsFirstHideableObject(DrawableHitObject hitObject) => !(hitObject is DrawableSpinner);
public override void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier;

View File

@ -125,7 +125,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return new DrawableSliderTail(slider, tail);
case SliderHeadCircle head:
return new DrawableSliderHead(slider, head) { OnShake = Shake };
return new DrawableSliderHead(slider, head)
{
OnShake = Shake,
CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true
};
case SliderTick tick:
return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position };

View File

@ -1,16 +1,17 @@
// 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 osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// Ensures that <see cref="HitObject"/>s are hit in-order.
/// Ensures that <see cref="HitObject"/>s are hit in-order. Affectionately known as "note lock".
/// If a <see cref="HitObject"/> is hit out of order:
/// <list type="number">
/// <item><description>The hit is blocked if it occurred earlier than the previous <see cref="HitObject"/>'s start time.</description></item>
@ -36,13 +37,9 @@ namespace osu.Game.Rulesets.Osu.UI
{
DrawableHitObject blockingObject = null;
// Find the last hitobject which blocks future hits.
foreach (var obj in hitObjectContainer.AliveObjects)
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
if (obj == hitObject)
break;
if (drawableCanBlockFutureHits(obj))
if (hitObjectCanBlockFutureHits(obj))
blockingObject = obj;
}
@ -54,74 +51,56 @@ namespace osu.Game.Rulesets.Osu.UI
// 1. The last blocking hitobject has been judged.
// 2. The current time is after the last hitobject's start time.
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime)
return true;
return false;
return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
}
/// <summary>
/// Handles a <see cref="HitObject"/> being hit to potentially miss all earlier <see cref="HitObject"/>s.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> that was hit.</param>
public void HandleHit(HitObject hitObject)
public void HandleHit(DrawableHitObject hitObject)
{
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
if (!hitObjectCanBlockFutureHits(hitObject))
return;
double maximumTime = hitObject.StartTime;
if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
// Iterate through and apply miss results to all top-level and nested hitobjects which block future hits.
foreach (var obj in hitObjectContainer.AliveObjects)
foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
if (obj.Judged || obj.HitObject.StartTime >= maximumTime)
if (obj.Judged)
continue;
if (hitObjectCanBlockFutureHits(obj.HitObject))
applyMiss(obj);
foreach (var nested in obj.NestedHitObjects)
{
if (nested.Judged || nested.HitObject.StartTime >= maximumTime)
continue;
if (hitObjectCanBlockFutureHits(nested.HitObject))
applyMiss(nested);
if (hitObjectCanBlockFutureHits(obj))
((DrawableOsuHitObject)obj).MissForcefully();
}
}
static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully();
}
/// <summary>
/// Whether a <see cref="DrawableHitObject"/> blocks hits on future <see cref="DrawableHitObject"/>s until its start time is reached.
/// </summary>
/// <remarks>
/// This will ONLY match on top-most <see cref="DrawableHitObject"/>s.
/// </remarks>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to test.</param>
private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject)
{
// Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over.
return hitObject is DrawableHitCircle || hitObject is DrawableSlider;
}
/// <summary>
/// Whether a <see cref="HitObject"/> blocks hits on future <see cref="HitObject"/>s until its start time is reached.
/// </summary>
/// <remarks>
/// This is more rigorous and may not match on top-most <see cref="HitObject"/>s as <see cref="drawableCanBlockFutureHits"/> does.
/// </remarks>
/// <param name="hitObject">The <see cref="HitObject"/> to test.</param>
private static bool hitObjectCanBlockFutureHits(HitObject hitObject)
{
// Unlike the above we will receive slider tails, but they do not block future hits.
if (hitObject is SliderTailCircle)
return false;
private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject)
=> hitObject is DrawableHitCircle;
// All other hitcircles continue to block future hits.
return hitObject is HitCircle;
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
{
foreach (var obj in hitObjectContainer.AliveObjects)
{
if (obj.HitObject.StartTime >= targetTime)
yield break;
yield return obj;
foreach (var nestedObj in obj.NestedHitObjects)
{
if (nestedObj.HitObject.StartTime >= targetTime)
break;
yield return nestedObj;
}
}
}
}
}

View File

@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.UI
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
// Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
hitPolicy.HandleHit(result.HitObject);
hitPolicy.HandleHit(judgedObject);
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;

View File

@ -0,0 +1,98 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
[HeadlessTest]
public class TestSceneKeyBindings : OsuManualInputManagerTestScene
{
private readonly ActionReceiver receiver;
public TestSceneKeyBindings()
{
Add(new TestKeyBindingContainer
{
Child = receiver = new ActionReceiver()
});
}
[Test]
public void TestDefaultsWhenNotDatabased()
{
AddStep("fire key", () =>
{
InputManager.PressKey(Key.A);
InputManager.ReleaseKey(Key.A);
});
AddAssert("received key", () => receiver.ReceivedAction);
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) =>
throw new System.NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) =>
throw new System.NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) =>
throw new System.NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) =>
throw new System.NotImplementedException();
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0)
{
return new[]
{
new KeyBinding(InputKey.A, TestAction.Down),
};
}
public override string Description => "test";
public override string ShortName => "test";
}
private enum TestAction
{
Down,
}
private class TestKeyBindingContainer : DatabasedKeyBindingContainer<TestAction>
{
public TestKeyBindingContainer()
: base(new TestRuleset().RulesetInfo, 0)
{
}
}
private class ActionReceiver : CompositeDrawable, IKeyBindingHandler<TestAction>
{
public bool ReceivedAction;
public bool OnPressed(TestAction action)
{
ReceivedAction = action == TestAction.Down;
return true;
}
public void OnReleased(TestAction action)
{
}
}
}
}

View File

@ -0,0 +1,43 @@
// 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 NUnit.Framework;
using osu.Game.Overlays;
using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneDashboardOverlay : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(DashboardOverlay),
typeof(DashboardOverlayHeader),
typeof(FriendDisplay)
};
protected override bool UseOnlineAPI => true;
private readonly DashboardOverlay overlay;
public TestSceneDashboardOverlay()
{
Add(overlay = new DashboardOverlay());
}
[Test]
public void TestShow()
{
AddStep("Show", overlay.Show);
}
[Test]
public void TestHide()
{
AddStep("Hide", overlay.Hide);
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Game.Users;
using osu.Game.Overlays;
using osu.Framework.Allocation;
using NUnit.Framework;
using osu.Game.Online.API;
namespace osu.Game.Tests.Visual.Online
{
@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private FriendDisplay display;
private TestFriendDisplay display;
[SetUp]
public void Setup() => Schedule(() =>
@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
Child = new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = display = new FriendDisplay()
Child = display = new TestFriendDisplay()
};
});
@ -83,5 +84,17 @@ namespace osu.Game.Tests.Visual.Online
LastVisit = DateTimeOffset.Now
}
};
private class TestFriendDisplay : FriendDisplay
{
public void Fetch()
{
base.APIStateChanged(API, APIState.Online);
}
public override void APIStateChanged(IAPIProvider api, APIState state)
{
}
}
}
}

View File

@ -0,0 +1,85 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Chat;
using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
[HeadlessTest]
public class TestSceneNowPlayingCommand : OsuTestScene
{
[Cached(typeof(IChannelPostTarget))]
private PostTarget postTarget { get; set; }
public TestSceneNowPlayingCommand()
{
Add(postTarget = new PostTarget());
}
[Test]
public void TestGenericActivity()
{
AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby());
AddStep("Run command", () => Add(new NowPlayingCommand()));
AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is listening"));
}
[Test]
public void TestEditActivity()
{
AddStep("Set activity", () => API.Activity.Value = new UserActivity.Editing(new BeatmapInfo()));
AddStep("Run command", () => Add(new NowPlayingCommand()));
AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is editing"));
}
[Test]
public void TestPlayActivity()
{
AddStep("Set activity", () => API.Activity.Value = new UserActivity.SoloGame(new BeatmapInfo(), new RulesetInfo()));
AddStep("Run command", () => Add(new NowPlayingCommand()));
AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is playing"));
}
[TestCase(true)]
[TestCase(false)]
public void TestLinkPresence(bool hasOnlineId)
{
AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby());
AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null)
{
BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null }
});
AddStep("Run command", () => Add(new NowPlayingCommand()));
if (hasOnlineId)
AddAssert("Check link presence", () => postTarget.LastMessage.Contains("https://osu.ppy.sh/b/1234"));
else
AddAssert("Check link not present", () => !postTarget.LastMessage.Contains("https://"));
}
public class PostTarget : Component, IChannelPostTarget
{
public void PostMessage(string text, bool isAction = false, Channel target = null)
{
LastMessage = text;
}
public string LastMessage { get; private set; }
}
}
}

View File

@ -359,6 +359,68 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null);
}
[Test]
public void TestPresentNewRulesetNewBeatmap()
{
createSongSelect();
changeRuleset(2);
addRulesetImportStep(2);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
addRulesetImportStep(0);
addRulesetImportStep(0);
addRulesetImportStep(0);
BeatmapInfo target = null;
AddStep("select beatmap/ruleset externally", () =>
{
target = manager.GetAllUsableBeatmapSets()
.Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last();
Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0);
Beatmap.Value = manager.GetWorkingBeatmap(target);
});
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
// this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target);
}
[Test]
public void TestPresentNewBeatmapNewRuleset()
{
createSongSelect();
changeRuleset(2);
addRulesetImportStep(2);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
addRulesetImportStep(0);
addRulesetImportStep(0);
addRulesetImportStep(0);
BeatmapInfo target = null;
AddStep("select beatmap/ruleset externally", () =>
{
target = manager.GetAllUsableBeatmapSets()
.Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last();
Beatmap.Value = manager.GetWorkingBeatmap(target);
Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0);
});
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0);
// this is an important check, to make sure updateComponentFromBeatmap() was actually run
AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target);
}
[Test]
public void TestRulesetChangeResetsMods()
{

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual
typeof(OnScreenDisplay),
typeof(NotificationOverlay),
typeof(DirectOverlay),
typeof(SocialOverlay),
typeof(DashboardOverlay),
typeof(ChannelManager),
typeof(ChatOverlay),
typeof(SettingsOverlay),

View File

@ -187,27 +187,13 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[HitObjects]");
// TODO: implement other legacy rulesets
switch (beatmap.BeatmapInfo.RulesetID)
{
case 0:
foreach (var h in beatmap.HitObjects)
handleOsuHitObject(writer, h);
break;
case 1:
foreach (var h in beatmap.HitObjects)
handleTaikoHitObject(writer, h);
break;
case 2:
foreach (var h in beatmap.HitObjects)
handleCatchHitObject(writer, h);
break;
case 3:
foreach (var h in beatmap.HitObjects)
handleManiaHitObject(writer, h);
break;
}
}
@ -328,12 +314,6 @@ namespace osu.Game.Beatmaps.Formats
}
}
private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
private void handleManiaHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false, bool zeroBanks = false)
{
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);

View File

@ -62,6 +62,14 @@ namespace osu.Game.Input.Bindings
store.KeyBindingChanged -= ReloadMappings;
}
protected override void ReloadMappings() => KeyBindings = store.Query(ruleset?.ID, variant).ToList();
protected override void ReloadMappings()
{
if (ruleset != null && !ruleset.ID.HasValue)
// if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings.
// fallback to defaults instead.
KeyBindings = DefaultKeyBindings;
else
KeyBindings = store.Query(ruleset?.ID, variant).ToList();
}
}
}

View File

@ -18,24 +18,32 @@ namespace osu.Game.Online.API
public T Result { get; private set; }
protected APIRequest()
{
base.Success += () => TriggerSuccess(((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject);
}
/// <summary>
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
/// </summary>
public new event APISuccessHandler<T> Success;
protected override void PostProcess()
{
base.PostProcess();
Result = ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
}
internal void TriggerSuccess(T result)
{
if (Result != null)
throw new InvalidOperationException("Attempted to trigger success more than once");
Result = result;
Success?.Invoke(result);
TriggerSuccess();
}
internal override void TriggerSuccess()
{
base.TriggerSuccess();
Success?.Invoke(Result);
}
}
@ -99,6 +107,8 @@ namespace osu.Game.Online.API
if (checkAndScheduleFailure())
return;
PostProcess();
API.Schedule(delegate
{
if (cancelled) return;
@ -107,7 +117,14 @@ namespace osu.Game.Online.API
});
}
internal void TriggerSuccess()
/// <summary>
/// Perform any post-processing actions after a successful request.
/// </summary>
protected virtual void PostProcess()
{
}
internal virtual void TriggerSuccess()
{
Success?.Invoke();
}

View File

@ -18,7 +18,7 @@ namespace osu.Game.Online.Chat
/// <summary>
/// Manages everything channel related
/// </summary>
public class ChannelManager : PollingComponent
public class ChannelManager : PollingComponent, IChannelPostTarget
{
/// <summary>
/// The channels the player joins on startup
@ -204,6 +204,10 @@ namespace osu.Game.Online.Chat
switch (command)
{
case "np":
AddInternal(new NowPlayingCommand());
break;
case "me":
if (string.IsNullOrWhiteSpace(content))
{
@ -234,7 +238,7 @@ namespace osu.Game.Online.Chat
break;
case "help":
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel]"));
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /np"));
break;
default:

View File

@ -0,0 +1,19 @@
// 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.Online.Chat
{
[Cached(typeof(IChannelPostTarget))]
public interface IChannelPostTarget
{
/// <summary>
/// Posts a message to the currently opened channel.
/// </summary>
/// <param name="text">The message text that is going to be posted</param>
/// <param name="isAction">Is true if the message is an action, e.g.: user is currently eating </param>
/// <param name="target">An optional target channel. If null, <see cref="ChannelManager.CurrentChannel"/> will be used.</param>
void PostMessage(string text, bool isAction = false, Channel target = null);
}
}

View File

@ -0,0 +1,55 @@
// 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.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Users;
namespace osu.Game.Online.Chat
{
public class NowPlayingCommand : Component
{
[Resolved]
private IChannelPostTarget channelManager { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private Bindable<WorkingBeatmap> currentBeatmap { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
string verb;
BeatmapInfo beatmap;
switch (api.Activity.Value)
{
case UserActivity.SoloGame solo:
verb = "playing";
beatmap = solo.Beatmap;
break;
case UserActivity.Editing edit:
verb = "editing";
beatmap = edit.Beatmap;
break;
default:
verb = "listening to";
beatmap = currentBeatmap.Value.BeatmapInfo;
break;
}
var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[https://osu.ppy.sh/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString();
channelManager.PostMessage($"is {verb} {beatmapString}", true);
Expire();
}
}
}

View File

@ -3,7 +3,7 @@
using System;
using System.Threading.Tasks;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
namespace osu.Game.Online
@ -11,7 +11,7 @@ namespace osu.Game.Online
/// <summary>
/// A component which requires a constant polling process.
/// </summary>
public abstract class PollingComponent : Component
public abstract class PollingComponent : CompositeDrawable // switch away from Component because InternalChildren are used in usages.
{
private double? lastTimePolled;

View File

@ -67,7 +67,7 @@ namespace osu.Game
private DirectOverlay direct;
private SocialOverlay social;
private DashboardOverlay dashboard;
private UserProfileOverlay userProfile;
@ -611,7 +611,7 @@ namespace osu.Game
//overlay elements
loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true);
loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true);
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
@ -670,7 +670,7 @@ namespace osu.Game
}
// ensure only one of these overlays are open at once.
var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, social, direct, changelogOverlay, rankingsOverlay };
var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, direct, changelogOverlay, rankingsOverlay };
foreach (var overlay in singleDisplayOverlays)
{
@ -842,7 +842,7 @@ namespace osu.Game
return true;
case GlobalAction.ToggleSocial:
social.ToggleVisibility();
dashboard.ToggleVisibility();
return true;
case GlobalAction.ResetInputSettings:

View File

@ -0,0 +1,24 @@
// 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.
namespace osu.Game.Overlays.Dashboard
{
public class DashboardOverlayHeader : TabControlOverlayHeader<DashboardOverlayTabs>
{
protected override OverlayTitle CreateTitle() => new DashboardTitle();
private class DashboardTitle : OverlayTitle
{
public DashboardTitle()
{
Title = "dashboard";
IconTexture = "Icons/changelog";
}
}
}
public enum DashboardOverlayTabs
{
Friends
}
}

View File

@ -16,7 +16,7 @@ using osuTK;
namespace osu.Game.Overlays.Dashboard.Friends
{
public class FriendDisplay : CompositeDrawable
public class FriendDisplay : OverlayView<List<User>>
{
private List<User> users = new List<User>();
@ -26,34 +26,29 @@ namespace osu.Game.Overlays.Dashboard.Friends
set
{
users = value;
onlineStreamControl.Populate(value);
}
}
[Resolved]
private IAPIProvider api { get; set; }
private GetFriendsRequest request;
private CancellationTokenSource cancellationToken;
private Drawable currentContent;
private readonly FriendOnlineStreamControl onlineStreamControl;
private readonly Box background;
private readonly Box controlBackground;
private readonly UserListToolbar userListToolbar;
private readonly Container itemsPlaceholder;
private readonly LoadingLayer loading;
private FriendOnlineStreamControl onlineStreamControl;
private Box background;
private Box controlBackground;
private UserListToolbar userListToolbar;
private Container itemsPlaceholder;
private LoadingLayer loading;
public FriendDisplay()
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
@ -134,11 +129,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
}
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
background.Colour = colourProvider.Background4;
controlBackground.Colour = colourProvider.Background5;
}
@ -152,14 +143,11 @@ namespace osu.Game.Overlays.Dashboard.Friends
userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels());
}
public void Fetch()
{
if (!api.IsLoggedIn)
return;
protected override APIRequest<List<User>> CreateRequest() => new GetFriendsRequest();
request = new GetFriendsRequest();
request.Success += response => Schedule(() => Users = response);
api.Queue(request);
protected override void OnSuccess(List<User> response)
{
Users = response;
}
private void recreatePanels()
@ -258,9 +246,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
protected override void Dispose(bool isDisposing)
{
request?.Cancel();
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}

View File

@ -0,0 +1,150 @@
// 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.Threading;
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.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends;
namespace osu.Game.Overlays
{
public class DashboardOverlay : FullscreenOverlay
{
private CancellationTokenSource cancellationToken;
private Box background;
private Container content;
private DashboardOverlayHeader header;
private LoadingLayer loading;
private OverlayScrollContainer scrollFlow;
public DashboardOverlay()
: base(OverlayColourScheme.Purple)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
header = new DashboardOverlayHeader
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Depth = -float.MaxValue
},
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
}
},
loading = new LoadingLayer(content),
};
background.Colour = ColourProvider.Background5;
}
protected override void LoadComplete()
{
base.LoadComplete();
header.Current.BindValueChanged(onTabChanged);
}
private bool displayUpdateRequired = true;
protected override void PopIn()
{
base.PopIn();
// We don't want to create a new display on every call, only when exiting from fully closed state.
if (displayUpdateRequired)
{
header.Current.TriggerChange();
displayUpdateRequired = false;
}
}
protected override void PopOutComplete()
{
base.PopOutComplete();
loadDisplay(Empty());
displayUpdateRequired = true;
}
private void loadDisplay(Drawable display)
{
scrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
if (API.IsLoggedIn)
loading.Hide();
content.Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
private void onTabChanged(ValueChangedEvent<DashboardOverlayTabs> tab)
{
cancellationToken?.Cancel();
loading.Show();
if (!API.IsLoggedIn)
{
loadDisplay(Empty());
return;
}
switch (tab.NewValue)
{
case DashboardOverlayTabs.Friends:
loadDisplay(new FriendDisplay());
break;
default:
throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
}
}
public override void APIStateChanged(IAPIProvider api, APIState state)
{
if (State.Value == Visibility.Hidden)
return;
header.Current.TriggerChange();
}
protected override void Dispose(bool isDisposing)
{
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}
}

View File

@ -12,6 +12,8 @@ namespace osu.Game.Overlays
{
public abstract class OverlayHeader : Container
{
public const int CONTENT_X_MARGIN = 50;
private readonly Box titleBackground;
protected readonly FillFlowContainer HeaderInfo;
@ -54,7 +56,7 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Horizontal = UserProfileOverlay.CONTENT_X_MARGIN,
Horizontal = CONTENT_X_MARGIN,
},
Children = new[]
{

View File

@ -0,0 +1,79 @@
// 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.Game.Online.API;
namespace osu.Game.Overlays
{
/// <summary>
/// A subview containing online content, to be displayed inside a <see cref="FullscreenOverlay"/>.
/// </summary>
/// <remarks>
/// Automatically performs a data fetch on load.
/// </remarks>
/// <typeparam name="T">The type of the API response.</typeparam>
public abstract class OverlayView<T> : CompositeDrawable, IOnlineComponent
where T : class
{
[Resolved]
protected IAPIProvider API { get; private set; }
private APIRequest<T> request;
protected OverlayView()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
protected override void LoadComplete()
{
base.LoadComplete();
API.Register(this);
}
/// <summary>
/// Create the API request for fetching data.
/// </summary>
protected abstract APIRequest<T> CreateRequest();
/// <summary>
/// Fired when results arrive from the main API request.
/// </summary>
/// <param name="response"></param>
protected abstract void OnSuccess(T response);
/// <summary>
/// Force a re-request for data from the API.
/// </summary>
protected void PerformFetch()
{
request?.Cancel();
request = CreateRequest();
request.Success += response => Schedule(() => OnSuccess(response));
API.Queue(request);
}
public virtual void APIStateChanged(IAPIProvider api, APIState state)
{
switch (state)
{
case APIState.Online:
PerformFetch();
break;
}
}
protected override void Dispose(bool isDisposing)
{
request?.Cancel();
API?.Unregister(this);
base.Dispose(isDisposing);
}
}
}

View File

@ -44,7 +44,7 @@ namespace osu.Game.Overlays
},
TabControl = CreateTabControl().With(control =>
{
control.Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN };
control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN };
control.Current = Current;
})
}

View File

@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Toolbar
}
[BackgroundDependencyLoader(true)]
private void load(SocialOverlay chat)
private void load(DashboardOverlay dashboard)
{
StateContainer = chat;
StateContainer = dashboard;
}
}
}

View File

@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Mods
protected Bindable<bool> IncreaseFirstObjectVisibility = new Bindable<bool>();
/// <summary>
/// Check whether the provided hitobject should be considered the "first" hideable object.
/// Can be used to skip spinners, for instance.
/// </summary>
/// <param name="hitObject">The hitobject to check.</param>
protected virtual bool IsFirstHideableObject(DrawableHitObject hitObject) => true;
public void ReadFromConfig(OsuConfigManager config)
{
IncreaseFirstObjectVisibility = config.GetBindable<bool>(OsuSetting.IncreaseFirstObjectVisibility);
@ -30,8 +37,11 @@ namespace osu.Game.Rulesets.Mods
public virtual void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var d in drawables.Skip(IncreaseFirstObjectVisibility.Value ? 1 : 0))
d.ApplyCustomUpdateState += ApplyHiddenState;
if (IncreaseFirstObjectVisibility.Value)
drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1);
foreach (var dho in drawables)
dho.ApplyCustomUpdateState += ApplyHiddenState;
}
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)

View File

@ -428,7 +428,7 @@ namespace osu.Game.Screens.Select
}
/// <summary>
/// selection has been changed as the result of a user interaction.
/// Selection has been changed as the result of a user interaction.
/// </summary>
private void performUpdateSelected()
{
@ -437,7 +437,7 @@ namespace osu.Game.Screens.Select
selectionChangedDebounce?.Cancel();
if (beatmap == null)
if (beatmapNoDebounce == null)
run();
else
selectionChangedDebounce = Scheduler.AddDelayed(run, 200);
@ -450,9 +450,11 @@ namespace osu.Game.Screens.Select
{
Mods.Value = Array.Empty<Mod>();
// required to return once in order to have the carousel in a good state.
// if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset.
return;
// transferRulesetValue() may trigger a refilter. If the current selection does not match the new ruleset, we want to switch away from it.
// The default logic on WorkingBeatmap change is to switch to a matching ruleset (see workingBeatmapChanged()), but we don't want that here.
// We perform an early selection attempt and clear out the beatmap selection to avoid a second ruleset change (revert).
if (beatmap != null && !Carousel.SelectBeatmap(beatmap, false))
beatmap = null;
}
// We may be arriving here due to another component changing the bindable Beatmap.
@ -716,7 +718,7 @@ namespace osu.Game.Screens.Select
if (decoupledRuleset.Value?.Equals(Ruleset.Value) == true)
return false;
Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\"");
Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\")");
rulesetNoDebounce = decoupledRuleset.Value = Ruleset.Value;
// if we have a pending filter operation, we want to run it now.

View File

@ -19,7 +19,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="DiffPlex" Version="1.6.1" />
<PackageReference Include="Humanizer" Version="2.7.9" />
<PackageReference Include="Humanizer" Version="2.8.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />

View File

@ -76,7 +76,7 @@
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
<ItemGroup Label="Transitive Dependencies">
<PackageReference Include="DiffPlex" Version="1.6.1" />
<PackageReference Include="Humanizer" Version="2.7.9" />
<PackageReference Include="Humanizer" Version="2.8.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />