1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 19:22:56 +08:00

Merge branch 'master' into fix-bad-make-current-call

This commit is contained in:
Dan Balasescu 2021-05-28 00:32:51 +09:00 committed by GitHub
commit 7e0a4562c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 2152 additions and 446 deletions

View File

@ -31,6 +31,12 @@
"commands": [ "commands": [
"CodeFileSanity" "CodeFileSanity"
] ]
},
"ppy.localisationanalyser.tools": {
"version": "2021.524.0",
"commands": [
"localisation"
]
} }
} }
} }

View File

@ -1,7 +0,0 @@
---
name: Feature Request
about: Propose a feature you would like to see in the game!
---
**Describe the new feature:**
**Proposal designs of the feature:**

View File

@ -1,5 +1,12 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Suggestions or feature request
url: https://github.com/ppy/osu/discussions/categories/ideas
about: Got something you think should change or be added? Search for or start a new discussion!
- name: Help
url: https://github.com/ppy/osu/discussions/categories/q-a
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
- name: osu!stable issues - name: osu!stable issues
url: https://github.com/ppy/osu-stable-issues url: https://github.com/ppy/osu-stable-issues
about: For issues regarding osu!stable (not osu!lazer), open them here. about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.521.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.524.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -5,12 +5,14 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
@ -32,6 +34,16 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay))); AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay)));
} }
[Test]
public void TestSpinningSamplePitchShift()
{
AddStep("Add spinner", () => SetContents(() => testSingle(5, true, 4000)));
AddUntilStep("Pitch starts low", () => getSpinningSample().Frequency.Value < 0.8);
AddUntilStep("Pitch increases", () => getSpinningSample().Frequency.Value > 0.8);
PausableSkinnableSound getSpinningSample() => drawableSpinner.ChildrenOfType<PausableSkinnableSound>().FirstOrDefault(s => s.Samples.Any(i => i.LookupNames.Any(l => l.Contains("spinnerspin"))));
}
[TestCase(false)] [TestCase(false)]
[TestCase(true)] [TestCase(true)]
public void TestLongSpinner(bool autoplay) public void TestLongSpinner(bool autoplay)
@ -93,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
base.Update(); base.Update();
if (auto) if (auto)
RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3)); RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 2));
} }
} }
} }

View File

@ -16,6 +16,7 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose;
using osuTK; using osuTK;
@ -25,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{ {
public class SliderSelectionBlueprint : OsuSelectionBlueprint<Slider> public class SliderSelectionBlueprint : OsuSelectionBlueprint<Slider>
{ {
protected new DrawableSlider DrawableObject => (DrawableSlider)base.DrawableObject;
protected SliderBodyPiece BodyPiece { get; private set; } protected SliderBodyPiece BodyPiece { get; private set; }
protected SliderCircleOverlay HeadOverlay { get; private set; } protected SliderCircleOverlay HeadOverlay { get; private set; }
protected SliderCircleOverlay TailOverlay { get; private set; } protected SliderCircleOverlay TailOverlay { get; private set; }
@ -236,7 +239,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)), new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
}; };
public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); // Always refer to the drawable object's slider body so subsequent movement deltas are calculated with updated positions.
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;

View File

@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects; public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; [CanBeNull]
public PlaySliderBody SliderBody => Body.Drawable as PlaySliderBody;
public IBindable<int> PathVersion => pathVersion; public IBindable<int> PathVersion => pathVersion;
private readonly Bindable<int> pathVersion = new Bindable<int>(); private readonly Bindable<int> pathVersion = new Bindable<int>();
@ -215,16 +216,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1); double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
Ball.UpdateProgress(completionProgress); Ball.UpdateProgress(completionProgress);
sliderBody?.UpdateProgress(completionProgress); SliderBody?.UpdateProgress(completionProgress);
foreach (DrawableHitObject hitObject in NestedHitObjects) foreach (DrawableHitObject hitObject in NestedHitObjects)
{ {
if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(sliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(sliderBody?.SnakedEnd ?? 0)); if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking; if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking;
} }
Size = sliderBody?.Size ?? Vector2.Zero; Size = SliderBody?.Size ?? Vector2.Zero;
OriginPosition = sliderBody?.PathOffset ?? Vector2.Zero; OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero;
if (DrawSize != Vector2.Zero) if (DrawSize != Vector2.Zero)
{ {
@ -238,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override void OnKilled() public override void OnKilled()
{ {
base.OnKilled(); base.OnKilled();
sliderBody?.RecyclePath(); SliderBody?.RecyclePath();
} }
protected override void ApplySkin(ISkinSource skin, bool allowFallback) protected override void ApplySkin(ISkinSource skin, bool allowFallback)
@ -324,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
case ArmedState.Hit: case ArmedState.Hit:
Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out); Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
if (sliderBody?.SnakingOut.Value == true) if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break; break;
} }
@ -332,7 +333,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => sliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
private class DefaultSliderBody : PlaySliderBody private class DefaultSliderBody : PlaySliderBody
{ {

View File

@ -7,13 +7,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking, IHasMainCirclePiece public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, IHasMainCirclePiece
{ {
public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject;
@ -111,7 +112,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
} }
public void UpdateSnakingPosition(Vector2 start, Vector2 end) => protected override void OnApply()
Position = HitObject.RepeatIndex % 2 == 0 ? end : start; {
base.OnApply();
if (Slider != null)
Position = Slider.CurvePositionAt(HitObject.RepeatIndex % 2 == 0 ? 1 : 0);
}
} }
} }

View File

@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Bindable<bool> isSpinning; private Bindable<bool> isSpinning;
private bool spinnerFrequencyModulate; private bool spinnerFrequencyModulate;
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
/// <summary> /// <summary>
/// The amount of bonus score gained from spinning after the required number of spins, for display purposes. /// The amount of bonus score gained from spinning after the required number of spins, for display purposes.
/// </summary> /// </summary>
@ -106,9 +109,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
isSpinning.BindValueChanged(updateSpinningSample); isSpinning.BindValueChanged(updateSpinningSample);
} }
private const float spinning_sample_initial_frequency = 1.0f;
private const float spinning_sample_modulated_base_frequency = 0.5f;
protected override void OnFree() protected override void OnFree()
{ {
base.OnFree(); base.OnFree();

View File

@ -79,8 +79,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// Old osu! used hit sounding to determine various hit type information // Old osu! used hit sounding to determine various hit type information
IList<HitSampleInfo> samples = obj.Samples; IList<HitSampleInfo> samples = obj.Samples;
bool strong = samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
switch (obj) switch (obj)
{ {
case IHasDistance distanceData: case IHasDistance distanceData:
@ -94,15 +92,11 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing) for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
{ {
IList<HitSampleInfo> currentSamples = allSamples[i]; IList<HitSampleInfo> currentSamples = allSamples[i];
bool isRim = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE);
strong = currentSamples.Any(s => s.Name == HitSampleInfo.HIT_FINISH);
yield return new Hit yield return new Hit
{ {
StartTime = j, StartTime = j,
Type = isRim ? HitType.Rim : HitType.Centre,
Samples = currentSamples, Samples = currentSamples,
IsStrong = strong
}; };
i = (i + 1) % allSamples.Count; i = (i + 1) % allSamples.Count;
@ -117,7 +111,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{ {
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
IsStrong = strong,
Duration = taikoDuration, Duration = taikoDuration,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4 TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4
}; };
@ -143,16 +136,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
default: default:
{ {
bool isRimDefinition(HitSampleInfo s) => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE;
bool isRim = samples.Any(isRimDefinition);
yield return new Hit yield return new Hit
{ {
StartTime = obj.StartTime, StartTime = obj.StartTime,
Type = isRim ? HitType.Rim : HitType.Centre,
Samples = samples, Samples = samples,
IsStrong = strong
}; };
break; break;

View File

@ -69,7 +69,11 @@ namespace osu.Game.Rulesets.Taiko.Edit
{ {
EditorBeatmap.PerformOnSelection(h => EditorBeatmap.PerformOnSelection(h =>
{ {
if (h is Hit taikoHit) taikoHit.Type = state ? HitType.Rim : HitType.Centre; if (h is Hit taikoHit)
{
taikoHit.Type = state ? HitType.Rim : HitType.Centre;
EditorBeatmap.Update(h);
}
}); });
} }

View File

@ -17,13 +17,25 @@ namespace osu.Game.Rulesets.Taiko.Objects
public HitType Type public HitType Type
{ {
get => TypeBindable.Value; get => TypeBindable.Value;
set set => TypeBindable.Value = value;
{
TypeBindable.Value = value;
updateSamplesFromType();
}
} }
public Hit()
{
TypeBindable.BindValueChanged(_ => updateSamplesFromType());
SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples());
}
private void updateTypeFromSamples()
{
Type = getRimSamples().Any() ? HitType.Rim : HitType.Centre;
}
/// <summary>
/// Returns an array of any samples which would cause this object to be a "rim" type hit.
/// </summary>
private HitSampleInfo[] getRimSamples() => Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray();
private void updateSamplesFromType() private void updateSamplesFromType()
{ {
var rimSamples = getRimSamples(); var rimSamples = getRimSamples();
@ -42,11 +54,6 @@ namespace osu.Game.Rulesets.Taiko.Objects
} }
} }
/// <summary>
/// Returns an array of any samples which would cause this object to be a "rim" type hit.
/// </summary>
private HitSampleInfo[] getRimSamples() => Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray();
protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime };
public class StrongNestedHit : StrongNestedHitObject public class StrongNestedHit : StrongNestedHitObject

View File

@ -33,14 +33,21 @@ namespace osu.Game.Rulesets.Taiko.Objects
public bool IsStrong public bool IsStrong
{ {
get => IsStrongBindable.Value; get => IsStrongBindable.Value;
set set => IsStrongBindable.Value = value;
{
IsStrongBindable.Value = value;
updateSamplesFromStrong();
}
} }
private void updateSamplesFromStrong() protected TaikoStrongableHitObject()
{
IsStrongBindable.BindValueChanged(_ => updateSamplesFromType());
SamplesBindable.BindCollectionChanged((_, __) => updateTypeFromSamples());
}
private void updateTypeFromSamples()
{
IsStrong = getStrongSamples().Any();
}
private void updateSamplesFromType()
{ {
var strongSamples = getStrongSamples(); var strongSamples = getStrongSamples();

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host); var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(new MemoryStream()); await importCollectionsFromStream(osu, new MemoryStream());
Assert.That(osu.CollectionManager.Collections.Count, Is.Zero); Assert.That(osu.CollectionManager.Collections.Count, Is.Zero);
} }
@ -43,7 +43,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host); var osu = LoadOsuIntoHost(host);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host, true); var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
@ -110,7 +110,7 @@ namespace osu.Game.Tests.Collections.IO
ms.Seek(0, SeekOrigin.Begin); ms.Seek(0, SeekOrigin.Begin);
await osu.CollectionManager.Import(ms); await importCollectionsFromStream(osu, ms);
} }
Assert.That(host.UpdateThread.Running, Is.True); Assert.That(host.UpdateThread.Running, Is.True);
@ -134,7 +134,7 @@ namespace osu.Game.Tests.Collections.IO
{ {
var osu = LoadOsuIntoHost(host, true); var osu = LoadOsuIntoHost(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); await importCollectionsFromStream(osu, TestResources.OpenResource("Collections/collections.db"));
// Move first beatmap from second collection into the first. // Move first beatmap from second collection into the first.
osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]); osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]);
@ -169,5 +169,12 @@ namespace osu.Game.Tests.Collections.IO
} }
} }
} }
private static async Task importCollectionsFromStream(TestOsuGameBase osu, Stream stream)
{
// intentionally spin this up on a separate task to avoid disposal deadlocks.
// see https://github.com/EventStore/EventStore/issues/1179
await Task.Run(() => osu.CollectionManager.Import(stream).Wait());
}
} }
} }

View File

@ -88,13 +88,18 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
beforeLoadAction?.Invoke(); beforeLoadAction?.Invoke();
prepareBeatmap();
LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
}
private void prepareBeatmap()
{
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
Beatmap.Value.BeatmapInfo.EpilepsyWarning = epilepsyWarning; Beatmap.Value.BeatmapInfo.EpilepsyWarning = epilepsyWarning;
foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>()) foreach (var mod in SelectedMods.Value.OfType<IApplicableToTrack>())
mod.ApplyToTrack(Beatmap.Value.Track); mod.ApplyToTrack(Beatmap.Value.Track);
LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive)));
} }
[Test] [Test]
@ -178,10 +183,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load slow dummy beatmap", () => AddStep("load slow dummy beatmap", () =>
{ {
LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); prepareBeatmap();
Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000); slowPlayer = new SlowLoadPlayer(false, false);
LoadScreen(loader = new TestPlayerLoader(() => slowPlayer));
}); });
AddStep("schedule slow load", () => Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000));
AddUntilStep("wait for player to be current", () => slowPlayer.IsCurrentScreen()); AddUntilStep("wait for player to be current", () => slowPlayer.IsCurrentScreen());
} }

View File

@ -53,7 +53,8 @@ namespace osu.Game.Tests.Visual.Gameplay
CreateTest(null); CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space)); AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("score shown", () => Player.IsScoreShown); AddUntilStep("wait for score shown", () => Player.IsScoreShown);
AddUntilStep("time less than storyboard duration", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < currentStoryboardDuration);
} }
[Test] [Test]

View File

@ -73,8 +73,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
for (int i = 0; i < users; i++) for (int i = 0; i < users; i++)
spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); spectatorClient.StartPlay(i, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
Client.CurrentMatchPlayingUserIds.Clear(); spectatorClient.Schedule(() =>
Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers); {
Client.CurrentMatchPlayingUserIds.Clear();
Client.CurrentMatchPlayingUserIds.AddRange(spectatorClient.PlayingUsers);
});
Children = new Drawable[] Children = new Drawable[]
{ {
@ -91,6 +94,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
AddUntilStep("wait for load", () => leaderboard.IsLoaded); AddUntilStep("wait for load", () => leaderboard.IsLoaded);
AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0);
} }
[Test] [Test]

View File

@ -0,0 +1,175 @@
// 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 Markdig.Syntax.Inlines;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays;
using osu.Game.Overlays.Wiki.Markdown;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneWikiMarkdownContainer : OsuTestScene
{
private TestMarkdownContainer markdownContainer;
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Orange);
[SetUp]
public void Setup() => Schedule(() =>
{
Children = new Drawable[]
{
new Box
{
Colour = overlayColour.Background5,
RelativeSizeAxes = Axes.Both,
},
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Child = markdownContainer = new TestMarkdownContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
};
});
[Test]
public void TestLink()
{
AddStep("set current path", () => markdownContainer.CurrentPath = "Article_styling_criteria/");
AddStep("set '/wiki/Main_Page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_Page)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_Page");
AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ");
AddStep("set './Writing''", () => markdownContainer.Text = "[wiki writing guidline](./Writing)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Writing");
AddStep("set 'Formatting''", () => markdownContainer.Text = "[wiki formatting guidline](Formatting)");
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/Formatting");
}
[Test]
public void TestOutdatedNoticeBox()
{
AddStep("Add outdated yaml header", () =>
{
markdownContainer.Text = @"---
outdated: true
---";
});
}
[Test]
public void TestNeedsCleanupNoticeBox()
{
AddStep("Add needs cleanup yaml header", () =>
{
markdownContainer.Text = @"---
needs_cleanup: true
---";
});
}
[Test]
public void TestOnlyShowOutdatedNoticeBox()
{
AddStep("Add outdated and needs cleanup yaml", () =>
{
markdownContainer.Text = @"---
outdated: true
needs_cleanup: true
---";
});
}
[Test]
public void TestAbsoluteImage()
{
AddStep("Add absolute image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.Text = "![intro](/wiki/Interface/img/intro-screen.jpg)";
});
}
[Test]
public void TestRelativeImage()
{
AddStep("Add relative image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.CurrentPath = "Interface/";
markdownContainer.Text = "![intro](img/intro-screen.jpg)";
});
}
[Test]
public void TestBlockImage()
{
AddStep("Add paragraph with block image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.CurrentPath = "Interface/";
markdownContainer.Text = @"Line before image
![play menu](img/play-menu.jpg ""Main Menu in osu!"")
Line after image";
});
}
[Test]
public void TestInlineImage()
{
AddStep("Add inline image", () =>
{
markdownContainer.DocumentUrl = "https://dev.ppy.sh";
markdownContainer.Text = "![osu! mode icon](/wiki/shared/mode/osu.png) osu!";
});
}
private class TestMarkdownContainer : WikiMarkdownContainer
{
public LinkInline Link;
public new string DocumentUrl
{
set => base.DocumentUrl = value;
}
public override MarkdownTextFlowContainer CreateTextFlow() => new TestMarkdownTextFlowContainer
{
UrlAdded = link => Link = link,
};
private class TestMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
{
public Action<LinkInline> UrlAdded;
protected override void AddLinkText(string text, LinkInline linkInline)
{
base.AddLinkText(text, linkInline);
UrlAdded?.Invoke(linkInline);
}
protected override void AddImage(LinkInline linkInline) => AddDrawable(new WikiMarkdownImage(linkInline));
}
}
}
}

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.KeyBinding; using osu.Game.Overlays.KeyBinding;
using osuTK.Input; using osuTK.Input;
@ -28,6 +29,39 @@ namespace osu.Game.Tests.Visual.Settings
panel.Show(); panel.Show();
} }
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Scroll to top", () => panel.ChildrenOfType<SettingsPanel.SettingsSectionsContainer>().First().ScrollToTop());
AddWaitStep("wait for scroll", 5);
}
[Test]
public void TestBindingMouseWheelToNonGameplay()
{
scrollToAndStartBinding("Increase volume");
AddStep("press k", () => InputManager.Key(Key.K));
checkBinding("Increase volume", "K");
AddStep("click again", () => InputManager.Click(MouseButton.Left));
AddStep("scroll mouse wheel", () => InputManager.ScrollVerticalBy(1));
checkBinding("Increase volume", "Wheel Up");
}
[Test]
public void TestBindingMouseWheelToGameplay()
{
scrollToAndStartBinding("Left button");
AddStep("press k", () => InputManager.Key(Key.Z));
checkBinding("Left button", "Z");
AddStep("click again", () => InputManager.Click(MouseButton.Left));
AddStep("scroll mouse wheel", () => InputManager.ScrollVerticalBy(1));
checkBinding("Left button", "Z");
}
[Test] [Test]
public void TestClickTwiceOnClearButton() public void TestClickTwiceOnClearButton()
{ {
@ -36,6 +70,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("click first row", () => AddStep("click first row", () =>
{ {
firstRow = panel.ChildrenOfType<KeyBindingRow>().First(); firstRow = panel.ChildrenOfType<KeyBindingRow>().First();
InputManager.MoveMouseTo(firstRow); InputManager.MoveMouseTo(firstRow);
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
@ -104,6 +139,64 @@ namespace osu.Game.Tests.Visual.Settings
} }
} }
[Test]
public void TestSingleBindingResetButton()
{
KeyBindingRow settingsKeyBindingRow = null;
AddStep("click first row", () =>
{
settingsKeyBindingRow = panel.ChildrenOfType<KeyBindingRow>().First();
InputManager.MoveMouseTo(settingsKeyBindingRow);
InputManager.Click(MouseButton.Left);
InputManager.PressKey(Key.P);
InputManager.ReleaseKey(Key.P);
});
AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha > 0);
AddStep("click reset button for bindings", () =>
{
var resetButton = settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First();
resetButton.Click();
});
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0);
AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
}
[Test]
public void TestResetAllBindingsButton()
{
KeyBindingRow settingsKeyBindingRow = null;
AddStep("click first row", () =>
{
settingsKeyBindingRow = panel.ChildrenOfType<KeyBindingRow>().First();
InputManager.MoveMouseTo(settingsKeyBindingRow);
InputManager.Click(MouseButton.Left);
InputManager.PressKey(Key.P);
InputManager.ReleaseKey(Key.P);
});
AddUntilStep("restore button shown", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha > 0);
AddStep("click reset button for bindings", () =>
{
var resetButton = panel.ChildrenOfType<ResetButton>().First();
resetButton.Click();
});
AddUntilStep("restore button hidden", () => settingsKeyBindingRow.ChildrenOfType<RestoreDefaultValueButton<bool>>().First().Alpha == 0);
AddAssert("binding cleared", () => settingsKeyBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().ElementAt(0).KeyBinding.KeyCombination.Equals(settingsKeyBindingRow.Defaults.ElementAt(0)));
}
[Test] [Test]
public void TestClickRowSelectsFirstBinding() public void TestClickRowSelectsFirstBinding()
{ {
@ -135,5 +228,37 @@ namespace osu.Game.Tests.Visual.Settings
AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().IsBinding); AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType<KeyBindingRow.KeyButton>().First().IsBinding);
} }
private void checkBinding(string name, string keyName)
{
AddAssert($"Check {name} is bound to {keyName}", () =>
{
var firstRow = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text == name));
var firstButton = firstRow.ChildrenOfType<KeyBindingRow.KeyButton>().First();
return firstButton.Text.Text == keyName;
});
}
private void scrollToAndStartBinding(string name)
{
KeyBindingRow.KeyButton firstButton = null;
AddStep($"Scroll to {name}", () =>
{
var firstRow = panel.ChildrenOfType<KeyBindingRow>().First(r => r.ChildrenOfType<OsuSpriteText>().Any(s => s.Text == name));
firstButton = firstRow.ChildrenOfType<KeyBindingRow.KeyButton>().First();
panel.ChildrenOfType<SettingsPanel.SettingsSectionsContainer>().First().ScrollTo(firstButton);
});
AddWaitStep("wait for scroll", 5);
AddStep("click to bind", () =>
{
InputManager.MoveMouseTo(firstButton);
InputManager.Click(MouseButton.Left);
});
}
} }
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
{ {
@ -37,7 +38,7 @@ namespace osu.Game.Tests.Visual.Settings
private class TestSettingsTextBox : SettingsTextBox private class TestSettingsTextBox : SettingsTextBox
{ {
public new Drawable RestoreDefaultValueButton => this.ChildrenOfType<RestoreDefaultValueButton>().Single(); public Drawable RestoreDefaultValueButton => this.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
} }
} }
} }

View File

@ -786,9 +786,12 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
} }
private void checkVisibleItemCount(bool diff, int count) => private void checkVisibleItemCount(bool diff, int count)
AddAssert($"{count} {(diff ? "diffs" : "sets")} visible", () => {
// until step required as we are querying against alive items, which are loaded asynchronously inside DrawableCarouselBeatmapSet.
AddUntilStep($"{count} {(diff ? "diffs" : "sets")} visible", () =>
carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count); carousel.Items.Count(s => (diff ? s.Item is CarouselBeatmap : s.Item is CarouselBeatmapSet) && s.Item.Visible) == count);
}
private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null); private void checkNoSelection() => AddAssert("Selection is null", () => currentSelection == null);

View File

@ -86,6 +86,15 @@ _**italic with underscore, bold with asterisk**_";
}); });
} }
[Test]
public void TestLinkWithTitle()
{
AddStep("Add Link with title", () =>
{
markdownContainer.Text = "[wikipedia](https://www.wikipedia.org \"The Free Encyclopedia\")";
});
}
[Test] [Test]
public void TestInlineCode() public void TestInlineCode()
{ {

View File

@ -0,0 +1,183 @@
// 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 System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneUpdateableBeatmapSetCover : OsuTestScene
{
[Test]
public void TestLocal([Values] BeatmapSetCoverType coverType)
{
AddStep("setup cover", () => Child = new UpdateableBeatmapSetCover(coverType)
{
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both,
Masking = true,
});
AddUntilStep("wait for load", () => this.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
}
[Test]
public void TestUnloadAndReload()
{
OsuScrollContainer scroll = null;
List<UpdateableBeatmapSetCover> covers = new List<UpdateableBeatmapSetCover>();
AddStep("setup covers", () =>
{
BeatmapSetInfo setInfo = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
FillFlowContainer fillFlow;
Child = scroll = new OsuScrollContainer
{
Size = new Vector2(500f),
Child = fillFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Padding = new MarginPadding { Bottom = 550 }
}
};
var coverTypes = Enum.GetValues(typeof(BeatmapSetCoverType))
.Cast<BeatmapSetCoverType>()
.ToList();
for (int i = 0; i < 25; i++)
{
var coverType = coverTypes[i % coverTypes.Count];
var cover = new UpdateableBeatmapSetCover(coverType)
{
BeatmapSet = setInfo,
Height = 100,
Masking = true,
};
if (coverType == BeatmapSetCoverType.Cover)
cover.Width = 500;
else if (coverType == BeatmapSetCoverType.Card)
cover.Width = 400;
else if (coverType == BeatmapSetCoverType.List)
cover.Size = new Vector2(100, 50);
fillFlow.Add(cover);
covers.Add(cover);
}
});
var loadedCovers = covers.Where(c => c.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
AddUntilStep("some loaded", () => loadedCovers.Any());
AddStep("scroll to end", () => scroll.ScrollToEnd());
AddUntilStep("all unloaded", () => !loadedCovers.Any());
}
[Test]
public void TestSetNullBeatmapWhileLoading()
{
TestUpdateableBeatmapSetCover updateableCover = null;
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover
{
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both,
Masking = true,
});
AddStep("change model", () => updateableCover.BeatmapSet = null);
AddWaitStep("wait some", 5);
AddAssert("no cover added", () => !updateableCover.ChildrenOfType<DelayedLoadUnloadWrapper>().Any());
}
[Test]
public void TestCoverChangeOnNewBeatmap()
{
TestUpdateableBeatmapSetCover updateableCover = null;
BeatmapSetCover initialCover = null;
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover(0)
{
BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"),
RelativeSizeAxes = Axes.Both,
Masking = true,
Alpha = 0.4f
});
AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Any());
AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType<BeatmapSetCover>().Single());
AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1);
AddStep("switch beatmap",
() => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg"));
AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Except(new[] { initialCover }).Any());
}
private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers { Cover = coverUrl }
}
};
private class TestUpdateableBeatmapSetCover : UpdateableBeatmapSetCover
{
private readonly int loadDelay;
public TestUpdateableBeatmapSetCover(int loadDelay = 10000)
{
this.loadDelay = loadDelay;
}
protected override Drawable CreateDrawable(BeatmapSetInfo model)
{
if (model == null)
return null;
return new TestBeatmapSetCover(model, loadDelay)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
};
}
}
private class TestBeatmapSetCover : BeatmapSetCover
{
private readonly int loadDelay;
public TestBeatmapSetCover(BeatmapSetInfo set, int loadDelay)
: base(set)
{
this.loadDelay = loadDelay;
}
[BackgroundDependencyLoader]
private void load()
{
Thread.Sleep(loadDelay);
}
}
}
}

View File

@ -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.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
@ -8,78 +9,52 @@ using osu.Game.Graphics;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
public class UpdateableBeatmapSetCover : Container public class UpdateableBeatmapSetCover : ModelBackedDrawable<BeatmapSetInfo>
{ {
private Drawable displayedCover; private readonly BeatmapSetCoverType coverType;
private BeatmapSetInfo beatmapSet;
public BeatmapSetInfo BeatmapSet public BeatmapSetInfo BeatmapSet
{ {
get => beatmapSet; get => Model;
set set => Model = value;
{
if (value == beatmapSet) return;
beatmapSet = value;
if (IsLoaded)
updateCover();
}
} }
private BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover; public new bool Masking
public BeatmapSetCoverType CoverType
{ {
get => coverType; get => base.Masking;
set set => base.Masking = value;
{
if (value == coverType) return;
coverType = value;
if (IsLoaded)
updateCover();
}
} }
public UpdateableBeatmapSetCover() public UpdateableBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover)
{ {
Child = new Box this.coverType = coverType;
InternalChild = new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f), Colour = OsuColour.Gray(0.2f),
}; };
} }
protected override void LoadComplete() protected override double LoadDelay => 500;
{
base.LoadComplete();
updateCover();
}
private void updateCover() protected override double TransformDuration => 400;
{
displayedCover?.FadeOut(400);
displayedCover?.Expire();
displayedCover = null;
if (beatmapSet != null) protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad);
protected override Drawable CreateDrawable(BeatmapSetInfo model)
{
if (model == null)
return null;
return new BeatmapSetCover(model, coverType)
{ {
Add(displayedCover = new DelayedLoadUnloadWrapper(() => RelativeSizeAxes = Axes.Both,
{ Anchor = Anchor.Centre,
var cover = new BeatmapSetCover(beatmapSet, coverType) Origin = Anchor.Centre,
{ FillMode = FillMode.Fill,
Anchor = Anchor.Centre, };
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
};
cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out);
return cover;
}));
}
} }
} }
} }

View File

@ -58,8 +58,13 @@ namespace osu.Game.Collections
if (storage.Exists(database_name)) if (storage.Exists(database_name))
{ {
List<BeatmapCollection> beatmapCollections;
using (var stream = storage.GetStream(database_name)) using (var stream = storage.GetStream(database_name))
importCollections(readCollections(stream)); beatmapCollections = readCollections(stream);
// intentionally fire-and-forget async.
importCollections(beatmapCollections);
} }
} }

View File

@ -6,11 +6,13 @@ using Markdig.Extensions.AutoIdentifiers;
using Markdig.Extensions.Tables; using Markdig.Extensions.Tables;
using Markdig.Extensions.Yaml; using Markdig.Extensions.Yaml;
using Markdig.Syntax; using Markdig.Syntax;
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.Graphics.Containers.Markdown; using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
namespace osu.Game.Graphics.Containers.Markdown namespace osu.Game.Graphics.Containers.Markdown
{ {
@ -21,6 +23,16 @@ namespace osu.Game.Graphics.Containers.Markdown
LineSpacing = 21; LineSpacing = 21;
} }
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var api = parent.Get<IAPIProvider>();
// needs to be set before the base BDL call executes to avoid invalidating any already populated markdown content.
DocumentUrl = api.WebsiteRootUrl;
return base.CreateChildDependencies(parent);
}
protected override void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level) protected override void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level)
{ {
switch (markdownObject) switch (markdownObject)

View File

@ -1,48 +1,63 @@
// 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 Markdig.Syntax.Inlines; using Markdig.Syntax.Inlines;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown; using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites; using osu.Game.Online.Chat;
using osu.Framework.Input.Events;
using osu.Game.Overlays; using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown namespace osu.Game.Graphics.Containers.Markdown
{ {
public class OsuMarkdownLinkText : MarkdownLinkText public class OsuMarkdownLinkText : MarkdownLinkText
{ {
[Resolved] [Resolved(canBeNull: true)]
private OverlayColourProvider colourProvider { get; set; } private OsuGame game { get; set; }
private SpriteText spriteText; private readonly string text;
private readonly string title;
public OsuMarkdownLinkText(string text, LinkInline linkInline) public OsuMarkdownLinkText(string text, LinkInline linkInline)
: base(text, linkInline) : base(text, linkInline)
{ {
this.text = text;
title = linkInline.Title;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
spriteText.Colour = colourProvider.Light2; var textDrawable = CreateSpriteText().With(t => t.Text = text);
InternalChildren = new Drawable[]
{
textDrawable,
new OsuMarkdownLinkCompiler(new[] { textDrawable })
{
RelativeSizeAxes = Axes.Both,
Action = OnLinkPressed,
TooltipText = title ?? Url,
}
};
} }
public override SpriteText CreateSpriteText() protected override void OnLinkPressed() => game?.HandleLink(Url);
{
return spriteText = base.CreateSpriteText();
}
protected override bool OnHover(HoverEvent e) private class OsuMarkdownLinkCompiler : DrawableLinkCompiler
{ {
spriteText.Colour = colourProvider.Light1; public OsuMarkdownLinkCompiler(IEnumerable<Drawable> parts)
return base.OnHover(e); : base(parts)
} {
}
protected override void OnHoverLost(HoverLostEvent e) [BackgroundDependencyLoader]
{ private void load(OverlayColourProvider colourProvider)
spriteText.Colour = colourProvider.Light2; {
base.OnHoverLost(e); IdleColour = colourProvider.Light2;
HoverColour = colourProvider.Light1;
}
} }
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -180,9 +181,9 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
private string text; private LocalisableString text;
public string Text public LocalisableString Text
{ {
get => text; get => text;
set set

View File

@ -73,10 +73,11 @@ namespace osu.Game.Input.Bindings
else else
{ {
KeyBindings = store.Query(ruleset?.ID, variant) KeyBindings = store.Query(ruleset?.ID, variant)
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.IntAction))
// this ordering is important to ensure that we read entries from the database in the order // this ordering is important to ensure that we read entries from the database in the order
// enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise // enforced by DefaultKeyBindings. allow for song select to handle actions that may otherwise
// have been eaten by the music controller due to query order. // have been eaten by the music controller due to query order.
.OrderBy(b => defaults.FindIndex(d => (int)d.Action == b.IntAction)).ToList(); .ToList();
} }
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -16,6 +17,17 @@ namespace osu.Game.Input
{ {
public event Action KeyBindingChanged; public event Action KeyBindingChanged;
/// <summary>
/// Keys which should not be allowed for gameplay input purposes.
/// </summary>
private static readonly IEnumerable<InputKey> banned_keys = new[]
{
InputKey.MouseWheelDown,
InputKey.MouseWheelLeft,
InputKey.MouseWheelUp,
InputKey.MouseWheelRight
};
public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null)
: base(contextFactory, storage) : base(contextFactory, storage)
{ {
@ -93,6 +105,9 @@ namespace osu.Game.Input
using (ContextFactory.GetForWrite()) using (ContextFactory.GetForWrite())
{ {
var dbKeyBinding = (DatabasedKeyBinding)keyBinding; var dbKeyBinding = (DatabasedKeyBinding)keyBinding;
Debug.Assert(dbKeyBinding.RulesetID == null || CheckValidForGameplay(keyBinding.KeyCombination));
Refresh(ref dbKeyBinding); Refresh(ref dbKeyBinding);
if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination))
@ -103,5 +118,16 @@ namespace osu.Game.Input
KeyBindingChanged?.Invoke(); KeyBindingChanged?.Invoke();
} }
public static bool CheckValidForGameplay(KeyCombination combination)
{
foreach (var key in banned_keys)
{
if (combination.Keys.Contains(key))
return false;
}
return true;
}
} }
} }

View File

@ -0,0 +1,38 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="solo" xml:space="preserve">
<value>ソロ</value>
</data>
<data name="playlists" xml:space="preserve">
<value>プレイリスト</value>
</data>
<data name="play" xml:space="preserve">
<value>遊ぶ</value>
</data>
<data name="multi" xml:space="preserve">
<value>マルチ</value>
</data>
<data name="edit" xml:space="preserve">
<value>エディット</value>
</data>
<data name="browse" xml:space="preserve">
<value>ブラウズ</value>
</data>
<data name="exit" xml:space="preserve">
<value>閉じる</value>
</data>
<data name="settings" xml:space="preserve">
<value>設定</value>
</data>
</root>

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="solo" xml:space="preserve">
<value>solo</value>
</data>
<data name="multi" xml:space="preserve">
<value>multi</value>
</data>
<data name="playlists" xml:space="preserve">
<value>playlists</value>
</data>
<data name="play" xml:space="preserve">
<value>play</value>
</data>
<data name="edit" xml:space="preserve">
<value>edit</value>
</data>
<data name="browse" xml:space="preserve">
<value>browse</value>
</data>
<data name="settings" xml:space="preserve">
<value>settings</value>
</data>
<data name="back" xml:space="preserve">
<value>back</value>
</data>
<data name="exit" xml:space="preserve">
<value>exit</value>
</data>
</root>

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class ButtonSystemStrings
{
private const string prefix = @"osu.Game.Localisation.ButtonSystem";
/// <summary>
/// "solo"
/// </summary>
public static LocalisableString Solo => new TranslatableString(getKey(@"solo"), @"solo");
/// <summary>
/// "multi"
/// </summary>
public static LocalisableString Multi => new TranslatableString(getKey(@"multi"), @"multi");
/// <summary>
/// "playlists"
/// </summary>
public static LocalisableString Playlists => new TranslatableString(getKey(@"playlists"), @"playlists");
/// <summary>
/// "play"
/// </summary>
public static LocalisableString Play => new TranslatableString(getKey(@"play"), @"play");
/// <summary>
/// "edit"
/// </summary>
public static LocalisableString Edit => new TranslatableString(getKey(@"edit"), @"edit");
/// <summary>
/// "browse"
/// </summary>
public static LocalisableString Browse => new TranslatableString(getKey(@"browse"), @"browse");
/// <summary>
/// "settings"
/// </summary>
public static LocalisableString Settings => new TranslatableString(getKey(@"settings"), @"settings");
/// <summary>
/// "back"
/// </summary>
public static LocalisableString Back => new TranslatableString(getKey(@"back"), @"back");
/// <summary>
/// "exit"
/// </summary>
public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"exit");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="header_title" xml:space="preserve">
<value>chat</value>
</data>
<data name="header_description" xml:space="preserve">
<value>join the real-time discussion</value>
</data>
</root>

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.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class ChatStrings
{
private const string prefix = "osu.Game.Localisation.Chat";
/// <summary>
/// "chat"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "chat");
/// <summary>
/// "join the real-time discussion"
/// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "join the real-time discussion");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="cancel" xml:space="preserve">
<value>Cancel</value>
</data>
</root>

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.Localisation;
namespace osu.Game.Localisation
{
public static class CommonStrings
{
private const string prefix = "osu.Game.Localisation.Common";
/// <summary>
/// "Cancel"
/// </summary>
public static LocalisableString Cancel => new TranslatableString(getKey("cancel"), "Cancel");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,16 @@
// 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.ComponentModel;
namespace osu.Game.Localisation
{
public enum Language
{
[Description("English")]
en,
[Description("日本語")]
ja
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="header_title" xml:space="preserve">
<value>notifications</value>
</data>
<data name="header_description" xml:space="preserve">
<value>waiting for 'ya</value>
</data>
</root>

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.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class NotificationsStrings
{
private const string prefix = "osu.Game.Localisation.Notifications";
/// <summary>
/// "notifications"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "notifications");
/// <summary>
/// "waiting for 'ya"
/// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "waiting for 'ya");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="header_title" xml:space="preserve">
<value>now playing</value>
</data>
<data name="header_description" xml:space="preserve">
<value>manage the currently playing track</value>
</data>
</root>

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.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class NowPlayingStrings
{
private const string prefix = "osu.Game.Localisation.NowPlaying";
/// <summary>
/// "now playing"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "now playing");
/// <summary>
/// "manage the currently playing track"
/// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "manage the currently playing track");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,69 @@
// 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.Globalization;
using System.IO;
using System.Resources;
using System.Threading.Tasks;
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public class ResourceManagerLocalisationStore : ILocalisationStore
{
private readonly Dictionary<string, ResourceManager> resourceManagers = new Dictionary<string, ResourceManager>();
public ResourceManagerLocalisationStore(string cultureCode)
{
EffectiveCulture = new CultureInfo(cultureCode);
}
public void Dispose()
{
}
public string Get(string lookup)
{
var split = lookup.Split(':');
string ns = split[0];
string key = split[1];
lock (resourceManagers)
{
if (!resourceManagers.TryGetValue(ns, out var manager))
resourceManagers[ns] = manager = new ResourceManager(ns, GetType().Assembly);
try
{
return manager.GetString(key, EffectiveCulture);
}
catch (MissingManifestResourceException)
{
// in the case the manifest is missing, it is likely that the user is adding code-first implementations of new localisation namespaces.
// it's fine to ignore this as localisation will fallback to default values.
return null;
}
}
}
public Task<string> GetAsync(string lookup)
{
return Task.FromResult(Get(lookup));
}
public Stream GetStream(string name)
{
throw new NotImplementedException();
}
public IEnumerable<string> GetAvailableResources()
{
throw new NotImplementedException();
}
public CultureInfo EffectiveCulture { get; }
}
}

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="header_title" xml:space="preserve">
<value>settings</value>
</data>
<data name="header_description" xml:space="preserve">
<value>change the way osu! behaves</value>
</data>
</root>

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.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class SettingsStrings
{
private const string prefix = "osu.Game.Localisation.Settings";
/// <summary>
/// "settings"
/// </summary>
public static LocalisableString HeaderTitle => new TranslatableString(getKey("header_title"), "settings");
/// <summary>
/// "change the way osu! behaves"
/// </summary>
public static LocalisableString HeaderDescription => new TranslatableString(getKey("header_description"), "change the way osu! behaves");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -51,6 +51,7 @@ using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel; using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Skinning.Editor; using osu.Game.Skinning.Editor;
namespace osu.Game namespace osu.Game
@ -562,6 +563,12 @@ namespace osu.Game
{ {
base.LoadComplete(); base.LoadComplete();
foreach (var language in Enum.GetValues(typeof(Language)).OfType<Language>())
{
var cultureCode = language.ToString();
Localisation.AddLanguage(cultureCode, new ResourceManagerLocalisationStore(cultureCode));
}
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results // The next time this is updated is in UpdateAfterChildren, which occurs too late and results
// in the cursor being shown for a few frames during the intro. // in the cursor being shown for a few frames during the intro.
// This prevents the cursor from showing until we have a screen with CursorVisible = true // This prevents the cursor from showing until we have a screen with CursorVisible = true

View File

@ -90,7 +90,7 @@ namespace osu.Game.Overlays.BeatmapListing
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
Child = beatmapCover = new UpdateableBeatmapSetCover Child = beatmapCover = new TopSearchBeatmapSetCover
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0, Alpha = 0,
@ -184,5 +184,10 @@ namespace osu.Game.Overlays.BeatmapListing
return true; return true;
} }
} }
private class TopSearchBeatmapSetCover : UpdateableBeatmapSetCover
{
protected override bool TransformImmediately => true;
}
} }
} }

View File

@ -24,6 +24,8 @@ using osu.Game.Overlays.Chat.Tabs;
using osuTK.Input; using osuTK.Input;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Online; using osu.Game.Online;
namespace osu.Game.Overlays namespace osu.Game.Overlays
@ -31,8 +33,8 @@ namespace osu.Game.Overlays
public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent
{ {
public string IconTexture => "Icons/Hexacons/messaging"; public string IconTexture => "Icons/Hexacons/messaging";
public string Title => "chat"; public LocalisableString Title => ChatStrings.HeaderTitle;
public string Description => "join the real-time discussion"; public LocalisableString Description => ChatStrings.HeaderDescription;
private const float textbox_height = 60; private const float textbox_height = 60;
private const float channel_selection_min_height = 0.3f; private const float channel_selection_min_height = 0.3f;

View File

@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Dialog
}, },
new PopupDialogCancelButton new PopupDialogCancelButton
{ {
Text = @"Cancel", Text = Localisation.CommonStrings.Cancel,
Action = onCancel Action = onCancel
}, },
}; };

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.API; using osu.Game.Online.API;
using osuTK.Graphics; using osuTK.Graphics;
@ -18,8 +19,8 @@ namespace osu.Game.Overlays
where T : OverlayHeader where T : OverlayHeader
{ {
public virtual string IconTexture => Header.Title.IconTexture ?? string.Empty; public virtual string IconTexture => Header.Title.IconTexture ?? string.Empty;
public virtual string Title => Header.Title.Title ?? string.Empty; public virtual LocalisableString Title => Header.Title.Title;
public virtual string Description => Header.Title.Description ?? string.Empty; public virtual LocalisableString Description => Header.Title.Description;
public T Header { get; } public T Header { get; }

View File

@ -1,14 +1,16 @@
// 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 osu.Framework.Localisation;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public interface INamedOverlayComponent public interface INamedOverlayComponent
{ {
string IconTexture { get; } string IconTexture { get; }
string Title { get; } LocalisableString Title { get; }
string Description { get; } LocalisableString Description { get; }
} }
} }

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -16,6 +17,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Input.Bindings;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
@ -45,12 +47,19 @@ namespace osu.Game.Overlays.KeyBinding
} }
} }
private Container content;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
content.ReceivePositionalInputAt(screenSpacePos);
public bool FilteringActive { get; set; } public bool FilteringActive { get; set; }
private OsuSpriteText text; private OsuSpriteText text;
private FillFlowContainer cancelAndClearButtons; private FillFlowContainer cancelAndClearButtons;
private FillFlowContainer<KeyButton> buttons; private FillFlowContainer<KeyButton> buttons;
private Bindable<bool> isDefault { get; } = new BindableBool(true);
public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString()); public IEnumerable<string> FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString());
public KeyBindingRow(object action, IEnumerable<Framework.Input.Bindings.KeyBinding> bindings) public KeyBindingRow(object action, IEnumerable<Framework.Input.Bindings.KeyBinding> bindings)
@ -60,9 +69,6 @@ namespace osu.Game.Overlays.KeyBinding
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = padding;
} }
[Resolved] [Resolved]
@ -71,51 +77,72 @@ namespace osu.Game.Overlays.KeyBinding
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
EdgeEffect = new EdgeEffectParameters RelativeSizeAxes = Axes.X;
{ AutoSizeAxes = Axes.Y;
Radius = 2, Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS };
Colour = colours.YellowDark.Opacity(0),
Type = EdgeEffectType.Shadow,
Hollow = true,
};
Children = new Drawable[] InternalChildren = new Drawable[]
{ {
new Box new RestoreDefaultValueButton<bool>
{ {
RelativeSizeAxes = Axes.Both, Current = isDefault,
Colour = Color4.Black, Action = RestoreDefaults,
Alpha = 0.6f,
},
text = new OsuSpriteText
{
Text = action.GetDescription(),
Margin = new MarginPadding(padding),
},
buttons = new FillFlowContainer<KeyButton>
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
},
cancelAndClearButtons = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(padding) { Top = height + padding * 2 },
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Alpha = 0, },
Spacing = new Vector2(5), content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Masking = true,
CornerRadius = padding,
EdgeEffect = new EdgeEffectParameters
{
Radius = 2,
Colour = colours.YellowDark.Opacity(0),
Type = EdgeEffectType.Shadow,
Hollow = true,
},
Children = new Drawable[] Children = new Drawable[]
{ {
new CancelButton { Action = finalise }, new Box
new ClearButton { Action = clear }, {
}, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.6f,
},
text = new OsuSpriteText
{
Text = action.GetDescription(),
Margin = new MarginPadding(padding),
},
buttons = new FillFlowContainer<KeyButton>
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
},
cancelAndClearButtons = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(padding) { Top = height + padding * 2 },
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Alpha = 0,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new CancelButton { Action = finalise },
new ClearButton { Action = clear },
},
}
}
} }
}; };
foreach (var b in bindings) foreach (var b in bindings)
buttons.Add(new KeyButton(b)); buttons.Add(new KeyButton(b));
updateIsDefaultValue();
} }
public void RestoreDefaults() public void RestoreDefaults()
@ -128,18 +155,20 @@ namespace osu.Game.Overlays.KeyBinding
button.UpdateKeyCombination(d); button.UpdateKeyCombination(d);
store.Update(button.KeyBinding); store.Update(button.KeyBinding);
} }
isDefault.Value = true;
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
FadeEdgeEffectTo(1, transition_time, Easing.OutQuint); content.FadeEdgeEffectTo(1, transition_time, Easing.OutQuint);
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
FadeEdgeEffectTo(0, transition_time, Easing.OutQuint); content.FadeEdgeEffectTo(0, transition_time, Easing.OutQuint);
base.OnHoverLost(e); base.OnHoverLost(e);
} }
@ -287,6 +316,8 @@ namespace osu.Game.Overlays.KeyBinding
{ {
store.Update(bindTarget.KeyBinding); store.Update(bindTarget.KeyBinding);
updateIsDefaultValue();
bindTarget.IsBinding = false; bindTarget.IsBinding = false;
Schedule(() => Schedule(() =>
{ {
@ -304,8 +335,8 @@ namespace osu.Game.Overlays.KeyBinding
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)
{ {
AutoSizeDuration = 500; content.AutoSizeDuration = 500;
AutoSizeEasing = Easing.OutQuint; content.AutoSizeEasing = Easing.OutQuint;
cancelAndClearButtons.FadeIn(300, Easing.OutQuint); cancelAndClearButtons.FadeIn(300, Easing.OutQuint);
cancelAndClearButtons.BypassAutoSizeAxes &= ~Axes.Y; cancelAndClearButtons.BypassAutoSizeAxes &= ~Axes.Y;
@ -330,6 +361,11 @@ namespace osu.Game.Overlays.KeyBinding
if (bindTarget != null) bindTarget.IsBinding = true; if (bindTarget != null) bindTarget.IsBinding = true;
} }
private void updateIsDefaultValue()
{
isDefault.Value = bindings.Select(b => b.KeyCombination).SequenceEqual(Defaults);
}
private class CancelButton : TriangleButton private class CancelButton : TriangleButton
{ {
public CancelButton() public CancelButton()
@ -378,9 +414,6 @@ namespace osu.Game.Overlays.KeyBinding
Margin = new MarginPadding(padding); Margin = new MarginPadding(padding);
// todo: use this in a meaningful way
// var isDefault = keyBinding.Action is Enum;
Masking = true; Masking = true;
CornerRadius = padding; CornerRadius = padding;
@ -445,6 +478,9 @@ namespace osu.Game.Overlays.KeyBinding
public void UpdateKeyCombination(KeyCombination newCombination) public void UpdateKeyCombination(KeyCombination newCombination)
{ {
if ((KeyBinding as DatabasedKeyBinding)?.RulesetID != null && !KeyBindingStore.CheckValidForGameplay(newCombination))
return;
KeyBinding.KeyCombination = newCombination; KeyBinding.KeyCombination = newCombination;
Text.Text = KeyBinding.KeyCombination.ReadableString(); Text.Text = KeyBinding.KeyCombination.ReadableString();
} }

View File

@ -61,8 +61,11 @@ namespace osu.Game.Overlays.KeyBinding
{ {
Text = "Reset all bindings in section"; Text = "Reset all bindings in section";
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Margin = new MarginPadding { Top = 5 }; Width = 0.5f;
Height = 20; Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
Margin = new MarginPadding { Top = 15 };
Height = 30;
Content.CornerRadius = 5; Content.CornerRadius = 5;
} }

View File

@ -2,14 +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; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
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.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
@ -20,26 +19,16 @@ namespace osu.Game.Overlays.News.Displays
/// </summary> /// </summary>
public class ArticleListing : CompositeDrawable public class ArticleListing : CompositeDrawable
{ {
public Action<APINewsSidebar> SidebarMetadataUpdated; private readonly Action fetchMorePosts;
[Resolved]
private IAPIProvider api { get; set; }
private FillFlowContainer content; private FillFlowContainer content;
private ShowMoreButton showMore; private ShowMoreButton showMore;
private GetNewsRequest request; private CancellationTokenSource cancellationToken;
private Cursor lastCursor;
private readonly int? year; public ArticleListing(Action fetchMorePosts)
/// <summary>
/// Instantiate a listing for the specified year.
/// </summary>
/// <param name="year">The year to load articles from. If null, will show the most recent articles.</param>
public ArticleListing(int? year = null)
{ {
this.year = year; this.fetchMorePosts = fetchMorePosts;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -47,6 +36,7 @@ namespace osu.Game.Overlays.News.Displays
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Padding = new MarginPadding Padding = new MarginPadding
{ {
Vertical = 20, Vertical = 20,
@ -75,53 +65,25 @@ namespace osu.Game.Overlays.News.Displays
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Margin = new MarginPadding Margin = new MarginPadding { Top = 15 },
{ Action = fetchMorePosts,
Top = 15
},
Action = performFetch,
Alpha = 0 Alpha = 0
} }
} }
}; };
performFetch();
} }
private void performFetch() public void AddPosts(IEnumerable<APINewsPost> posts, bool morePostsAvailable) => Schedule(() =>
{ LoadComponentsAsync(posts.Select(p => new NewsCard(p)).ToList(), loaded =>
request?.Cancel();
request = new GetNewsRequest(year, lastCursor);
request.Success += response => Schedule(() => onSuccess(response));
api.PerformAsync(request);
}
private CancellationTokenSource cancellationToken;
private void onSuccess(GetNewsResponse response)
{
cancellationToken?.Cancel();
// only needs to be updated on the initial load, as the content won't change during pagination.
if (lastCursor == null)
SidebarMetadataUpdated?.Invoke(response.SidebarMetadata);
// store cursor for next pagination request.
lastCursor = response.Cursor;
LoadComponentsAsync(response.NewsPosts.Select(p => new NewsCard(p)).ToList(), loaded =>
{ {
content.AddRange(loaded); content.AddRange(loaded);
showMore.IsLoading = false; showMore.IsLoading = false;
showMore.Alpha = response.Cursor != null ? 1 : 0; showMore.Alpha = morePostsAvailable ? 1 : 0;
}, (cancellationToken = new CancellationTokenSource()).Token); }, (cancellationToken = new CancellationTokenSource()).Token)
} );
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
request?.Cancel();
cancellationToken?.Cancel(); cancellationToken?.Cancel();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }

View File

@ -6,6 +6,7 @@ using System.Threading;
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.Game.Online.API.Requests;
using osu.Game.Overlays.News; using osu.Game.Overlays.News;
using osu.Game.Overlays.News.Displays; using osu.Game.Overlays.News.Displays;
using osu.Game.Overlays.News.Sidebar; using osu.Game.Overlays.News.Sidebar;
@ -14,13 +15,21 @@ namespace osu.Game.Overlays
{ {
public class NewsOverlay : OnlineOverlay<NewsHeader> public class NewsOverlay : OnlineOverlay<NewsHeader>
{ {
private readonly Bindable<string> article = new Bindable<string>(null); private readonly Bindable<string> article = new Bindable<string>();
private readonly Container sidebarContainer; private readonly Container sidebarContainer;
private readonly NewsSidebar sidebar; private readonly NewsSidebar sidebar;
private readonly Container content; private readonly Container content;
private GetNewsRequest request;
private Cursor lastCursor;
/// <summary>
/// The year currently being displayed. If null, the main listing is being displayed.
/// </summary>
private int? displayedYear;
private CancellationTokenSource cancellationToken; private CancellationTokenSource cancellationToken;
private bool displayUpdateRequired = true; private bool displayUpdateRequired = true;
@ -65,7 +74,13 @@ namespace osu.Game.Overlays
base.LoadComplete(); base.LoadComplete();
// should not be run until first pop-in to avoid requesting data before user views. // should not be run until first pop-in to avoid requesting data before user views.
article.BindValueChanged(onArticleChanged); article.BindValueChanged(a =>
{
if (a.NewValue == null)
loadListing();
else
loadArticle(a.NewValue);
});
} }
protected override NewsHeader CreateHeader() => new NewsHeader { ShowFrontPage = ShowFrontPage }; protected override NewsHeader CreateHeader() => new NewsHeader { ShowFrontPage = ShowFrontPage };
@ -95,7 +110,7 @@ namespace osu.Game.Overlays
public void ShowYear(int year) public void ShowYear(int year)
{ {
loadFrontPage(year); loadListing(year);
Show(); Show();
} }
@ -108,7 +123,11 @@ namespace osu.Game.Overlays
protected void LoadDisplay(Drawable display) protected void LoadDisplay(Drawable display)
{ {
ScrollFlow.ScrollToStart(); ScrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded => content.Child = loaded, (cancellationToken = new CancellationTokenSource()).Token); LoadComponentAsync(display, loaded =>
{
content.Child = loaded;
Loading.Hide();
}, (cancellationToken = new CancellationTokenSource()).Token);
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
@ -118,48 +137,65 @@ namespace osu.Game.Overlays
sidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0)); sidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0));
} }
private void onArticleChanged(ValueChangedEvent<string> article) private void loadListing(int? year = null)
{ {
if (article.NewValue == null)
loadFrontPage();
else
loadArticle(article.NewValue);
}
private void loadFrontPage(int? year = null)
{
beginLoading();
Header.SetFrontPage(); Header.SetFrontPage();
var page = new ArticleListing(year); displayedYear = year;
page.SidebarMetadataUpdated += metadata => Schedule(() => lastCursor = null;
beginLoading(true);
request = new GetNewsRequest(displayedYear);
request.Success += response => Schedule(() =>
{ {
sidebar.Metadata.Value = metadata; lastCursor = response.Cursor;
Loading.Hide(); sidebar.Metadata.Value = response.SidebarMetadata;
var listing = new ArticleListing(getMorePosts);
listing.AddPosts(response.NewsPosts, response.Cursor != null);
LoadDisplay(listing);
}); });
LoadDisplay(page);
API.PerformAsync(request);
}
private void getMorePosts()
{
beginLoading(false);
request = new GetNewsRequest(displayedYear, lastCursor);
request.Success += response => Schedule(() =>
{
lastCursor = response.Cursor;
if (content.Child is ArticleListing listing)
listing.AddPosts(response.NewsPosts, response.Cursor != null);
});
API.PerformAsync(request);
} }
private void loadArticle(string article) private void loadArticle(string article)
{ {
beginLoading(); // This is not yet implemented nor called from anywhere.
beginLoading(true);
Header.SetArticle(article); Header.SetArticle(article);
// Temporary, should be handled by ArticleDisplay later
LoadDisplay(Empty()); LoadDisplay(Empty());
Loading.Hide();
} }
private void beginLoading() private void beginLoading(bool showLoadingOverlay)
{ {
request?.Cancel();
cancellationToken?.Cancel(); cancellationToken?.Cancel();
Loading.Show();
if (showLoadingOverlay)
Loading.Show();
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
request?.Cancel();
cancellationToken?.Cancel(); cancellationToken?.Cancel();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }

View File

@ -11,16 +11,18 @@ using osu.Game.Graphics.Containers;
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent public class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent
{ {
public string IconTexture => "Icons/Hexacons/notification"; public string IconTexture => "Icons/Hexacons/notification";
public string Title => "notifications"; public LocalisableString Title => NotificationsStrings.HeaderTitle;
public string Description => "waiting for 'ya"; public LocalisableString Description => NotificationsStrings.HeaderDescription;
private const float width = 320; private const float width = 320;

View File

@ -19,6 +19,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.Music; using osu.Game.Overlays.Music;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -28,8 +29,8 @@ namespace osu.Game.Overlays
public class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent public class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent
{ {
public string IconTexture => "Icons/Hexacons/music"; public string IconTexture => "Icons/Hexacons/music";
public string Title => "now playing"; public LocalisableString Title => NowPlayingStrings.HeaderTitle;
public string Description => "manage the currently playing track"; public LocalisableString Description => NowPlayingStrings.HeaderDescription;
private const float player_height = 130; private const float player_height = 130;
private const float transition_length = 800; private const float transition_length = 800;

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
@ -19,15 +20,15 @@ namespace osu.Game.Overlays
private readonly OsuSpriteText titleText; private readonly OsuSpriteText titleText;
private readonly Container icon; private readonly Container icon;
private string title; private LocalisableString title;
public string Title public LocalisableString Title
{ {
get => title; get => title;
protected set => titleText.Text = title = value; protected set => titleText.Text = title = value;
} }
public string Description { get; protected set; } public LocalisableString Description { get; protected set; }
private string iconTexture; private string iconTexture;

View File

@ -41,12 +41,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{ {
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
new UpdateableBeatmapSetCover new UpdateableBeatmapSetCover(BeatmapSetCoverType.List)
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = cover_width, Width = cover_width,
BeatmapSet = beatmap.BeatmapSet, BeatmapSet = beatmap.BeatmapSet,
CoverType = BeatmapSetCoverType.List,
}, },
new Container new Container
{ {

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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays
{
public class RestoreDefaultValueButton<T> : OsuButton, IHasTooltip, IHasCurrentValue<T>
{
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
// this is done to ensure a click on this button doesn't trigger focus on a parent element which contains the button.
public override bool AcceptsFocus => true;
public Bindable<T> Current
{
get => current.Current;
set => current.Current = value;
}
private Color4 buttonColour;
private bool hovering;
public RestoreDefaultValueButton()
{
Height = 1;
RelativeSizeAxes = Axes.Y;
Width = SettingsPanel.CONTENT_MARGINS;
}
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
BackgroundColour = colour.Yellow;
buttonColour = colour.Yellow;
Content.Width = 0.33f;
Content.CornerRadius = 3;
Content.EdgeEffect = new EdgeEffectParameters
{
Colour = buttonColour.Opacity(0.1f),
Type = EdgeEffectType.Glow,
Radius = 2,
};
Padding = new MarginPadding { Vertical = 1.5f };
Alpha = 0f;
Action += () =>
{
if (!current.Disabled) current.SetDefault();
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.ValueChanged += _ => UpdateState();
Current.DisabledChanged += _ => UpdateState();
Current.DefaultChanged += _ => UpdateState();
UpdateState();
}
public string TooltipText => "revert to default";
protected override bool OnHover(HoverEvent e)
{
hovering = true;
UpdateState();
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
hovering = false;
UpdateState();
}
public void UpdateState() => Scheduler.AddOnce(updateState);
private void updateState()
{
if (current == null)
return;
this.FadeTo(current.IsDefault ? 0f :
hovering && !current.Disabled ? 1f : 0.65f, 200, Easing.OutQuint);
this.FadeColour(current.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint);
}
}
}

View File

@ -1,27 +1,45 @@
// 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.Configuration; using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.General namespace osu.Game.Overlays.Settings.Sections.General
{ {
public class LanguageSettings : SettingsSubsection public class LanguageSettings : SettingsSubsection
{ {
private SettingsDropdown<Language> languageSelection;
private Bindable<string> frameworkLocale;
protected override string Header => "Language"; protected override string Header => "Language";
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkConfigManager frameworkConfig) private void load(FrameworkConfigManager frameworkConfig)
{ {
frameworkLocale = frameworkConfig.GetBindable<string>(FrameworkSetting.Locale);
Children = new Drawable[] Children = new Drawable[]
{ {
languageSelection = new SettingsEnumDropdown<Language>
{
LabelText = "Language",
},
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = "Prefer metadata in original language", LabelText = "Prefer metadata in original language",
Current = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowUnicode) Current = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowUnicode)
}, },
}; };
if (!Enum.TryParse<Language>(frameworkLocale.Value, out var locale))
locale = Language.en;
languageSelection.Current.Value = locale;
languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToString());
} }
} }
} }

View File

@ -4,6 +4,7 @@
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.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -11,10 +12,10 @@ namespace osu.Game.Overlays.Settings
{ {
public class SettingsHeader : Container public class SettingsHeader : Container
{ {
private readonly string heading; private readonly LocalisableString heading;
private readonly string subheading; private readonly LocalisableString subheading;
public SettingsHeader(string heading, string subheading) public SettingsHeader(LocalisableString heading, LocalisableString subheading)
{ {
this.heading = heading; this.heading = heading;
this.subheading = subheading; this.subheading = subheading;

View File

@ -5,16 +5,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -108,7 +103,7 @@ namespace osu.Game.Overlays.Settings
protected SettingsItem() protected SettingsItem()
{ {
RestoreDefaultValueButton restoreDefaultButton; RestoreDefaultValueButton<T> restoreDefaultButton;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -116,7 +111,7 @@ namespace osu.Game.Overlays.Settings
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
restoreDefaultButton = new RestoreDefaultValueButton(), restoreDefaultButton = new RestoreDefaultValueButton<T>(),
FlowContent = new FillFlowContainer FlowContent = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -137,7 +132,7 @@ namespace osu.Game.Overlays.Settings
controlWithCurrent.Current.DisabledChanged += _ => updateDisabled(); controlWithCurrent.Current.DisabledChanged += _ => updateDisabled();
if (ShowsDefaultIndicator) if (ShowsDefaultIndicator)
restoreDefaultButton.Bindable = controlWithCurrent.Current; restoreDefaultButton.Current = controlWithCurrent.Current;
} }
} }
@ -146,101 +141,5 @@ namespace osu.Game.Overlays.Settings
if (labelText != null) if (labelText != null)
labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1;
} }
protected internal class RestoreDefaultValueButton : Container, IHasTooltip
{
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
private Bindable<T> bindable;
public Bindable<T> Bindable
{
get => bindable;
set
{
bindable = value;
bindable.ValueChanged += _ => UpdateState();
bindable.DisabledChanged += _ => UpdateState();
bindable.DefaultChanged += _ => UpdateState();
UpdateState();
}
}
private Color4 buttonColour;
private bool hovering;
public RestoreDefaultValueButton()
{
RelativeSizeAxes = Axes.Y;
Width = SettingsPanel.CONTENT_MARGINS;
Padding = new MarginPadding { Vertical = 1.5f };
Alpha = 0f;
}
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
buttonColour = colour.Yellow;
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
CornerRadius = 3,
Masking = true,
Colour = buttonColour,
EdgeEffect = new EdgeEffectParameters
{
Colour = buttonColour.Opacity(0.1f),
Type = EdgeEffectType.Glow,
Radius = 2,
},
Width = 0.33f,
Child = new Box { RelativeSizeAxes = Axes.Both },
};
}
protected override void LoadComplete()
{
base.LoadComplete();
UpdateState();
}
public string TooltipText => "revert to default";
protected override bool OnClick(ClickEvent e)
{
if (bindable != null && !bindable.Disabled)
bindable.SetDefault();
return true;
}
protected override bool OnHover(HoverEvent e)
{
hovering = true;
UpdateState();
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
hovering = false;
UpdateState();
}
public void UpdateState() => Scheduler.AddOnce(updateState);
private void updateState()
{
if (bindable == null)
return;
this.FadeTo(bindable.IsDefault ? 0f :
hovering && !bindable.Disabled ? 1f : 0.65f, 200, Easing.OutQuint);
this.FadeColour(bindable.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint);
}
}
} }
} }

View File

@ -10,14 +10,16 @@ using osuTK.Graphics;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class SettingsOverlay : SettingsPanel, INamedOverlayComponent public class SettingsOverlay : SettingsPanel, INamedOverlayComponent
{ {
public string IconTexture => "Icons/Hexacons/settings"; public string IconTexture => "Icons/Hexacons/settings";
public string Title => "settings"; public LocalisableString Title => SettingsStrings.HeaderTitle;
public string Description => "change the way osu! behaves"; public LocalisableString Description => SettingsStrings.HeaderDescription;
protected override IEnumerable<SettingsSection> CreateSections() => new SettingsSection[] protected override IEnumerable<SettingsSection> CreateSections() => new SettingsSection[]
{ {

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
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;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -49,8 +50,6 @@ namespace osu.Game.Overlays
private readonly bool showSidebar; private readonly bool showSidebar;
protected Box Background;
protected SettingsPanel(bool showSidebar) protected SettingsPanel(bool showSidebar)
{ {
this.showSidebar = showSidebar; this.showSidebar = showSidebar;
@ -63,13 +62,13 @@ namespace osu.Game.Overlays
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
InternalChild = ContentContainer = new Container InternalChild = ContentContainer = new NonMaskedContent
{ {
Width = WIDTH, Width = WIDTH,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Children = new Drawable[] Children = new Drawable[]
{ {
Background = new Box new Box
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
@ -165,7 +164,7 @@ namespace osu.Game.Overlays
{ {
base.PopOut(); base.PopOut();
ContentContainer.MoveToX(-WIDTH, TRANSITION_LENGTH, Easing.OutQuint); ContentContainer.MoveToX(-WIDTH + ExpandedPosition, TRANSITION_LENGTH, Easing.OutQuint);
Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint); Sidebar?.MoveToX(-sidebar_width, TRANSITION_LENGTH, Easing.OutQuint);
this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint); this.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);
@ -191,7 +190,13 @@ namespace osu.Game.Overlays
Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 }; Padding = new MarginPadding { Top = GetToolbarHeight?.Invoke() ?? 0 };
} }
protected class SettingsSectionsContainer : SectionsContainer<SettingsSection> private class NonMaskedContent : Container<Drawable>
{
// masking breaks the pan-out transform with nested sub-settings panels.
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
}
public class SettingsSectionsContainer : SectionsContainer<SettingsSection>
{ {
public SearchContainer<SettingsSection> SearchContainer; public SearchContainer<SettingsSection> SearchContainer;

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Game.Graphics.Containers.Markdown;
namespace osu.Game.Overlays.Wiki.Markdown
{
public class WikiMarkdownContainer : OsuMarkdownContainer
{
public string CurrentPath
{
set => DocumentUrl = $"{DocumentUrl}wiki/{value}";
}
protected override void AddMarkdownComponent(IMarkdownObject markdownObject, FillFlowContainer container, int level)
{
switch (markdownObject)
{
case YamlFrontMatterBlock yamlFrontMatterBlock:
container.Add(new WikiNoticeContainer(yamlFrontMatterBlock));
break;
case ParagraphBlock paragraphBlock:
// Check if paragraph only contains an image
if (paragraphBlock.Inline.Count() == 1 && paragraphBlock.Inline.FirstChild is LinkInline { IsImage: true } linkInline)
{
container.Add(new WikiMarkdownImageBlock(linkInline));
return;
}
break;
}
base.AddMarkdownComponent(markdownObject, container, level);
}
public override MarkdownTextFlowContainer CreateTextFlow() => new WikiMarkdownTextFlowContainer();
private class WikiMarkdownTextFlowContainer : OsuMarkdownTextFlowContainer
{
protected override void AddImage(LinkInline linkInline) => AddDrawable(new WikiMarkdownImage(linkInline));
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Markdig.Syntax.Inlines;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Cursor;
namespace osu.Game.Overlays.Wiki.Markdown
{
public class WikiMarkdownImage : MarkdownImage, IHasTooltip
{
public string TooltipText { get; }
public WikiMarkdownImage(LinkInline linkInline)
: base(linkInline.Url)
{
TooltipText = linkInline.Title;
}
protected override ImageContainer CreateImageContainer(string url)
{
// The idea is replace "https://website.url/wiki/{path-to-image}" to "https://website.url/wiki/images/{path-to-image}"
// "/wiki/images/*" is route to fetch wiki image from osu!web server (see: https://github.com/ppy/osu-web/blob/4205eb66a4da86bdee7835045e4bf28c35456e04/routes/web.php#L289)
url = url.Replace("/wiki/", "/wiki/images/");
return base.CreateImageContainer(url);
}
}
}

View File

@ -0,0 +1,49 @@
// 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 Markdig.Syntax.Inlines;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osuTK;
namespace osu.Game.Overlays.Wiki.Markdown
{
public class WikiMarkdownImageBlock : FillFlowContainer
{
[Resolved]
private IMarkdownTextComponent parentTextComponent { get; set; }
private readonly LinkInline linkInline;
public WikiMarkdownImageBlock(LinkInline linkInline)
{
this.linkInline = linkInline;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
Spacing = new Vector2(0, 3);
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new WikiMarkdownImage(linkInline)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
parentTextComponent.CreateSpriteText().With(t =>
{
t.Text = linkInline.Title;
t.Anchor = Anchor.TopCentre;
t.Origin = Anchor.TopCentre;
}),
};
}
}
}

View File

@ -0,0 +1,97 @@
// 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 Markdig.Extensions.Yaml;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Wiki.Markdown
{
public class WikiNoticeContainer : FillFlowContainer
{
private readonly bool isOutdated;
private readonly bool needsCleanup;
public WikiNoticeContainer(YamlFrontMatterBlock yamlFrontMatterBlock)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Direction = FillDirection.Vertical;
foreach (var line in yamlFrontMatterBlock.Lines)
{
switch (line.ToString())
{
case "outdated: true":
isOutdated = true;
break;
case "needs_cleanup: true":
needsCleanup = true;
break;
}
}
}
[BackgroundDependencyLoader]
private void load()
{
// Reference : https://github.com/ppy/osu-web/blob/master/resources/views/wiki/_notice.blade.php and https://github.com/ppy/osu-web/blob/master/resources/lang/en/wiki.php
// TODO : add notice box for fallback translation, legal translation and outdated translation after implement wiki locale in the future.
if (isOutdated)
{
Add(new NoticeBox
{
Text = "The content on this page is incomplete or outdated. If you are able to help out, please consider updating the article!",
});
}
else if (needsCleanup)
{
Add(new NoticeBox
{
Text = "This page does not meet the standards of the osu! wiki and needs to be cleaned up or rewritten. If you are able to help out, please consider updating the article!",
});
}
}
private class NoticeBox : Container
{
[Resolved]
private IMarkdownTextFlowComponent parentFlowComponent { get; set; }
public string Text { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, OsuColour colour)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
MarkdownTextFlowContainer textFlow;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
textFlow = parentFlowComponent.CreateTextFlow().With(t =>
{
t.Colour = colour.Orange1;
t.Padding = new MarginPadding
{
Vertical = 10,
Horizontal = 15,
};
})
};
textFlow.AddText(Text);
}
}
}
}

View File

@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Edit
} }
} }
private void updateReplay() => drawableRuleset.RegenerateAutoplay(); private void updateReplay() => Scheduler.AddOnce(drawableRuleset.RegenerateAutoplay);
private void addHitObject(HitObject hitObject) private void addHitObject(HitObject hitObject)
{ {

View File

@ -172,7 +172,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
base.AddInternal(Samples = new PausableSkinnableSound()); base.AddInternal(Samples = new PausableSkinnableSound());
CurrentSkin = skinSource; CurrentSkin = skinSource;
CurrentSkin.SourceChanged += onSkinSourceChanged; CurrentSkin.SourceChanged += skinSourceChanged;
}
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
skinChanged();
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -305,6 +311,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// Invoked for this <see cref="DrawableHitObject"/> to take on any values from a newly-applied <see cref="HitObject"/>. /// Invoked for this <see cref="DrawableHitObject"/> to take on any values from a newly-applied <see cref="HitObject"/>.
/// This is also fired after any changes which occurred via an <see cref="osu.Game.Rulesets.Objects.HitObject.ApplyDefaults"/> call.
/// </summary> /// </summary>
protected virtual void OnApply() protected virtual void OnApply()
{ {
@ -312,6 +319,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary> /// <summary>
/// Invoked for this <see cref="DrawableHitObject"/> to revert any values previously taken on from the currently-applied <see cref="HitObject"/>. /// Invoked for this <see cref="DrawableHitObject"/> to revert any values previously taken on from the currently-applied <see cref="HitObject"/>.
/// This is also fired after any changes which occurred via an <see cref="osu.Game.Rulesets.Objects.HitObject.ApplyDefaults"/> call.
/// </summary> /// </summary>
protected virtual void OnFree() protected virtual void OnFree()
{ {
@ -495,7 +503,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected ISkinSource CurrentSkin { get; private set; } protected ISkinSource CurrentSkin { get; private set; }
private void onSkinSourceChanged() => Scheduler.AddOnce(() => private void skinSourceChanged() => Scheduler.AddOnce(skinChanged);
private void skinChanged()
{ {
UpdateComboColour(); UpdateComboColour();
@ -503,7 +513,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (IsLoaded) if (IsLoaded)
updateState(State.Value, true); updateState(State.Value, true);
}); }
protected void UpdateComboColour() protected void UpdateComboColour()
{ {
@ -747,7 +757,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (HitObject != null) if (HitObject != null)
HitObject.DefaultsApplied -= onDefaultsApplied; HitObject.DefaultsApplied -= onDefaultsApplied;
CurrentSkin.SourceChanged -= onSkinSourceChanged; CurrentSkin.SourceChanged -= skinSourceChanged;
} }
} }

View File

@ -13,6 +13,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events; using osu.Framework.Input.StateChanges.Events;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -169,6 +170,13 @@ namespace osu.Game.Rulesets.UI
: base(ruleset, variant, unique) : base(ruleset, variant, unique)
{ {
} }
protected override void ReloadMappings()
{
base.ReloadMappings();
KeyBindings = KeyBindings.Where(b => KeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
}
} }
} }

View File

@ -61,6 +61,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
inputManager = GetContainingInputManager(); inputManager = GetContainingInputManager();
Beatmap.HitObjectAdded += hitObjectAdded;
// updates to selected are handled for us by SelectionHandler. // updates to selected are handled for us by SelectionHandler.
NewCombo.BindTo(SelectionHandler.SelectionNewComboState); NewCombo.BindTo(SelectionHandler.SelectionNewComboState);
@ -259,10 +261,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
public virtual HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => null; public virtual HitObjectSelectionBlueprint CreateHitObjectBlueprintFor(HitObject hitObject) => null;
protected override void OnBlueprintAdded(HitObject item) private void hitObjectAdded(HitObject obj)
{ {
base.OnBlueprintAdded(item); // refresh the tool to handle the case of placement completing.
refreshTool(); refreshTool();
// on successful placement, the new combo button should be reset as this is the most common user interaction. // on successful placement, the new combo button should be reset as this is the most common user interaction.

View File

@ -77,7 +77,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
double offset = result.Time.Value - blueprints.First().Item.StartTime; double offset = result.Time.Value - blueprints.First().Item.StartTime;
if (offset != 0) if (offset != 0)
Beatmap.PerformOnSelection(obj => obj.StartTime += offset); {
Beatmap.PerformOnSelection(obj =>
{
obj.StartTime += offset;
Beatmap.Update(obj);
});
}
} }
return true; return true;

View File

@ -125,6 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return; return;
h.Samples.Add(new HitSampleInfo(sampleName)); h.Samples.Add(new HitSampleInfo(sampleName));
EditorBeatmap.Update(h);
}); });
} }
@ -134,7 +135,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param> /// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName) public void RemoveHitSample(string sampleName)
{ {
EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName)); EditorBeatmap.PerformOnSelection(h =>
{
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
EditorBeatmap.Update(h);
});
} }
/// <summary> /// <summary>

View File

@ -276,7 +276,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(selected.First().StartTime); var timingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(selected.First().StartTime);
double adjustment = timingPoint.BeatLength / EditorBeatmap.BeatDivisor * amount; double adjustment = timingPoint.BeatLength / EditorBeatmap.BeatDivisor * amount;
EditorBeatmap.PerformOnSelection(h => h.StartTime += adjustment); EditorBeatmap.PerformOnSelection(h =>
{
h.StartTime += adjustment;
EditorBeatmap.Update(h);
});
} }
} }

View File

@ -19,6 +19,7 @@ using osu.Game.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Screens.Menu namespace osu.Game.Screens.Menu
@ -50,7 +51,7 @@ namespace osu.Game.Screens.Menu
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
public Button(string text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown) public Button(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
{ {
this.sampleName = sampleName; this.sampleName = sampleName;
this.clickAction = clickAction; this.clickAction = clickAction;

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading; using osu.Framework.Threading;
@ -22,6 +23,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
@ -97,8 +99,8 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[] buttonArea.AddRange(new Drawable[]
{ {
new Button(@"settings", string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O), new Button(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
backButton = new Button(@"back", @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH) backButton = new Button(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
{ {
VisibleState = ButtonSystemState.Play, VisibleState = ButtonSystemState.Play,
}, },
@ -121,19 +123,19 @@ namespace osu.Game.Screens.Menu
private LoginOverlay loginOverlay { get; set; } private LoginOverlay loginOverlay { get; set; }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) private void load(AudioManager audio, IdleTracker idleTracker, GameHost host, LocalisationManager strings)
{ {
buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); buttonsPlay.Add(new Button(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); buttonsPlay.Add(new Button(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new Button(@"playlists", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.Add(new Button(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); buttonsTopLevel.Add(new Button(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
buttonsTopLevel.Add(new Button(@"edit", @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); buttonsTopLevel.Add(new Button(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(@"browse", @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D)); buttonsTopLevel.Add(new Button(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
if (host.CanExit) if (host.CanExit)
buttonsTopLevel.Add(new Button(@"exit", string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); buttonsTopLevel.Add(new Button(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsTopLevel); buttonArea.AddRange(buttonsTopLevel);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
InternalChild = judgementsFlow = new JudgementFlow(); InternalChild = judgementsFlow = new JudgementFlow();
} }
protected override void OnNewJudgement(JudgementResult judgement) => judgementsFlow.Push(GetColourForHitResult(HitWindows.ResultFor(judgement.TimeOffset))); protected override void OnNewJudgement(JudgementResult judgement) => judgementsFlow.Push(GetColourForHitResult(judgement.Type));
private class JudgementFlow : FillFlowContainer<HitErrorCircle> private class JudgementFlow : FillFlowContainer<HitErrorCircle>
{ {

View File

@ -196,12 +196,12 @@ namespace osu.Game.Screens.Play
} }
if (lowestTopScreenSpace.HasValue) if (lowestTopScreenSpace.HasValue)
topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(lowestTopScreenSpace.Value).Y; topRightElements.Y = TopScoringElementsHeight = MathHelper.Clamp(ToLocalSpace(lowestTopScreenSpace.Value).Y, 0, DrawHeight - topRightElements.DrawHeight);
else else
topRightElements.Y = 0; topRightElements.Y = 0;
if (highestBottomScreenSpace.HasValue) if (highestBottomScreenSpace.HasValue)
bottomRightElements.Y = BottomScoringElementsHeight = -(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y); bottomRightElements.Y = BottomScoringElementsHeight = -MathHelper.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - bottomRightElements.DrawHeight);
else else
bottomRightElements.Y = 0; bottomRightElements.Y = 0;
} }

View File

@ -36,7 +36,7 @@ namespace osu.Game.Screens.Select.Carousel
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; } private ManageCollectionsDialog manageCollectionsDialog { get; set; }
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.Children ?? Enumerable.Empty<DrawableCarouselItem>(); public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty<DrawableCarouselItem>() : beatmapContainer.AliveChildren;
[CanBeNull] [CanBeNull]
private Container<DrawableCarouselItem> beatmapContainer; private Container<DrawableCarouselItem> beatmapContainer;

View File

@ -59,6 +59,10 @@ namespace osu.Game.Skinning.Editor
// the selection quad is always upright, so use an AABB rect to make mutating the values easier. // the selection quad is always upright, so use an AABB rect to make mutating the values easier.
var selectionRect = getSelectionQuad().AABBFloat; var selectionRect = getSelectionQuad().AABBFloat;
// If the selection has no area we cannot scale it
if (selectionRect.Area == 0)
return false;
// copy to mutate, as we will need to compare to the original later on. // copy to mutate, as we will need to compare to the original later on.
var adjustedRect = selectionRect; var adjustedRect = selectionRect;

View File

@ -338,6 +338,7 @@ namespace osu.Game.Skinning
{ {
var score = container.OfType<LegacyScoreCounter>().FirstOrDefault(); var score = container.OfType<LegacyScoreCounter>().FirstOrDefault();
var accuracy = container.OfType<GameplayAccuracyCounter>().FirstOrDefault(); var accuracy = container.OfType<GameplayAccuracyCounter>().FirstOrDefault();
var combo = container.OfType<LegacyComboCounter>().FirstOrDefault();
if (score != null && accuracy != null) if (score != null && accuracy != null)
{ {
@ -353,9 +354,12 @@ namespace osu.Game.Skinning
hitError.Anchor = Anchor.BottomCentre; hitError.Anchor = Anchor.BottomCentre;
hitError.Origin = Anchor.CentreLeft; hitError.Origin = Anchor.CentreLeft;
hitError.Rotation = -90; hitError.Rotation = -90;
}
if (songProgress != null) if (songProgress != null)
hitError.Y -= SongProgress.MAX_HEIGHT; {
if (hitError != null) hitError.Y -= SongProgress.MAX_HEIGHT;
if (combo != null) combo.Y -= SongProgress.MAX_HEIGHT;
} }
}) })
{ {

View File

@ -57,7 +57,13 @@ namespace osu.Game.Storyboards.Drawables
public DrawableStoryboard(Storyboard storyboard) public DrawableStoryboard(Storyboard storyboard)
{ {
Storyboard = storyboard; Storyboard = storyboard;
Size = new Vector2(640, 480); Size = new Vector2(640, 480);
bool onlyHasVideoElements = Storyboard.Layers.SelectMany(l => l.Elements).Any(e => !(e is StoryboardVideo));
Width = Height * (storyboard.BeatmapInfo.WidescreenStoryboard || onlyHasVideoElements ? 16 / 9f : 4 / 3f);
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;

View File

@ -5,6 +5,7 @@ using System.Threading;
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 osuTK;
namespace osu.Game.Storyboards.Drawables namespace osu.Game.Storyboards.Drawables
{ {
@ -15,6 +16,8 @@ namespace osu.Game.Storyboards.Drawables
public override bool IsPresent => Enabled && base.IsPresent; public override bool IsPresent => Enabled && base.IsPresent;
protected LayerElementContainer ElementContainer { get; }
public DrawableStoryboardLayer(StoryboardLayer layer) public DrawableStoryboardLayer(StoryboardLayer layer)
{ {
Layer = layer; Layer = layer;
@ -24,10 +27,10 @@ namespace osu.Game.Storyboards.Drawables
Enabled = layer.VisibleWhenPassing; Enabled = layer.VisibleWhenPassing;
Masking = layer.Masking; Masking = layer.Masking;
InternalChild = new LayerElementContainer(layer); InternalChild = ElementContainer = new LayerElementContainer(layer);
} }
private class LayerElementContainer : LifetimeManagementContainer protected class LayerElementContainer : LifetimeManagementContainer
{ {
private readonly StoryboardLayer storyboardLayer; private readonly StoryboardLayer storyboardLayer;
@ -35,8 +38,8 @@ namespace osu.Game.Storyboards.Drawables
{ {
storyboardLayer = layer; storyboardLayer = layer;
Width = 640; Size = new Vector2(640, 480);
Height = 480;
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
} }

View File

@ -53,7 +53,7 @@ namespace osu.Game.Storyboards
public Storyboard() public Storyboard()
{ {
layers.Add("Video", new StoryboardLayer("Video", 4, false)); layers.Add("Video", new StoryboardVideoLayer("Video", 4, false));
layers.Add("Background", new StoryboardLayer("Background", 3)); layers.Add("Background", new StoryboardLayer("Background", 3));
layers.Add("Fail", new StoryboardLayer("Fail", 2) { VisibleWhenPassing = false, }); layers.Add("Fail", new StoryboardLayer("Fail", 2) { VisibleWhenPassing = false, });
layers.Add("Pass", new StoryboardLayer("Pass", 1) { VisibleWhenFailing = false, }); layers.Add("Pass", new StoryboardLayer("Pass", 1) { VisibleWhenFailing = false, });
@ -85,12 +85,8 @@ namespace osu.Game.Storyboards
} }
} }
public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) =>
{ new DrawableStoryboard(this);
var drawable = new DrawableStoryboard(this);
drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f);
return drawable;
}
public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore)
{ {

View File

@ -32,7 +32,7 @@ namespace osu.Game.Storyboards
Elements.Add(element); Elements.Add(element);
} }
public DrawableStoryboardLayer CreateDrawable() public virtual DrawableStoryboardLayer CreateDrawable()
=> new DrawableStoryboardLayer(this) { Depth = Depth, Name = Name }; => new DrawableStoryboardLayer(this) { Depth = Depth, Name = Name };
} }
} }

View File

@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Storyboards.Drawables;
using osuTK;
namespace osu.Game.Storyboards
{
public class StoryboardVideoLayer : StoryboardLayer
{
public StoryboardVideoLayer(string name, int depth, bool masking)
: base(name, depth, masking)
{
}
public override DrawableStoryboardLayer CreateDrawable()
=> new DrawableStoryboardVideoLayer(this) { Depth = Depth, Name = Name };
public class DrawableStoryboardVideoLayer : DrawableStoryboardLayer
{
public DrawableStoryboardVideoLayer(StoryboardVideoLayer layer)
: base(layer)
{
// for videos we want to take on the full size of the storyboard container hierarchy
// to allow the video to fill the full available region.
ElementContainer.RelativeSizeAxes = Axes.Both;
ElementContainer.Size = Vector2.One;
}
}
}
}

View File

@ -3,6 +3,7 @@
#nullable enable #nullable enable
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -53,6 +54,8 @@ namespace osu.Game.Tests.Visual.Spectator
}); });
} }
public new void Schedule(Action action) => base.Schedule(action);
/// <summary> /// <summary>
/// Sends frames for an arbitrary user. /// Sends frames for an arbitrary user.
/// </summary> /// </summary>

View File

@ -29,7 +29,11 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" /> <PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.521.0" /> <PackageReference Include="ppy.LocalisationAnalyser" Version="2021.524.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ppy.osu.Framework" Version="2021.524.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
<PackageReference Include="Sentry" Version="3.3.4" /> <PackageReference Include="Sentry" Version="3.3.4" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.521.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.524.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.422.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.521.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.524.0" />
<PackageReference Include="SharpCompress" Version="0.28.2" /> <PackageReference Include="SharpCompress" Version="0.28.2" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -120,6 +120,7 @@
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleTypeMembersOnOneLine/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MultipleTypeMembersOnOneLine/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NestedStringInterpolation/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NestedStringInterpolation/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAccessedField_002EGlobal/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NotAccessedField_002EGlobal/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NotOverriddenInSpecificCulture/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=OutdentIsOffPrevLevel/@EntryIndexedValue">WARNING</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=OutdentIsOffPrevLevel/@EntryIndexedValue">WARNING</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterHidesMember/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterHidesMember/@EntryIndexedValue">HINT</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterOnlyUsedForPreconditionCheck_002EGlobal/@EntryIndexedValue">HINT</s:String> <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ParameterOnlyUsedForPreconditionCheck_002EGlobal/@EntryIndexedValue">HINT</s:String>