1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 09:32:55 +08:00

Merge branch 'master' into fix-ticks-appearing-late

This commit is contained in:
Dan Balasescu 2018-02-01 18:28:42 +09:00 committed by GitHub
commit 39e626f29e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1556 additions and 280 deletions

@ -1 +1 @@
Subproject commit 209021fb491e21625127be5dbf5efb4c409e6f06
Subproject commit 90bf49a2df3dbad5994d922df63e4891c622dbc3

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
if (CheckPosition == null) return;
if (timeOffset > 0)
if (timeOffset >= 0)
AddJudgement(new Judgement { Result = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss });
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
AddNested(getVisualRepresentation?.Invoke(o));
}
protected override bool ProvidesJudgement => false;
protected override void AddNested(DrawableHitObject h)
{
var catchObject = (DrawableCatchHitObject)h;

View File

@ -24,17 +24,13 @@ namespace osu.Game.Rulesets.Catch.Scoring
switch (obj)
{
case JuiceStream stream:
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
foreach (var _ in stream.NestedHitObjects.Cast<CatchHitObject>())
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
break;
case BananaShower shower:
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
foreach (var _ in shower.NestedHitObjects.Cast<CatchHitObject>())
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
break;
case Fruit _:
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });

View File

@ -32,6 +32,10 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Reference Include="JetBrains.Annotations, Version=11.1.0.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="nunit.framework, Version=3.8.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll</HintPath>
<Private>True</Private>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="JetBrains.Annotations" version="11.1.0" targetFramework="net461" />
<package id="NUnit" version="3.8.1" targetFramework="net461" />
<package id="ppy.OpenTK" version="3.0.11" targetFramework="net461" />
</packages>

View File

@ -32,6 +32,10 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Reference Include="JetBrains.Annotations, Version=11.1.0.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="nunit.framework, Version=3.8.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll</HintPath>
<Private>True</Private>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="JetBrains.Annotations" version="11.1.0" targetFramework="net461" />
<package id="NUnit" version="3.8.1" targetFramework="net461" />
<package id="ppy.OpenTK" version="3.0.11" targetFramework="net461" />
</packages>

View File

@ -0,0 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Judgements
{
public class OsuSliderTailJudgement : OsuJudgement
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result) => 0;
}
}

View File

@ -18,14 +18,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach
{
private readonly Slider slider;
public readonly DrawableHitCircle InitialCircle;
private readonly List<Drawable> components = new List<Drawable>();
private readonly Container<DrawableSliderTick> ticks;
private readonly Container<DrawableRepeatPoint> repeatPoints;
public readonly DrawableHitCircle HeadCircle;
public readonly SliderBody Body;
public readonly SliderBall Ball;
@ -34,6 +29,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
slider = s;
DrawableSliderTail tail;
Container<DrawableSliderTick> ticks;
Container<DrawableRepeatPoint> repeatPoints;
Children = new Drawable[]
{
Body = new SliderBody(s)
@ -51,27 +50,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AlwaysPresent = true,
Alpha = 0
},
InitialCircle = new DrawableHitCircle(new HitCircle
{
StartTime = s.StartTime,
Position = s.StackedPosition,
IndexInCurrentCombo = s.IndexInCurrentCombo,
Scale = s.Scale,
ComboColour = s.ComboColour,
Samples = s.Samples,
SampleControlPoint = s.SampleControlPoint,
TimePreempt = s.TimePreempt,
TimeFadein = s.TimeFadein,
HitWindow300 = s.HitWindow300,
HitWindow100 = s.HitWindow100,
HitWindow50 = s.HitWindow50
})
HeadCircle = new DrawableHitCircle(s.HeadCircle),
tail = new DrawableSliderTail(s.TailCircle)
};
components.Add(Body);
components.Add(Ball);
AddNested(InitialCircle);
AddNested(HeadCircle);
AddNested(tail);
components.Add(tail);
foreach (var tick in s.NestedHitObjects.OfType<SliderTick>())
{
@ -81,6 +70,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
};
ticks.Add(drawableTick);
components.Add(drawableTick);
AddNested(drawableTick);
}
@ -115,27 +105,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
currentSpan = span;
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
if (!InitialCircle.Judgements.Any(j => j.IsHit))
InitialCircle.Position = slider.Curve.PositionAt(progress);
if (!HeadCircle.IsHit)
HeadCircle.Position = slider.Curve.PositionAt(progress);
foreach (var c in components.OfType<ISliderProgress>()) c.UpdateProgress(progress, span);
foreach (var c in components.OfType<ITrackSnaking>()) c.UpdateSnakingPosition(slider.Curve.PositionAt(Body.SnakedStart ?? 0), slider.Curve.PositionAt(Body.SnakedEnd ?? 0));
foreach (var t in ticks.Children) t.Tracking = Ball.Tracking;
foreach (var t in components.OfType<IRequireTracking>()) t.Tracking = Ball.Tracking;
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
{
if (!userTriggered && Time.Current >= slider.EndTime)
{
var judgementsCount = ticks.Children.Count + repeatPoints.Children.Count + 1;
var judgementsHit = ticks.Children.Count(t => t.Judgements.Any(j => j.IsHit)) + repeatPoints.Children.Count(t => t.Judgements.Any(j => j.IsHit));
if (InitialCircle.Judgements.Any(j => j.IsHit))
judgementsHit++;
var judgementsCount = NestedHitObjects.Count;
var judgementsHit = NestedHitObjects.Count(h => h.IsHit);
var hitFraction = (double)judgementsHit / judgementsCount;
if (hitFraction == 1 && InitialCircle.Judgements.Any(j => j.Result == HitResult.Great))
if (hitFraction == 1 && HeadCircle.Judgements.Any(j => j.Result == HitResult.Great))
AddJudgement(new OsuJudgement { Result = HitResult.Great });
else if (hitFraction >= 0.5 && InitialCircle.Judgements.Any(j => j.Result >= HitResult.Good))
else if (hitFraction >= 0.5 && HeadCircle.Judgements.Any(j => j.Result >= HitResult.Good))
AddJudgement(new OsuJudgement { Result = HitResult.Good });
else if (hitFraction > 0)
AddJudgement(new OsuJudgement { Result = HitResult.Meh });
@ -167,7 +155,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
public Drawable ProxiedLayer => InitialCircle.ApproachCircle;
public Drawable ProxiedLayer => HeadCircle.ApproachCircle;
public override Vector2 SelectionPoint => ToScreenSpace(Body.Position);
public override Quad SelectionQuad => Body.PathDrawQuad;

View File

@ -0,0 +1,32 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
{
/// <summary>
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
/// </summary>
public override bool DisplayJudgement => false;
public bool Tracking { get; set; }
public DrawableSliderTail(HitCircle hitCircle)
: base(hitCircle)
{
AlwaysPresent = true;
RelativeSizeAxes = Axes.Both;
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
{
if (!userTriggered && timeOffset >= 0)
AddJudgement(new OsuSliderTailJudgement { Result = Tracking ? HitResult.Great : HitResult.Miss });
}
}
}

View File

@ -11,11 +11,11 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderTick : DrawableOsuHitObject
public class DrawableSliderTick : DrawableOsuHitObject, IRequireTracking
{
private const double anim_duration = 150;
public bool Tracking;
public bool Tracking { get; set; }
public override bool DisplayJudgement => false;

View File

@ -0,0 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public interface IRequireTracking
{
/// <summary>
/// Whether the <see cref="DrawableSlider"/> is currently being tracked by the user.
/// </summary>
bool Tracking { get; set; }
}
}

View File

@ -80,6 +80,9 @@ namespace osu.Game.Rulesets.Osu.Objects
public double Velocity;
public double TickDistance;
public HitCircle HeadCircle;
public HitCircle TailCircle;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@ -97,10 +100,37 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.CreateNestedHitObjects();
createSliderEnds();
createTicks();
createRepeatPoints();
}
private void createSliderEnds()
{
HeadCircle = new HitCircle
{
StartTime = StartTime,
Position = StackedPosition,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboColour = ComboColour,
Samples = Samples,
SampleControlPoint = SampleControlPoint
};
TailCircle = new HitCircle
{
StartTime = EndTime,
Position = StackedEndPosition,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboColour = ComboColour,
Samples = Samples,
SampleControlPoint = SampleControlPoint
};
AddNested(HeadCircle);
AddNested(TailCircle);
}
private void createTicks()
{
if (TickDistance == 0) return;

View File

@ -16,6 +16,9 @@ using OpenTK;
using OpenTK.Graphics;
using osu.Game.Rulesets.Mods;
using System.Linq;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
namespace osu.Game.Rulesets.Osu.Tests
@ -144,7 +147,34 @@ namespace osu.Game.Rulesets.Osu.Tests
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(new[] { drawable });
drawable.OnJudgement += onJudgement;
Add(drawable);
}
private float judgementOffsetDirection = 1;
private void onJudgement(DrawableHitObject judgedObject, Judgement judgement)
{
var osuObject = judgedObject as DrawableOsuHitObject;
if (osuObject == null)
return;
OsuSpriteText text;
Add(text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = judgement.IsHit ? "Hit!" : "Miss!",
Colour = judgement.IsHit ? Color4.Green : Color4.Red,
TextSize = 30,
Position = osuObject.HitObject.StackedEndPosition + judgementOffsetDirection * new Vector2(0, 45)
});
text.Delay(150)
.Then().FadeOut(200)
.Then().Expire();
judgementOffsetDirection *= -1;
}
}
}

View File

@ -33,6 +33,10 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Reference Include="JetBrains.Annotations, Version=11.1.0.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="nunit.framework, Version=3.8.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll</HintPath>
<Private>True</Private>
@ -51,6 +55,7 @@
<Compile Include="Edit\OsuEditPlayfield.cs" />
<Compile Include="Edit\OsuEditRulesetContainer.cs" />
<Compile Include="Edit\OsuHitObjectComposer.cs" />
<Compile Include="Judgements\OsuSliderTailJudgement.cs" />
<Compile Include="Mods\OsuModAutopilot.cs" />
<Compile Include="Mods\OsuModAutoplay.cs" />
<Compile Include="Mods\OsuModDaycore.cs" />
@ -71,6 +76,8 @@
<Compile Include="Objects\Drawables\Connections\FollowPointRenderer.cs" />
<Compile Include="Judgements\OsuJudgement.cs" />
<Compile Include="Objects\Drawables\DrawableRepeatPoint.cs" />
<Compile Include="Objects\Drawables\DrawableSliderTail.cs" />
<Compile Include="Objects\Drawables\IRequireTracking.cs" />
<Compile Include="Objects\Drawables\ITrackSnaking.cs" />
<Compile Include="Objects\Drawables\Pieces\ApproachCircle.cs" />
<Compile Include="Objects\Drawables\Pieces\SpinnerBackground.cs" />

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="JetBrains.Annotations" version="11.1.0" targetFramework="net461" />
<package id="NUnit" version="3.8.1" targetFramework="net461" />
<package id="ppy.OpenTK" version="3.0.11" targetFramework="net461" />
</packages>

View File

@ -35,8 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Replays
{
bool hitButton = true;
Frames.Add(new ReplayFrame(-100000, null, null, ReplayButtonState.None));
Frames.Add(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, null, null, ReplayButtonState.None));
Frames.Add(new TaikoReplayFrame(-100000, ReplayButtonState.None));
Frames.Add(new TaikoReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, ReplayButtonState.None));
for (int i = 0; i < Beatmap.HitObjects.Count; i++)
{
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
break;
}
Frames.Add(new ReplayFrame(j, null, null, button));
Frames.Add(new TaikoReplayFrame(j, button));
d = (d + 1) % 4;
if (++count == req)
break;
@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Replays
{
foreach (var tick in drumRoll.NestedHitObjects.OfType<DrumRollTick>())
{
Frames.Add(new ReplayFrame(tick.StartTime, null, null, hitButton ? ReplayButtonState.Right1 : ReplayButtonState.Right2));
Frames.Add(new TaikoReplayFrame(tick.StartTime, hitButton ? ReplayButtonState.Right1 : ReplayButtonState.Right2));
hitButton = !hitButton;
}
}
@ -107,18 +107,18 @@ namespace osu.Game.Rulesets.Taiko.Replays
button = hitButton ? ReplayButtonState.Left1 : ReplayButtonState.Left2;
}
Frames.Add(new ReplayFrame(h.StartTime, null, null, button));
Frames.Add(new TaikoReplayFrame(h.StartTime, button));
}
else
throw new InvalidOperationException("Unknown hit object type.");
Frames.Add(new ReplayFrame(endTime + KEY_UP_DELAY, null, null, ReplayButtonState.None));
Frames.Add(new TaikoReplayFrame(endTime + KEY_UP_DELAY, ReplayButtonState.None));
if (i < Beatmap.HitObjects.Count - 1)
{
double waitTime = Beatmap.HitObjects[i + 1].StartTime - 1000;
if (waitTime > endTime)
Frames.Add(new ReplayFrame(waitTime, null, null, ReplayButtonState.None));
Frames.Add(new TaikoReplayFrame(waitTime, ReplayButtonState.None));
}
hitButton = !hitButton;

View File

@ -0,0 +1,17 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Replays;
namespace osu.Game.Rulesets.Taiko.Replays
{
public class TaikoReplayFrame : ReplayFrame
{
public override bool IsImportant => MouseLeft || MouseRight;
public TaikoReplayFrame(double time, ReplayButtonState buttons)
: base(time, null, null, buttons)
{
}
}
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
/// <summary>
/// Taiko fails at the end of the map if the player has not half-filled their HP bar.
/// </summary>
protected override bool DefaultFailCondition => Hits == MaxHits && Health.Value <= 0.5;
protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value <= 0.5;
private double hpIncreaseTick;
private double hpIncreaseGreat;

View File

@ -32,6 +32,10 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Reference Include="JetBrains.Annotations, Version=11.1.0.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="nunit.framework, Version=3.8.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll</HintPath>
<Private>True</Private>
@ -91,6 +95,7 @@
<Compile Include="Replays\TaikoAutoGenerator.cs" />
<Compile Include="Objects\TaikoHitObject.cs" />
<Compile Include="Objects\TaikoHitObjectDifficulty.cs" />
<Compile Include="Replays\TaikoReplayFrame.cs" />
<Compile Include="TaikoDifficultyCalculator.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Scoring\TaikoScoreProcessor.cs" />

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="JetBrains.Annotations" version="11.1.0" targetFramework="net461" />
<package id="NUnit" version="3.8.1" targetFramework="net461" />
<package id="ppy.OpenTK" version="3.0.11" targetFramework="net461" />
</packages>

View File

@ -0,0 +1,247 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Game.Online.Chat;
namespace osu.Game.Tests.Chat
{
[TestFixture]
public class MessageFormatterTests
{
[Test]
public void TestBareLink()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a http://www.basic-link.com/?test=test." });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("http://www.basic-link.com/?test=test", result.Links[0].Url);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(36, result.Links[0].Length);
}
[Test]
public void TestMultipleComplexLinks()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a http://test.io/link#fragment. (see https://twitter.com). Also, This string should not be altered. http://example.com/" });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(3, result.Links.Count);
Assert.AreEqual("http://test.io/link#fragment", result.Links[0].Url);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(28, result.Links[0].Length);
Assert.AreEqual("https://twitter.com", result.Links[1].Url);
Assert.AreEqual(45, result.Links[1].Index);
Assert.AreEqual(19, result.Links[1].Length);
Assert.AreEqual("http://example.com/", result.Links[2].Url);
Assert.AreEqual(108, result.Links[2].Index);
Assert.AreEqual(19, result.Links[2].Length);
}
[Test]
public void TestAjaxLinks()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "https://twitter.com/#!/hashbanglinks" });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(result.Content, result.Links[0].Url);
Assert.AreEqual(0, result.Links[0].Index);
Assert.AreEqual(36, result.Links[0].Length);
}
[Test]
public void TestUnixHomeLinks()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "http://www.chiark.greenend.org.uk/~sgtatham/putty/" });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(result.Content, result.Links[0].Url);
Assert.AreEqual(0, result.Links[0].Index);
Assert.AreEqual(50, result.Links[0].Length);
}
[Test]
public void TestCaseInsensitiveLinks()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "look: http://puu.sh/7Ggh8xcC6/asf0asd9876.NEF" });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(6, result.Links[0].Index);
Assert.AreEqual(39, result.Links[0].Length);
}
[Test]
public void TestWikiLink()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [[Wiki Link]]." });
Assert.AreEqual("This is a Wiki Link.", result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("https://osu.ppy.sh/wiki/Wiki Link", result.Links[0].Url);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(9, result.Links[0].Length);
}
[Test]
public void TestMultiWikiLink()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [[Wiki Link]] [[Wiki:Link]][[Wiki.Link]]." });
Assert.AreEqual("This is a Wiki Link Wiki:LinkWiki.Link.", result.DisplayContent);
Assert.AreEqual(3, result.Links.Count);
Assert.AreEqual("https://osu.ppy.sh/wiki/Wiki Link", result.Links[0].Url);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(9, result.Links[0].Length);
Assert.AreEqual("https://osu.ppy.sh/wiki/Wiki:Link", result.Links[1].Url);
Assert.AreEqual(20, result.Links[1].Index);
Assert.AreEqual(9, result.Links[1].Length);
Assert.AreEqual("https://osu.ppy.sh/wiki/Wiki.Link", result.Links[2].Url);
Assert.AreEqual(29, result.Links[2].Index);
Assert.AreEqual(9, result.Links[2].Length);
}
[Test]
public void TestOldFormatLink()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a (simple test)[https://osu.ppy.sh] of links." });
Assert.AreEqual("This is a simple test of links.", result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(11, result.Links[0].Length);
}
[Test]
public void TestNewFormatLink()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [https://osu.ppy.sh simple test]." });
Assert.AreEqual("This is a simple test.", result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(11, result.Links[0].Length);
}
[Test]
public void TestMarkdownFormatLink()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [simple test](https://osu.ppy.sh)." });
Assert.AreEqual("This is a simple test.", result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(11, result.Links[0].Length);
}
[Test]
public void TestChannelLink()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is an #english and #japanese." });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(2, result.Links.Count);
Assert.AreEqual("osu://chan/#english", result.Links[0].Url);
Assert.AreEqual("osu://chan/#japanese", result.Links[1].Url);
}
[Test]
public void TestOsuProtocol()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a custom protocol osu://chan/#english." });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("osu://chan/#english", result.Links[0].Url);
Assert.AreEqual(26, result.Links[0].Index);
Assert.AreEqual(19, result.Links[0].Length);
result = MessageFormatter.FormatMessage(new Message { Content = "This is a [custom protocol](osu://chan/#english)." });
Assert.AreEqual("This is a custom protocol.", result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("osu://chan/#english", result.Links[0].Url);
Assert.AreEqual("#english", result.Links[0].Argument);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(15, result.Links[0].Length);
}
[Test]
public void TestOsuMpProtocol()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "Join my multiplayer game osump://12346." });
Assert.AreEqual(result.Content, result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("osump://12346", result.Links[0].Url);
Assert.AreEqual(25, result.Links[0].Index);
Assert.AreEqual(13, result.Links[0].Length);
}
[Test]
public void TestRecursiveBreaking()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [https://osu.ppy.sh [[simple test]]]." });
Assert.AreEqual("This is a [[simple test]].", result.DisplayContent);
Assert.AreEqual(1, result.Links.Count);
Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url);
Assert.AreEqual(10, result.Links[0].Index);
Assert.AreEqual(15, result.Links[0].Length);
}
[Test]
public void TestLinkComplex()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12" });
Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!\0\0\0", result.DisplayContent);
Assert.AreEqual(5, result.Links.Count);
Link f = result.Links.Find(l => l.Url == "https://osu.ppy.sh/wiki/wiki links");
Assert.AreEqual(44, f.Index);
Assert.AreEqual(10, f.Length);
f = result.Links.Find(l => l.Url == "http://www.simple-test.com");
Assert.AreEqual(10, f.Index);
Assert.AreEqual(11, f.Length);
f = result.Links.Find(l => l.Url == "http://google.com");
Assert.AreEqual(97, f.Index);
Assert.AreEqual(4, f.Length);
f = result.Links.Find(l => l.Url == "https://osu.ppy.sh");
Assert.AreEqual(78, f.Index);
Assert.AreEqual(18, f.Length);
f = result.Links.Find(l => l.Url == "\uD83D\uDE12");
Assert.AreEqual(101, f.Index);
Assert.AreEqual(3, f.Length);
}
[Test]
public void TestEmoji()
{
Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" });
Assert.AreEqual("Hello world\0\0\0<--This is an emoji,There are more:\0\0\0\0\0\0,\0\0\0", result.DisplayContent);
Assert.AreEqual(result.Links.Count, 4);
Assert.AreEqual(result.Links[0].Index, 11);
Assert.AreEqual(result.Links[1].Index, 49);
Assert.AreEqual(result.Links[2].Index, 52);
Assert.AreEqual(result.Links[3].Index, 56);
Assert.AreEqual(result.Links[0].Url, "\uD83D\uDE12");
Assert.AreEqual(result.Links[1].Url, "\uD83D\uDE10");
Assert.AreEqual(result.Links[2].Url, "\uD83D\uDE00");
Assert.AreEqual(result.Links[3].Url, "\uD83D\uDE20");
}
}
}

View File

@ -0,0 +1,217 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat;
using osu.Game.Users;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual
{
public class TestCaseChatLink : OsuTestCase
{
private readonly TestChatLineContainer textContainer;
private Color4 linkColour;
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(ChatLine),
typeof(Message),
typeof(LinkFlowContainer),
typeof(DummyEchoMessage),
typeof(LocalEchoMessage),
typeof(MessageFormatter)
};
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent);
public TestCaseChatLink()
{
Add(textContainer = new TestChatLineContainer
{
Padding = new MarginPadding { Left = 20, Right = 20 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
});
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
linkColour = colours.Blue;
dependencies.Cache(new ChatOverlay
{
AvailableChannels =
{
new Channel { Name = "#english" },
new Channel { Name = "#japanese" }
}
});
testLinksGeneral();
testEcho();
}
private void clear() => AddStep("clear messages", textContainer.Clear);
private void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions)
{
int index = textContainer.Count + 1;
var newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index));
textContainer.Add(newLine);
AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount);
AddAssert($"msg #{index} has the right action", hasExpectedActions);
AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic());
AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks);
bool hasExpectedActions()
{
var expectedActionsList = expectedActions.ToList();
if (expectedActionsList.Count != newLine.Message.Links.Count)
return false;
for (int i = 0; i < newLine.Message.Links.Count; i++)
{
var action = newLine.Message.Links[i].Action;
if (action != expectedActions[i]) return false;
}
return true;
}
bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast<OsuSpriteText>().All(sprite => sprite.Font == "Exo2.0-MediumItalic");
bool isShowingLinks()
{
bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour);
Color4 textColour = isAction && hasBackground ? OsuColour.FromHex(newLine.Message.Sender.Colour) : Color4.White;
var linkCompilers = newLine.ContentFlow.Where(d => d is DrawableLinkCompiler).ToList();
var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts);
return linkSprites.All(d => d.Colour == linkColour)
&& newLine.ContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour);
}
}
private void testLinksGeneral()
{
addMessageWithChecks("test!");
addMessageWithChecks("osu.ppy.sh!");
addMessageWithChecks("https://osu.ppy.sh!", 1, expectedActions: LinkAction.External);
addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp);
addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.External);
addMessageWithChecks("(osu forums)[https://osu.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[https://osu.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[osu forums](https://osu.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External);
addMessageWithChecks("[https://osu.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External);
addMessageWithChecks("is now listening to [https://osu.ppy.sh/s/93523 IMAGE -MATERIAL- <Version 0>]", 1, true, expectedActions: LinkAction.OpenBeatmapSet);
addMessageWithChecks("is now playing [https://osu.ppy.sh/b/252238 IMAGE -MATERIAL- <Version 0>]", 1, true, expectedActions: LinkAction.OpenBeatmap);
addMessageWithChecks("Let's (try)[https://osu.ppy.sh/home] [https://osu.ppy.sh/b/252238 multiple links] https://osu.ppy.sh/home", 3,
expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External });
// note that there's 0 links here (they get removed if a channel is not found)
addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present).");
addMessageWithChecks("I am important!", 0, false, true);
addMessageWithChecks("feels important", 0, true, true);
addMessageWithChecks("likes to post this [https://osu.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External);
addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch);
addMessageWithChecks("Join my [#english](osu://chan/#english).", 1, expectedActions: LinkAction.OpenChannel);
addMessageWithChecks("Join my osu://chan/#english.", 1, expectedActions: LinkAction.OpenChannel);
addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel });
addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel);
}
private void testEcho()
{
int echoCounter = 0;
addEchoWithWait("sent!", "received!");
addEchoWithWait("https://osu.ppy.sh/home", null, 500);
addEchoWithWait("[https://osu.ppy.sh/forum let's try multiple words too!]");
addEchoWithWait("(long loading times! clickable while loading?)[https://osu.ppy.sh/home]", null, 5000);
void addEchoWithWait(string text, string completeText = null, double delay = 250)
{
var newLine = new ChatLine(new DummyEchoMessage(text));
AddStep($"send msg #{++echoCounter} after {delay}ms", () =>
{
textContainer.Add(newLine);
Scheduler.AddDelayed(() => newLine.Message = new DummyMessage(completeText ?? text), delay);
});
AddUntilStep(() => textContainer.All(line => line.Message is DummyMessage), $"wait for msg #{echoCounter}");
}
}
private class DummyEchoMessage : LocalEchoMessage
{
public DummyEchoMessage(string text)
{
Content = text;
Timestamp = DateTimeOffset.Now;
Sender = DummyMessage.TEST_SENDER;
}
}
private class DummyMessage : Message
{
private static long messageCounter;
internal static readonly User TEST_SENDER_BACKGROUND = new User
{
Username = @"i-am-important",
Id = 42,
Colour = "#250cc9",
};
internal static readonly User TEST_SENDER = new User
{
Username = @"Somebody",
Id = 1,
};
public new DateTimeOffset Timestamp = DateTimeOffset.Now;
public DummyMessage(string text, bool isAction = false, bool isImportant = false, int number = 0)
: base(messageCounter++)
{
Content = text;
IsAction = isAction;
Sender = new User
{
Username = $"User {number}",
Id = number,
Colour = isImportant ? "#250cc9" : null,
};
}
}
private class TestChatLineContainer : FillFlowContainer<ChatLine>
{
protected override int Compare(Drawable x, Drawable y)
{
var xC = (ChatLine)x;
var yC = (ChatLine)y;
return xC.Message.CompareTo(yC.Message);
}
}
}
}

View File

@ -8,6 +8,8 @@ using OpenTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit.Layers.Selection;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects;
@ -35,9 +37,9 @@ namespace osu.Game.Tests.Visual
new SelectionLayer(playfield)
};
playfield.Add(new DrawableHitCircle(new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f }));
playfield.Add(new DrawableHitCircle(new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }));
playfield.Add(new DrawableSlider(new Slider
var hitCircle1 = new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f };
var hitCircle2 = new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f };
var slider = new Slider
{
ControlPoints = new List<Vector2>
{
@ -48,8 +50,16 @@ namespace osu.Game.Tests.Visual
Position = new Vector2(128, 256),
Velocity = 1,
TickDistance = 100,
Scale = 0.5f
}));
Scale = 0.5f,
};
hitCircle1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
hitCircle2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
playfield.Add(new DrawableHitCircle(hitCircle1));
playfield.Add(new DrawableHitCircle(hitCircle2));
playfield.Add(new DrawableSlider(slider));
}
}
}

View File

@ -4,7 +4,7 @@
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.ReplaySettings;
using osu.Game.Screens.Play.PlayerSettings;
namespace osu.Game.Tests.Visual
{
@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual
{
ExampleContainer container;
Add(new ReplaySettingsOverlay
Add(new PlayerSettingsOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual
Text = @"Button",
}));
AddStep(@"Add checkbox", () => container.Add(new ReplayCheckbox
AddStep(@"Add checkbox", () => container.Add(new PlayerCheckbox
{
LabelText = "Checkbox",
}));
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual
}));
}
private class ExampleContainer : ReplayGroup
private class ExampleContainer : PlayerSettingsGroup
{
protected override string Title => @"example";
}

View File

@ -33,6 +33,10 @@
<Reference Include="DeepEqual, Version=1.6.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>$(SolutionDir)\packages\DeepEqual.1.6.0.0\lib\net40\DeepEqual.dll</HintPath>
</Reference>
<Reference Include="JetBrains.Annotations, Version=11.1.0.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="nunit.framework, Version=3.8.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll</HintPath>
<Private>True</Private>
@ -90,6 +94,7 @@
<Compile Include="Beatmaps\Formats\LegacyStoryboardDecoderTest.cs" />
<Compile Include="Beatmaps\IO\OszArchiveReaderTest.cs" />
<Compile Include="Beatmaps\IO\ImportBeatmapTest.cs" />
<Compile Include="Chat\MessageFormatterTests.cs" />
<Compile Include="Resources\Resource.cs" />
<Compile Include="Beatmaps\Formats\LegacyBeatmapDecoderTest.cs" />
<Compile Include="Visual\TestCaseBeatmapCarousel.cs" />
@ -133,6 +138,7 @@
<Compile Include="Visual\TestCaseOnScreenDisplay.cs" />
<Compile Include="Visual\TestCaseAllPlayers.cs" />
<Compile Include="Visual\TestCaseOsuGame.cs" />
<Compile Include="Visual\TestCaseChatLink.cs" />
<Compile Include="Visual\TestCasePlaybackControl.cs" />
<Compile Include="Visual\TestCasePlaySongSelect.cs" />
<Compile Include="Visual\TestCasePopupDialog.cs" />

View File

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-->
<packages>
<package id="DeepEqual" version="1.6.0.0" targetFramework="net461" />
<package id="NUnit" version="3.8.1" targetFramework="net461" />
<package id="ppy.OpenTK" version="3.0.11" targetFramework="net461" />
<package id="System.ValueTuple" version="4.4.0" targetFramework="net461" />
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-->
<packages>
<package id="DeepEqual" version="1.6.0.0" targetFramework="net461" />
<package id="JetBrains.Annotations" version="11.1.0" targetFramework="net461" />
<package id="NUnit" version="3.8.1" targetFramework="net461" />
<package id="ppy.OpenTK" version="3.0.11" targetFramework="net461" />
<package id="System.ValueTuple" version="4.4.0" targetFramework="net461" />
</packages>

View File

@ -39,6 +39,8 @@ namespace osu.Game.Configuration
};
// Audio
Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
Set(OsuSetting.MenuVoice, true);
Set(OsuSetting.MenuMusic, true);
@ -101,6 +103,7 @@ namespace osu.Game.Configuration
MouseDisableButtons,
MouseDisableWheel,
AudioOffset,
VolumeInactive,
MenuMusic,
MenuVoice,
CursorRotation,

View File

@ -0,0 +1,100 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Online.Chat;
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
namespace osu.Game.Graphics.Containers
{
public class LinkFlowContainer : OsuTextFlowContainer
{
public LinkFlowContainer(Action<SpriteText> defaultCreationParameters = null)
: base(defaultCreationParameters)
{
}
public override bool HandleMouseInput => true;
private OsuGame game;
private Action showNotImplementedError;
[BackgroundDependencyLoader(true)]
private void load(OsuGame game, NotificationOverlay notifications)
{
// will be null in tests
this.game = game;
showNotImplementedError = () => notifications?.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.fa_life_saver,
});
}
public void AddLinks(string text, List<Link> links)
{
if (string.IsNullOrEmpty(text) || links == null)
return;
if (links.Count == 0)
{
AddText(text);
return;
}
int previousLinkEnd = 0;
foreach (var link in links)
{
AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd));
AddLink(text.Substring(link.Index, link.Length), link.Url, link.Action, link.Argument);
previousLinkEnd = link.Index + link.Length;
}
AddText(text.Substring(previousLinkEnd));
}
public void AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null)
{
AddInternal(new DrawableLinkCompiler(AddText(text).ToList())
{
TooltipText = tooltipText ?? (url != text ? url : string.Empty),
Action = () =>
{
switch (linkType)
{
case LinkAction.OpenBeatmap:
// todo: replace this with overlay.ShowBeatmap(id) once an appropriate API call is implemented.
if (int.TryParse(linkArgument, out int beatmapId))
Process.Start($"https://osu.ppy.sh/b/{beatmapId}");
break;
case LinkAction.OpenBeatmapSet:
if (int.TryParse(linkArgument, out int setId))
game?.ShowBeatmapSet(setId);
break;
case LinkAction.OpenChannel:
game?.OpenChannel(linkArgument);
break;
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
showNotImplementedError?.Invoke();
break;
case LinkAction.External:
Process.Start(url);
break;
default:
throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action.");
}
},
});
}
}
}

View File

@ -16,6 +16,8 @@ namespace osu.Game.Graphics.Containers
protected override Container<Drawable> Content => content;
protected virtual HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet);
public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Normal)
{
this.sampleSet = sampleSet;
@ -33,7 +35,7 @@ namespace osu.Game.Graphics.Containers
InternalChildren = new Drawable[]
{
content,
new HoverClickSounds(sampleSet)
CreateHoverClickSounds(sampleSet)
};
}
}

View File

@ -1,8 +1,10 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input;
@ -10,24 +12,34 @@ namespace osu.Game.Graphics.Containers
{
public class OsuHoverContainer : OsuClickableContainer
{
private Color4 hoverColour;
protected Color4 HoverColour;
protected Color4 IdleColour = Color4.White;
protected virtual IEnumerable<Drawable> EffectTargets => new[] { Content };
protected override bool OnHover(InputState state)
{
this.FadeColour(hoverColour, 500, Easing.OutQuint);
EffectTargets.ForEach(d => d.FadeColour(HoverColour, 500, Easing.OutQuint));
return base.OnHover(state);
}
protected override void OnHoverLost(InputState state)
{
this.FadeColour(Color4.White, 500, Easing.OutQuint);
EffectTargets.ForEach(d => d.FadeColour(IdleColour, 500, Easing.OutQuint));
base.OnHoverLost(state);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
hoverColour = colours.Yellow;
HoverColour = colours.Yellow;
}
protected override void LoadComplete()
{
base.LoadComplete();
EffectTargets.ForEach(d => d.FadeColour(IdleColour));
}
}
}

View File

@ -15,7 +15,7 @@ namespace osu.Game.Graphics.UserInterface
{
public class IconButton : OsuClickableContainer
{
private const float button_size = 30;
public const float BUTTON_SIZE = 30;
private Color4? flashColour;
/// <summary>
@ -106,7 +106,7 @@ namespace osu.Game.Graphics.UserInterface
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(button_size),
Size = new Vector2(BUTTON_SIZE),
CornerRadius = 5,
Masking = true,
EdgeEffect = new EdgeEffectParameters

View File

@ -7,6 +7,7 @@ using osu.Framework.Threading;
using OpenTK;
using osu.Framework.Audio;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Game.Input.Bindings;
namespace osu.Game.Graphics.UserInterface.Volume
@ -14,6 +15,7 @@ namespace osu.Game.Graphics.UserInterface.Volume
public class VolumeControl : OverlayContainer
{
private readonly VolumeMeter volumeMeterMaster;
private readonly IconButton muteIcon;
protected override bool BlockPassThroughMouse => false;
@ -34,6 +36,17 @@ namespace osu.Game.Graphics.UserInterface.Volume
Spacing = new Vector2(15, 0),
Children = new Drawable[]
{
new Container
{
Size = new Vector2(IconButton.BUTTON_SIZE),
Child = muteIcon = new IconButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Icon = FontAwesome.fa_volume_up,
Action = () => Adjust(GlobalAction.ToggleMute),
}
},
volumeMeterMaster = new VolumeMeter("Master"),
volumeMeterEffect = new VolumeMeter("Effects"),
volumeMeterMusic = new VolumeMeter("Music")
@ -46,18 +59,10 @@ namespace osu.Game.Graphics.UserInterface.Volume
{
base.LoadComplete();
volumeMeterMaster.Bindable.ValueChanged += volumeChanged;
volumeMeterEffect.Bindable.ValueChanged += volumeChanged;
volumeMeterMusic.Bindable.ValueChanged += volumeChanged;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
volumeMeterMaster.Bindable.ValueChanged -= volumeChanged;
volumeMeterEffect.Bindable.ValueChanged -= volumeChanged;
volumeMeterMusic.Bindable.ValueChanged -= volumeChanged;
volumeMeterMaster.Bindable.ValueChanged += _ => settingChanged();
volumeMeterEffect.Bindable.ValueChanged += _ => settingChanged();
volumeMeterMusic.Bindable.ValueChanged += _ => settingChanged();
muted.ValueChanged += _ => settingChanged();
}
public bool Adjust(GlobalAction action)
@ -76,23 +81,45 @@ namespace osu.Game.Graphics.UserInterface.Volume
else
volumeMeterMaster.Increase();
return true;
case GlobalAction.ToggleMute:
Show();
muted.Toggle();
return true;
}
return false;
}
private void volumeChanged(double newVolume)
private void settingChanged()
{
Show();
schedulePopOut();
}
private readonly BindableDouble muteAdjustment = new BindableDouble();
private readonly BindableBool muted = new BindableBool();
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
volumeMeterMaster.Bindable.BindTo(audio.Volume);
volumeMeterEffect.Bindable.BindTo(audio.VolumeSample);
volumeMeterMusic.Bindable.BindTo(audio.VolumeTrack);
muted.ValueChanged += mute =>
{
if (mute)
{
audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment);
muteIcon.Icon = FontAwesome.fa_volume_off;
}
else
{
audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment);
muteIcon.Icon = FontAwesome.fa_volume_up;
}
};
}
private ScheduledDelegate popOutDelegate;

View File

@ -70,11 +70,8 @@ namespace osu.Game.Graphics.UserInterface.Volume
public double Volume
{
get { return Bindable.Value; }
private set
{
Bindable.Value = value;
}
get => Bindable.Value;
private set => Bindable.Value = value;
}
public void Increase()

View File

@ -14,7 +14,7 @@ namespace osu.Game.Input.Bindings
/// A KeyBindingInputManager with a database backing for custom overrides.
/// </summary>
/// <typeparam name="T">The type of the custom action.</typeparam>
public class DatabasedKeyBindingInputManager<T> : KeyBindingContainer<T>
public class DatabasedKeyBindingContainer<T> : KeyBindingContainer<T>
where T : struct
{
private readonly RulesetInfo ruleset;
@ -31,7 +31,7 @@ namespace osu.Game.Input.Bindings
/// <param name="ruleset">A reference to identify the current <see cref="Ruleset"/>. Used to lookup mappings. Null for global mappings.</param>
/// <param name="variant">An optional variant for the specified <see cref="Ruleset"/>. Used when a ruleset has more than one possible keyboard layouts.</param>
/// <param name="simultaneousMode">Specify how to deal with multiple matches of <see cref="KeyCombination"/>s and <see cref="T"/>s.</param>
public DatabasedKeyBindingInputManager(RulesetInfo ruleset = null, int? variant = null, SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None)
public DatabasedKeyBindingContainer(RulesetInfo ruleset = null, int? variant = null, SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None)
: base(simultaneousMode)
{
this.ruleset = ruleset;

View File

@ -10,11 +10,11 @@ using osu.Framework.Input.Bindings;
namespace osu.Game.Input.Bindings
{
public class GlobalKeyBindingInputManager : DatabasedKeyBindingInputManager<GlobalAction>, IHandleGlobalInput
public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalInput
{
private readonly Drawable handler;
public GlobalKeyBindingInputManager(OsuGameBase game)
public GlobalActionContainer(OsuGameBase game)
{
if (game is IKeyBindingHandler<GlobalAction>)
handler = game;
@ -29,10 +29,11 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Up }, GlobalAction.IncreaseVolume),
new KeyBinding(new[] { InputKey.MouseWheelUp }, GlobalAction.IncreaseVolume),
new KeyBinding(new[] { InputKey.Down }, GlobalAction.DecreaseVolume),
new KeyBinding(new[] { InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume),
new KeyBinding(InputKey.Up, GlobalAction.IncreaseVolume),
new KeyBinding(InputKey.MouseWheelUp, GlobalAction.IncreaseVolume),
new KeyBinding(InputKey.Down, GlobalAction.DecreaseVolume),
new KeyBinding(InputKey.MouseWheelDown, GlobalAction.DecreaseVolume),
new KeyBinding(InputKey.F4, GlobalAction.ToggleMute),
};
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
@ -63,6 +64,8 @@ namespace osu.Game.Input.Bindings
IncreaseVolume,
[Description("Decrease Volume")]
DecreaseVolume,
[Description("Toggle mute")]
ToggleMute,
// In-Game Keybindings
[Description("Skip Cutscene")]

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore.Infrastructure;
using osu.Game.Database;
using osu.Game.Input.Bindings;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20180131154205_AddMuteBinding")]
public partial class AddMuteBinding : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql($"UPDATE KeyBinding SET Action = Action + 1 WHERE RulesetID IS NULL AND Variant IS NULL AND Action >= {(int)GlobalAction.ToggleMute}");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql($"DELETE FROM KeyBinding WHERE RulesetID IS NULL AND Variant IS NULL AND Action = {(int)GlobalAction.ToggleMute}");
migrationBuilder.Sql($"UPDATE KeyBinding SET Action = Action - 1 WHERE RulesetID IS NULL AND Variant IS NULL AND Action > {(int)GlobalAction.ToggleMute}");
}
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics.Cursor;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using OpenTK;
namespace osu.Game.Online.Chat
{
/// <summary>
/// An invisible drawable that brings multiple <see cref="SpriteText"/> pieces together to form a consumable clickable link.
/// </summary>
public class DrawableLinkCompiler : OsuHoverContainer, IHasTooltip
{
/// <summary>
/// Each word part of a chat link (split for word-wrap support).
/// </summary>
public List<SpriteText> Parts;
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Parts.Any(d => d.ReceiveMouseInputAt(screenSpacePos));
protected override HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts);
public DrawableLinkCompiler(IEnumerable<SpriteText> parts)
{
Parts = parts.ToList();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
IdleColour = colours.Blue;
}
protected override IEnumerable<Drawable> EffectTargets => Parts;
public string TooltipText { get; set; }
private class LinkHoverSounds : HoverClickSounds
{
private readonly List<SpriteText> parts;
public LinkHoverSounds(HoverSampleSet sampleSet, List<SpriteText> parts)
: base(sampleSet)
{
this.parts = parts;
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => parts.Any(d => d.ReceiveMouseInputAt(screenSpacePos));
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Newtonsoft.Json;
using osu.Game.Users;
@ -40,6 +41,17 @@ namespace osu.Game.Online.Chat
{
}
/// <summary>
/// The text that is displayed in chat.
/// </summary>
public string DisplayContent { get; set; }
/// <summary>
/// The links found in this message.
/// </summary>
/// <remarks>The <see cref="Link"/>s' <see cref="Link.Index"/> and <see cref="Link.Length"/>s are according to <see cref="DisplayContent"/></remarks>
public List<Link> Links;
public Message(long? id)
{
Id = id;

View File

@ -0,0 +1,263 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace osu.Game.Online.Chat
{
public static class MessageFormatter
{
// [[Performance Points]] -> wiki:Performance Points (https://osu.ppy.sh/wiki/Performance_Points)
private static readonly Regex wiki_regex = new Regex(@"\[\[([^\]]+)\]\]");
// (test)[https://osu.ppy.sh/b/1234] -> test (https://osu.ppy.sh/b/1234)
private static readonly Regex old_link_regex = new Regex(@"\(([^\)]*)\)\[([a-z]+://[^ ]+)\]");
// [https://osu.ppy.sh/b/1234 Beatmap [Hard] (poop)] -> Beatmap [hard] (poop) (https://osu.ppy.sh/b/1234)
private static readonly Regex new_link_regex = new Regex(@"\[([a-z]+://[^ ]+) ([^\[\]]*(((?<open>\[)[^\[\]]*)+((?<close-open>\])[^\[\]]*)+)*(?(open)(?!)))\]");
// [test](https://osu.ppy.sh/b/1234) -> test (https://osu.ppy.sh/b/1234) aka correct markdown format
private static readonly Regex markdown_link_regex = new Regex(@"\[([^\]]*)\]\(([a-z]+://[^ ]+)\)");
// advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used
// This is in the format (<required>, [optional]):
// http[s]://<domain>.<tld>[:port][/path][?query][#fragment]
private static readonly Regex advanced_link_regex = new Regex(
// protocol
@"(?<link>[a-z]*?:\/\/" +
// domain + tld
@"(?<domain>(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z0-9-]*[a-z0-9]" +
// port (optional)
@"(?::\d+)?)" +
// path (optional)
@"(?<path>(?:(?:\/+(?:[a-z0-9$_\.\+!\*\',;:\(\)@&~=-]|%[0-9a-f]{2})*)*" +
// query (optional)
@"(?:\?(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?" +
// fragment (optional)
@"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)",
RegexOptions.IgnoreCase);
// 00:00:000 (1,2,3) - test
private static readonly Regex time_regex = new Regex(@"\d\d:\d\d:\d\d\d? [^-]*");
// #osu
private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)");
// Unicode emojis
private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])");
private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null)
{
int captureOffset = 0;
foreach (Match m in regex.Matches(result.Text, startIndex))
{
var index = m.Index - captureOffset;
var displayText = string.Format(display,
m.Groups[0],
m.Groups.Count > 1 ? m.Groups[1].Value : "",
m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim();
var linkText = string.Format(link,
m.Groups[0],
m.Groups.Count > 1 ? m.Groups[1].Value : "",
m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim();
if (displayText.Length == 0 || linkText.Length == 0) continue;
// Check for encapsulated links
if (result.Links.Find(l => l.Index <= index && l.Index + l.Length >= index + m.Length || index <= l.Index && index + m.Length >= l.Index + l.Length) == null)
{
result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText);
//since we just changed the line display text, offset any already processed links.
result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0);
var details = getLinkDetails(linkText);
result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument));
//adjust the offset for processing the current matches group.
captureOffset += m.Length - displayText.Length;
}
}
}
private static void handleAdvanced(Regex regex, MessageFormatterResult result, int startIndex = 0)
{
foreach (Match m in regex.Matches(result.Text, startIndex))
{
var index = m.Index;
var link = m.Groups["link"].Value;
var indexLength = link.Length;
var details = getLinkDetails(link);
result.Links.Add(new Link(link, index, indexLength, details.Action, details.Argument));
}
}
private static LinkDetails getLinkDetails(string url)
{
var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
args[0] = args[0].TrimEnd(':');
switch (args[0])
{
case "http":
case "https":
// length > 3 since all these links need another argument to work
if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh"))
{
switch (args[2])
{
case "b":
case "beatmaps":
return new LinkDetails(LinkAction.OpenBeatmap, args[3]);
case "s":
case "beatmapsets":
case "d":
return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]);
}
}
return new LinkDetails(LinkAction.External, null);
case "osu":
// every internal link also needs some kind of argument
if (args.Length < 3)
return new LinkDetails(LinkAction.External, null);
LinkAction linkType;
switch (args[1])
{
case "chan":
linkType = LinkAction.OpenChannel;
break;
case "edit":
linkType = LinkAction.OpenEditorTimestamp;
break;
case "b":
linkType = LinkAction.OpenBeatmap;
break;
case "s":
case "dl":
linkType = LinkAction.OpenBeatmapSet;
break;
case "spectate":
linkType = LinkAction.Spectate;
break;
default:
linkType = LinkAction.External;
break;
}
return new LinkDetails(linkType, args[2]);
case "osump":
return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]);
default:
return new LinkDetails(LinkAction.External, null);
}
}
private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3)
{
var result = new MessageFormatterResult(toFormat);
// handle the [link display] format
handleMatches(new_link_regex, "{2}", "{1}", result, startIndex);
// handle the standard markdown []() format
handleMatches(markdown_link_regex, "{1}", "{2}", result, startIndex);
// handle the ()[] link format
handleMatches(old_link_regex, "{1}", "{2}", result, startIndex);
// handle wiki links
handleMatches(wiki_regex, "{1}", "https://osu.ppy.sh/wiki/{1}", result, startIndex);
// handle bare links
handleAdvanced(advanced_link_regex, result, startIndex);
// handle editor times
handleMatches(time_regex, "{0}", "osu://edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp);
// handle channels
handleMatches(channel_regex, "{0}", "osu://chan/{0}", result, startIndex, LinkAction.OpenChannel);
var empty = "";
while (space-- > 0)
empty += "\0";
handleMatches(emoji_regex, empty, "{0}", result, startIndex);
return result;
}
public static Message FormatMessage(Message inputMessage)
{
var result = format(inputMessage.Content);
inputMessage.DisplayContent = result.Text;
// Sometimes, regex matches are not in order
result.Links.Sort();
inputMessage.Links = result.Links;
return inputMessage;
}
public class MessageFormatterResult
{
public List<Link> Links = new List<Link>();
public string Text;
public string OriginalText;
public MessageFormatterResult(string text)
{
OriginalText = Text = text;
}
}
public class LinkDetails
{
public LinkAction Action;
public string Argument;
public LinkDetails(LinkAction action, string argument)
{
Action = action;
Argument = argument;
}
}
}
public enum LinkAction
{
External,
OpenBeatmap,
OpenBeatmapSet,
OpenChannel,
OpenEditorTimestamp,
JoinMultiplayerMatch,
Spectate,
}
public class Link : IComparable<Link>
{
public string Url;
public int Index;
public int Length;
public LinkAction Action;
public string Argument;
public Link(string url, int startIndex, int length, LinkAction action, string argument)
{
Url = url;
Index = startIndex;
Length = length;
Action = action;
Argument = argument;
}
public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1;
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Framework.Configuration;
using osu.Framework.Screens;
using osu.Game.Configuration;
@ -18,6 +19,7 @@ using OpenTK;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Audio;
using osu.Framework.Input.Bindings;
using osu.Framework.Platform;
using osu.Framework.Threading;
@ -27,6 +29,7 @@ using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Screens.Play;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using OpenTK.Graphics;
namespace osu.Game
@ -80,6 +83,9 @@ namespace osu.Game
private SettingsOverlay settings;
// todo: move this to SongSelect once Screen has the ability to unsuspend.
public readonly Bindable<IEnumerable<Mod>> SelectedMods = new Bindable<IEnumerable<Mod>>(new List<Mod>());
public OsuGame(string[] args = null)
{
this.args = args;
@ -111,15 +117,29 @@ namespace osu.Game
Task.Run(() => BeatmapManager.Import(paths.ToArray()));
}
dependencies.CacheAs<OsuGame>(this);
dependencies.CacheAs(this);
configRuleset = LocalConfig.GetBindable<int>(OsuSetting.Ruleset);
Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First();
Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0;
LocalConfig.BindWith(OsuSetting.VolumeInactive, inactiveVolumeAdjust);
}
private ScheduledDelegate scoreLoad;
/// <summary>
/// Open chat to a channel matching the provided name, if present.
/// </summary>
/// <param name="channelName">The name of the channel.</param>
public void OpenChannel(string channelName) => chat.OpenChannel(chat.AvailableChannels.Find(c => c.Name == channelName));
/// <summary>
/// Show a beatmap set as an overlay.
/// </summary>
/// <param name="setId">The set to display.</param>
public void ShowBeatmapSet(int setId) => beatmapSetOverlay.ShowBeatmapSet(setId);
protected void LoadScore(Score s)
{
scoreLoad?.Cancel();
@ -393,6 +413,20 @@ namespace osu.Game
return false;
}
private readonly BindableDouble inactiveVolumeAdjust = new BindableDouble();
protected override void OnDeactivated()
{
base.OnDeactivated();
Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeAdjust);
}
protected override void OnActivated()
{
base.OnActivated();
Audio.RemoveAdjustment(AdjustableProperty.Volume, inactiveVolumeAdjust);
}
public bool OnReleased(GlobalAction action) => false;
private Container mainContent;

View File

@ -95,7 +95,7 @@ namespace osu.Game
dependencies.Cache(new LargeTextureStore(new RawTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures"))));
dependencies.CacheAs<OsuGameBase>(this);
dependencies.CacheAs(this);
dependencies.Cache(LocalConfig);
runMigrations();
@ -212,10 +212,10 @@ namespace osu.Game
{
base.LoadComplete();
GlobalKeyBindingInputManager globalBinding;
GlobalActionContainer globalBinding;
CursorOverrideContainer = new CursorOverrideContainer { RelativeSizeAxes = Axes.Both };
CursorOverrideContainer.Child = globalBinding = new GlobalKeyBindingInputManager(this)
CursorOverrideContainer.Child = globalBinding = new GlobalActionContainer(this)
{
RelativeSizeAxes = Axes.Both,
Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both }

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation;
@ -82,16 +83,18 @@ namespace osu.Game.Overlays.Chat
private Message message;
private OsuSpriteText username;
private OsuTextFlowContainer contentFlow;
private LinkFlowContainer contentFlow;
public LinkFlowContainer ContentFlow => contentFlow;
public Message Message
{
get { return message; }
get => message;
set
{
if (message == value) return;
message = value;
message = MessageFormatter.FormatMessage(value);
if (!IsLoaded)
return;
@ -101,8 +104,9 @@ namespace osu.Game.Overlays.Chat
}
[BackgroundDependencyLoader(true)]
private void load(OsuColour colours)
private void load(OsuColour colours, ChatOverlay chat)
{
this.chat = chat;
customUsernameColour = colours.ChatBlue;
}
@ -187,7 +191,18 @@ namespace osu.Game.Overlays.Chat
Padding = new MarginPadding { Left = message_padding + padding },
Children = new Drawable[]
{
contentFlow = new OsuTextFlowContainer(t => { t.TextSize = text_size; })
contentFlow = new LinkFlowContainer(t =>
{
if (Message.IsAction)
{
t.Font = @"Exo2.0-MediumItalic";
if (senderHasBackground)
t.Colour = OsuColour.FromHex(message.Sender.Colour);
}
t.TextSize = text_size;
})
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
@ -195,30 +210,26 @@ namespace osu.Game.Overlays.Chat
}
}
};
if (message.IsAction && senderHasBackground)
contentFlow.Colour = OsuColour.FromHex(message.Sender.Colour);
updateMessageContent();
FinishTransforms(true);
}
private ChatOverlay chat;
private void updateMessageContent()
{
this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint);
timestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint);
timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}";
username.Text = $@"{message.Sender.Username}" + (senderHasBackground ? "" : ":");
username.Text = $@"{message.Sender.Username}" + (senderHasBackground || message.IsAction ? "" : ":");
if (message.IsAction)
{
contentFlow.Clear();
contentFlow.AddText("[", sprite => sprite.Padding = new MarginPadding { Right = action_padding });
contentFlow.AddText(message.Content, sprite => sprite.Font = @"Exo2.0-MediumItalic");
contentFlow.AddText("]", sprite => sprite.Padding = new MarginPadding { Left = action_padding });
}
else
contentFlow.Text = message.Content;
// remove non-existent channels from the link list
message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chat?.AvailableChannels.Any(c => c.Name == link.Argument) != true);
contentFlow.Clear();
contentFlow.AddLinks(message.DisplayContent, message.Links);
}
private class MessageSender : OsuClickableContainer, IHasContextMenu

View File

@ -60,6 +60,7 @@ namespace osu.Game.Overlays
public Bindable<double> ChatHeight { get; set; }
public List<Channel> AvailableChannels { get; private set; } = new List<Channel>();
private readonly Container channelSelectionContainer;
private readonly ChannelSelectionOverlay channelSelection;
@ -190,6 +191,8 @@ namespace osu.Game.Overlays
private double startDragChatHeight;
private bool isDragging;
public void OpenChannel(Channel channel) => addChannel(channel);
protected override bool OnDragStart(InputState state)
{
isDragging = tabsArea.IsHovered;
@ -298,6 +301,8 @@ namespace osu.Game.Overlays
ListChannelsRequest req = new ListChannelsRequest();
req.Success += delegate (List<Channel> channels)
{
AvailableChannels = channels;
Scheduler.Add(delegate
{
addChannel(channels.Find(c => c.Name == @"#lazer"));

View File

@ -12,7 +12,7 @@ namespace osu.Game.Overlays.KeyBinding
public override FontAwesome Icon => FontAwesome.fa_osu_hot;
public override string Header => "Global";
public GlobalKeyBindingsSection(GlobalKeyBindingInputManager manager)
public GlobalKeyBindingsSection(GlobalActionContainer manager)
{
Add(new DefaultBindingsSubsection(manager));
Add(new InGameKeyBindingsSubsection(manager));
@ -23,7 +23,7 @@ namespace osu.Game.Overlays.KeyBinding
{
protected override string Header => string.Empty;
public DefaultBindingsSubsection(GlobalKeyBindingInputManager manager)
public DefaultBindingsSubsection(GlobalActionContainer manager)
: base(null)
{
Defaults = manager.GlobalKeyBindings;
@ -34,7 +34,7 @@ namespace osu.Game.Overlays.KeyBinding
{
protected override string Header => "In Game";
public InGameKeyBindingsSubsection(GlobalKeyBindingInputManager manager) : base(null)
public InGameKeyBindingsSubsection(GlobalActionContainer manager) : base(null)
{
Defaults = manager.InGameKeyBindings;
}

View File

@ -15,7 +15,7 @@ namespace osu.Game.Overlays
protected override Drawable CreateHeader() => new SettingsHeader("key configuration", "Customise your keys!");
[BackgroundDependencyLoader(permitNulls: true)]
private void load(RulesetStore rulesets, GlobalKeyBindingInputManager global)
private void load(RulesetStore rulesets, GlobalActionContainer global)
{
AddSection(new GlobalKeyBindingsSection(global));

View File

@ -188,17 +188,19 @@ namespace osu.Game.Overlays.Mods
start = Mods.Length - 1;
for (int i = start; i < Mods.Length && i >= 0; i += direction)
{
if (Mods[i].HasImplementation)
{
changeSelectedIndex(i);
return;
}
}
if (SelectAt(i)) return;
Deselect();
}
public bool SelectAt(int index)
{
if (!Mods[index].HasImplementation) return false;
changeSelectedIndex(index);
return true;
}
public void Deselect() => changeSelectedIndex(-1);
private void displayMod(Mod mod)

View File

@ -113,6 +113,23 @@ namespace osu.Game.Overlays.Mods
}
}
/// <summary>
/// Select one or more mods in this section.
/// </summary>
/// <param name="mods">The types of <see cref="Mod"/>s which should be deselected.</param>
public void SelectTypes(IEnumerable<Mod> mods)
{
foreach (var button in buttons)
{
for (int i = 0; i < button.Mods.Length; i++)
{
foreach (var mod in mods)
if (mod.GetType().IsInstanceOfType(button.Mods[i]))
button.SelectAt(i);
}
}
}
protected ModSection()
{
AutoSizeAxes = Axes.Y;

View File

@ -51,6 +51,8 @@ namespace osu.Game.Overlays.Mods
[BackgroundDependencyLoader(permitNulls: true)]
private void load(OsuColour colours, OsuGame osu, RulesetStore rulesets)
{
SelectedMods.ValueChanged += selectedModsChanged;
LowMultiplierColour = colours.Red;
HighMultiplierColour = colours.Green;
@ -63,6 +65,37 @@ namespace osu.Game.Overlays.Mods
Ruleset.TriggerChange();
}
private void selectedModsChanged(IEnumerable<Mod> obj)
{
foreach (ModSection section in ModSectionsContainer.Children)
section.SelectTypes(obj);
updateMods();
}
private void updateMods()
{
double multiplier = 1.0;
bool ranked = true;
foreach (Mod mod in SelectedMods.Value)
{
multiplier *= mod.ScoreMultiplier;
ranked &= mod.Ranked;
}
MultiplierLabel.Text = $"{multiplier:N2}x";
if (!ranked)
MultiplierLabel.Text += " (Unranked)";
if (multiplier > 1.0)
MultiplierLabel.FadeColour(HighMultiplierColour, 200);
else if (multiplier < 1.0)
MultiplierLabel.FadeColour(LowMultiplierColour, 200);
else
MultiplierLabel.FadeColour(Color4.White, 200);
}
protected override void PopOut()
{
base.PopOut();
@ -97,6 +130,7 @@ namespace osu.Game.Overlays.Mods
{
foreach (ModSection section in ModSectionsContainer.Children)
section.DeselectAll();
refreshSelectedMods();
}
@ -119,30 +153,7 @@ namespace osu.Game.Overlays.Mods
refreshSelectedMods();
}
private void refreshSelectedMods()
{
SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray();
double multiplier = 1.0;
bool ranked = true;
foreach (Mod mod in SelectedMods.Value)
{
multiplier *= mod.ScoreMultiplier;
ranked &= mod.Ranked;
}
MultiplierLabel.Text = $"{multiplier:N2}x";
if (!ranked)
MultiplierLabel.Text += " (Unranked)";
if (multiplier > 1.0)
MultiplierLabel.FadeColour(HighMultiplierColour, 200);
else if (multiplier < 1.0)
MultiplierLabel.FadeColour(LowMultiplierColour, 200);
else
MultiplierLabel.FadeColour(Color4.White, 200);
}
private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray();
public ModSelectOverlay()
{

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Diagnostics;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation;
@ -9,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
@ -16,9 +18,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Users;
using System.Diagnostics;
using System.Collections.Generic;
using osu.Framework.Graphics.Cursor;
namespace osu.Game.Overlays.Profile
{
@ -103,7 +102,7 @@ namespace osu.Game.Overlays.Profile
Y = -75,
Size = new Vector2(25, 25)
},
new LinkFlowContainer.ProfileLink(user)
new ProfileLink(user)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
@ -329,12 +328,14 @@ namespace osu.Game.Overlays.Profile
{
infoTextLeft.AddText($"{user.Age} years old ", boldItalic);
}
if (user.Country != null)
{
infoTextLeft.AddText("from ");
infoTextLeft.AddText(user.Country.FullName, boldItalic);
countryFlag.Country = user.Country;
}
infoTextLeft.NewParagraph();
if (user.JoinDate.ToUniversalTime().Year < 2008)
@ -346,6 +347,7 @@ namespace osu.Game.Overlays.Profile
infoTextLeft.AddText("Joined ");
infoTextLeft.AddText(user.JoinDate.LocalDateTime.ToShortDateString(), boldItalic);
}
infoTextLeft.NewLine();
infoTextLeft.AddText("Last seen ");
infoTextLeft.AddText(user.LastVisit.LocalDateTime.ToShortDateString(), boldItalic);
@ -434,6 +436,28 @@ namespace osu.Game.Overlays.Profile
infoTextRight.NewLine();
}
private class ProfileLink : OsuHoverContainer, IHasTooltip
{
public string TooltipText => "View Profile in Browser";
public override bool HandleMouseInput => true;
public ProfileLink(User user)
{
Action = () => Process.Start($@"https://osu.ppy.sh/users/{user.Id}");
AutoSizeAxes = Axes.Both;
Child = new OsuSpriteText
{
Text = user.Username,
Font = @"Exo2.0-RegularItalic",
TextSize = 30,
};
}
}
private class GradeBadge : Container
{
private const float width = 50;
@ -471,61 +495,5 @@ namespace osu.Game.Overlays.Profile
badge.Texture = textures.Get($"Grades/{grade}");
}
}
private class LinkFlowContainer : OsuTextFlowContainer
{
public override bool HandleKeyboardInput => true;
public override bool HandleMouseInput => true;
public LinkFlowContainer(Action<SpriteText> defaultCreationParameters = null) : base(defaultCreationParameters)
{
}
protected override SpriteText CreateSpriteText() => new LinkText();
public void AddLink(string text, string url) => AddText(text, link => ((LinkText)link).Url = url);
public class LinkText : OsuSpriteText
{
private readonly OsuHoverContainer content;
public override bool HandleKeyboardInput => content.Action != null;
public override bool HandleMouseInput => content.Action != null;
protected override Container<Drawable> Content => content ?? (Container<Drawable>)this;
protected override IEnumerable<Drawable> FlowingChildren => Children;
public string Url
{
set
{
if (value != null)
content.Action = () => Process.Start(value);
}
}
public LinkText()
{
AddInternal(content = new OsuHoverContainer
{
AutoSizeAxes = Axes.Both,
});
}
}
public class ProfileLink : LinkText, IHasTooltip
{
public string TooltipText => "View Profile in Browser";
public ProfileLink(User user)
{
Text = user.Username;
Url = $@"https://osu.ppy.sh/users/{user.Id}";
Font = @"Exo2.0-RegularItalic";
TextSize = 30;
}
}
}
}
}

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Game.Configuration;
namespace osu.Game.Overlays.Settings.Sections.Audio
{
@ -12,11 +13,12 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
protected override string Header => "Volume";
[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load(AudioManager audio, OsuConfigManager config)
{
Children = new Drawable[]
{
new SettingsSlider<double> { LabelText = "Master", Bindable = audio.Volume, KeyboardStep = 0.1f },
new SettingsSlider<double> { LabelText = "Master (Window Inactive)", Bindable = config.GetBindable<double>(OsuSetting.VolumeInactive), KeyboardStep = 0.1f },
new SettingsSlider<double> { LabelText = "Effect", Bindable = audio.VolumeSample, KeyboardStep = 0.1f },
new SettingsSlider<double> { LabelText = "Music", Bindable = audio.VolumeTrack, KeyboardStep = 0.1f },
};

View File

@ -2,11 +2,11 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Game.Screens.Play.ReplaySettings;
using osu.Game.Screens.Play.PlayerSettings;
namespace osu.Game.Rulesets.Edit
{
public class ToolboxGroup : ReplayGroup
public class ToolboxGroup : PlayerSettingsGroup
{
protected override string Title => "toolbox";

View File

@ -171,10 +171,10 @@ namespace osu.Game.Rulesets.Scoring
public readonly Bindable<ScoringMode> Mode = new Bindable<ScoringMode>();
protected sealed override bool HasCompleted => Hits == MaxHits;
protected sealed override bool HasCompleted => JudgedHits == MaxHits;
protected int MaxHits { get; private set; }
protected int Hits { get; private set; }
protected int JudgedHits { get; private set; }
private double maxHighestCombo;
@ -259,7 +259,7 @@ namespace osu.Game.Rulesets.Scoring
baseScore += judgement.NumericResult;
rollingMaxBaseScore += judgement.MaxNumericResult;
Hits++;
JudgedHits++;
}
else if (judgement.IsHit)
bonusScore += judgement.NumericResult;
@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Scoring
baseScore -= judgement.NumericResult;
rollingMaxBaseScore -= judgement.MaxNumericResult;
Hits--;
JudgedHits--;
}
else if (judgement.IsHit)
bonusScore -= judgement.NumericResult;
@ -305,14 +305,14 @@ namespace osu.Game.Rulesets.Scoring
{
if (storeResults)
{
MaxHits = Hits;
MaxHits = JudgedHits;
maxHighestCombo = HighestCombo;
maxBaseScore = baseScore;
}
base.Reset(storeResults);
Hits = 0;
JudgedHits = 0;
baseScore = 0;
rollingMaxBaseScore = 0;
bonusScore = 0;

View File

@ -240,13 +240,13 @@ namespace osu.Game.Rulesets.UI
foreach (var mod in Mods.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty);
// Post-process the beatmap
processor.PostProcess(Beatmap);
// Apply defaults
foreach (var h in Beatmap.HitObjects)
h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty);
// Post-process the beatmap
processor.PostProcess(Beatmap);
KeyBindingInputManager = CreateInputManager();
KeyBindingInputManager.RelativeSizeAxes = Axes.Both;

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI
public abstract class RulesetInputManager<T> : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler
where T : struct
{
public class RulesetKeyBindingContainer : DatabasedKeyBindingInputManager<T>
public class RulesetKeyBindingContainer : DatabasedKeyBindingContainer<T>
{
public RulesetKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
: base(ruleset, variant, unique)
@ -136,9 +136,20 @@ namespace osu.Game.Rulesets.UI
int loops = 0;
while (validState && requireMoreUpdateLoops && loops++ < max_catch_up_updates_per_frame)
{
if (!base.UpdateSubTree())
return false;
if (isAttached)
{
// When handling replay input, we need to consider the possibility of fast-forwarding, which may cause the clock to be updated
// to a point very far into the future, then playing a frame at that time. In such a case, lifetime MUST be updated before
// input is handled. This is why base.Update is not called from the derived Update when handling replay input, and is instead
// called manually at the correct time here.
base.Update();
}
}
return true;
}
@ -173,8 +184,11 @@ namespace osu.Game.Rulesets.UI
// to ensure that the its time is valid for our children before input is processed
Clock.ProcessFrame();
// Process input
base.Update();
if (!isAttached)
{
// For non-replay input handling, this provides equivalent input ordering as if Update was not overridden
base.Update();
}
}
#endregion

View File

@ -3,29 +3,30 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Screens.Play.ReplaySettings;
using OpenTK;
using osu.Framework.Input;
using osu.Game.Screens.Play.PlayerSettings;
using OpenTK.Input;
namespace osu.Game.Screens.Play.HUD
{
public class ReplaySettingsOverlay : VisibilityContainer
public class PlayerSettingsOverlay : VisibilityContainer
{
private const int fade_duration = 200;
public bool ReplayLoaded;
public readonly PlaybackSettings PlaybackSettings;
public readonly VisualSettings VisualSettings;
//public readonly CollectionSettings CollectionSettings;
//public readonly DiscussionSettings DiscussionSettings;
public ReplaySettingsOverlay()
public PlayerSettingsOverlay()
{
AlwaysPresent = true;
RelativeSizeAxes = Axes.Both;
Child = new FillFlowContainer<ReplayGroup>
Child = new FillFlowContainer<PlayerSettingsGroup>
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
@ -33,11 +34,12 @@ namespace osu.Game.Screens.Play.HUD
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Margin = new MarginPadding { Top = 100, Right = 10 },
Children = new[]
Children = new PlayerSettingsGroup[]
{
//CollectionSettings = new CollectionSettings(),
//DiscussionSettings = new DiscussionSettings(),
PlaybackSettings = new PlaybackSettings(),
VisualSettings = new VisualSettings()
}
};

View File

@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play
public readonly HealthDisplay HealthDisplay;
public readonly SongProgress Progress;
public readonly ModDisplay ModDisplay;
public readonly ReplaySettingsOverlay ReplaySettingsOverlay;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
private Bindable<bool> showHud;
private readonly BindableBool replayLoaded = new BindableBool();
@ -58,7 +58,7 @@ namespace osu.Game.Screens.Play
HealthDisplay = CreateHealthDisplay(),
Progress = CreateProgress(),
ModDisplay = CreateModsContainer(),
ReplaySettingsOverlay = CreateReplaySettingsOverlay(),
PlayerSettingsOverlay = CreatePlayerSettingsOverlay()
}
});
@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play
ModDisplay.Current.BindTo(working.Mods);
ReplaySettingsOverlay.PlaybackSettings.AdjustableClock = adjustableSourceClock;
PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableSourceClock;
}
[BackgroundDependencyLoader(true)]
@ -115,16 +115,16 @@ namespace osu.Game.Screens.Play
private void replayLoadedValueChanged(bool loaded)
{
ReplaySettingsOverlay.ReplayLoaded = loaded;
PlayerSettingsOverlay.ReplayLoaded = loaded;
if (loaded)
{
ReplaySettingsOverlay.Show();
PlayerSettingsOverlay.Show();
ModDisplay.FadeIn(200);
}
else
{
ReplaySettingsOverlay.Hide();
PlayerSettingsOverlay.Hide();
ModDisplay.Delay(2000).FadeOut(200);
}
}
@ -213,7 +213,7 @@ namespace osu.Game.Screens.Play
Margin = new MarginPadding { Top = 20, Right = 10 },
};
protected virtual ReplaySettingsOverlay CreateReplaySettingsOverlay() => new ReplaySettingsOverlay();
protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay();
protected virtual void BindProcessor(ScoreProcessor processor)
{

View File

@ -209,6 +209,8 @@ namespace osu.Game.Screens.Play
{
Action = () =>
{
if (!IsCurrentScreen) return;
//we want to hide the hitrenderer immediately (looks better).
//we may be able to remove this once the mouse cursor trail is improved.
RulesetContainer?.Hide();
@ -274,6 +276,8 @@ namespace osu.Game.Screens.Play
{
onCompletionEvent = Schedule(delegate
{
if (!IsCurrentScreen) return;
var score = new Score
{
Beatmap = Beatmap.Value.BeatmapInfo,

View File

@ -13,6 +13,7 @@ using osu.Game.Screens.Backgrounds;
using OpenTK;
using osu.Framework.Localisation;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play.PlayerSettings;
namespace osu.Game.Screens.Play
{
@ -21,6 +22,7 @@ namespace osu.Game.Screens.Play
private Player player;
private BeatmapMetadataDisplay info;
private VisualSettings visualSettings;
private bool showOverlays = true;
public override bool ShowOverlaysOnEnter => showOverlays;
@ -49,6 +51,12 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
Add(visualSettings = new VisualSettings
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding(25)
});
LoadComponentAsync(player);
}
@ -110,7 +118,7 @@ namespace osu.Game.Screens.Play
private void pushWhenLoaded()
{
if (player.LoadState != LoadState.Ready)
if (player.LoadState != LoadState.Ready || visualSettings.IsHovered)
{
Schedule(pushWhenLoaded);
return;

View File

@ -1,15 +1,15 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.Music;
using System.Collections.Generic;
namespace osu.Game.Screens.Play.ReplaySettings
namespace osu.Game.Screens.Play.PlayerSettings
{
public class CollectionSettings : ReplayGroup
public class CollectionSettings : PlayerSettingsGroup
{
protected override string Title => @"collections";

View File

@ -6,9 +6,9 @@ using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.ReplaySettings
namespace osu.Game.Screens.Play.PlayerSettings
{
public class DiscussionSettings : ReplayGroup
public class DiscussionSettings : PlayerSettingsGroup
{
protected override string Title => @"discussions";
@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.ReplaySettings
{
Children = new Drawable[]
{
new ReplayCheckbox
new PlayerCheckbox
{
LabelText = "Show floating comments",
Bindable = config.GetBindable<bool>(OsuSetting.FloatingComments)

View File

@ -1,15 +1,15 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Timing;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Screens.Play.ReplaySettings
namespace osu.Game.Screens.Play.PlayerSettings
{
public class PlaybackSettings : ReplayGroup
public class PlaybackSettings : PlayerSettingsGroup
{
private const int padding = 10;
@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.ReplaySettings
public IAdjustableClock AdjustableClock { set; get; }
private readonly ReplaySliderBar<double> sliderbar;
private readonly PlayerSliderBar<double> sliderbar;
public PlaybackSettings()
{
@ -47,7 +47,7 @@ namespace osu.Game.Screens.Play.ReplaySettings
}
},
},
sliderbar = new ReplaySliderBar<double>
sliderbar = new PlayerSliderBar<double>
{
Bindable = new BindableDouble(1)
{

View File

@ -5,9 +5,9 @@ using osu.Framework.Allocation;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Play.ReplaySettings
namespace osu.Game.Screens.Play.PlayerSettings
{
public class ReplayCheckbox : OsuCheckbox
public class PlayerCheckbox : OsuCheckbox
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)

View File

@ -1,8 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -10,10 +8,12 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Screens.Play.ReplaySettings
namespace osu.Game.Screens.Play.PlayerSettings
{
public abstract class ReplayGroup : Container
public abstract class PlayerSettingsGroup : Container
{
/// <summary>
/// The title to be displayed in the header of this group.
@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.ReplaySettings
private Color4 buttonActiveColour;
protected ReplayGroup()
protected PlayerSettingsGroup()
{
AutoSizeAxes = Axes.Y;
Width = container_width;

View File

@ -1,16 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
using System;
using osu.Game.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Play.ReplaySettings
namespace osu.Game.Screens.Play.PlayerSettings
{
public class ReplaySliderBar<T> : SettingsSlider<T>
public class PlayerSliderBar<T> : SettingsSlider<T>
where T : struct, IEquatable<T>, IComparable, IConvertible
{
protected override Drawable CreateControl() => new Sliderbar

View File

@ -0,0 +1,52 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Screens.Play.PlayerSettings
{
public class VisualSettings : PlayerSettingsGroup
{
protected override string Title => "Visual settings";
private readonly PlayerSliderBar<double> dimSliderBar;
private readonly PlayerSliderBar<double> blurSliderBar;
private readonly PlayerCheckbox showStoryboardToggle;
private readonly PlayerCheckbox mouseWheelDisabledToggle;
public VisualSettings()
{
Children = new Drawable[]
{
new OsuSpriteText
{
Text = "Background dim:"
},
dimSliderBar = new PlayerSliderBar<double>(),
new OsuSpriteText
{
Text = "Background blur:"
},
blurSliderBar = new PlayerSliderBar<double>(),
new OsuSpriteText
{
Text = "Toggles:"
},
showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboards" },
mouseWheelDisabledToggle = new PlayerCheckbox { LabelText = "Disable mouse wheel" }
};
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
dimSliderBar.Bindable = config.GetBindable<double>(OsuSetting.DimLevel);
blurSliderBar.Bindable = config.GetBindable<double>(OsuSetting.BlurLevel);
showStoryboardToggle.Bindable = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
mouseWheelDisabledToggle.Bindable = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
}
}
}

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@ -47,10 +48,13 @@ namespace osu.Game.Screens.Select
private SampleChannel sampleConfirm;
[BackgroundDependencyLoader(true)]
private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay)
private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, OsuGame game)
{
sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection");
if (game != null)
modSelect.SelectedMods.BindTo(game.SelectedMods);
Footer.AddButton(@"mods", colours.Yellow, modSelect, Key.F1, float.MaxValue);
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.fa_times_circle_o, colours.Purple, null, Key.Number1);
@ -121,6 +125,9 @@ namespace osu.Game.Screens.Select
if (Beatmap.Value.Track != null)
Beatmap.Value.Track.Looping = false;
Beatmap.Value.Mods.UnbindBindings();
Beatmap.Value.Mods.Value = new Mod[] { };
return false;
}

View File

@ -181,7 +181,7 @@ namespace osu.Game.Screens.Select
[BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapManager beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours)
{
dependencies.CacheAs<SongSelect>(this);
dependencies.CacheAs(this);
if (Footer != null)
{

View File

@ -91,6 +91,10 @@
<Reference Include="Humanizer, Version=2.2.0.0, Culture=neutral, PublicKeyToken=979442b78dfc278e, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Humanizer.Core.2.2.0\lib\netstandard1.0\Humanizer.dll</HintPath>
</Reference>
<Reference Include="JetBrains.Annotations, Version=11.1.0.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\JetBrains.Annotations.11.1.0\lib\net20\JetBrains.Annotations.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.Data.Sqlite, Version=2.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Microsoft.Data.Sqlite.Core.2.0.0\lib\netstandard2.0\Microsoft.Data.Sqlite.dll</HintPath>
</Reference>
@ -271,11 +275,14 @@
<Compile Include="Configuration\SpeedChangeVisualisationMethod.cs" />
<Compile Include="Database\DatabaseContextFactory.cs" />
<Compile Include="Database\IHasPrimaryKey.cs" />
<Compile Include="Graphics\Containers\LinkFlowContainer.cs" />
<Compile Include="Graphics\Textures\LargeTextureStore.cs" />
<Compile Include="Online\API\Requests\GetUserRequest.cs" />
<Compile Include="Migrations\20180125143340_Settings.cs" />
<Compile Include="Migrations\20180125143340_Settings.Designer.cs">
<DependentUpon>20180125143340_Settings.cs</DependentUpon>
</Compile>
<Compile Include="Migrations\20180131154205_AddMuteBinding.cs" />
<Compile Include="Overlays\Profile\SupporterIcon.cs" />
<Compile Include="Online\API\Requests\GetFriendsRequest.cs" />
<Compile Include="Overlays\Settings\DangerousSettingsButton.cs" />
@ -301,6 +308,8 @@
</Compile>
<Compile Include="Migrations\OsuDbContextModelSnapshot.cs" />
<Compile Include="Online\API\Requests\GetBeatmapSetRequest.cs" />
<Compile Include="Online\Chat\DrawableLinkCompiler.cs" />
<Compile Include="Online\Chat\MessageFormatter.cs" />
<Compile Include="Online\API\Requests\APIResponseBeatmapSet.cs" />
<Compile Include="Online\API\Requests\GetUserMostPlayedBeatmapsRequest.cs" />
<Compile Include="Overlays\BeatmapSet\Scores\ClickableUsername.cs" />
@ -330,6 +339,7 @@
<Compile Include="Overlays\Social\SocialListPanel.cs" />
<Compile Include="Overlays\Social\SocialPanel.cs" />
<Compile Include="Rulesets\Mods\IApplicableToDrawableHitObject.cs" />
<Compile Include="Screens\Play\PlayerSettings\VisualSettings.cs" />
<Compile Include="Rulesets\UI\HitObjectContainer.cs" />
<Compile Include="Rulesets\UI\Scrolling\Visualisers\SequentialSpeedChangeVisualiser.cs" />
<Compile Include="Rulesets\UI\Scrolling\Visualisers\ISpeedChangeVisualiser.cs" />
@ -442,8 +452,8 @@
<Compile Include="Graphics\UserInterface\Volume\VolumeControlReceptor.cs" />
<Compile Include="Graphics\UserInterface\Volume\VolumeMeter.cs" />
<Compile Include="Input\Bindings\DatabasedKeyBinding.cs" />
<Compile Include="Input\Bindings\DatabasedKeyBindingInputManager.cs" />
<Compile Include="Input\Bindings\GlobalKeyBindingInputManager.cs" />
<Compile Include="Input\Bindings\DatabasedKeyBindingContainer.cs" />
<Compile Include="Input\Bindings\GlobalActionContainer.cs" />
<Compile Include="Input\Handlers\ReplayInputHandler.cs" />
<Compile Include="Input\KeyBindingStore.cs" />
<Compile Include="IO\FileInfo.cs" />
@ -466,7 +476,6 @@
<Compile Include="Online\API\Requests\SearchBeatmapSetsRequest.cs" />
<Compile Include="Online\API\Requests\GetMessagesRequest.cs" />
<Compile Include="Online\API\Requests\GetScoresRequest.cs" />
<Compile Include="Online\API\Requests\GetUserRequest.cs" />
<Compile Include="Online\API\Requests\GetUsersRequest.cs" />
<Compile Include="Online\API\Requests\ListChannelsRequest.cs" />
<Compile Include="Online\API\Requests\PostMessageRequest.cs" />
@ -761,13 +770,13 @@
<Compile Include="Screens\Play\Player.cs" />
<Compile Include="Screens\Play\PlayerLoader.cs" />
<Compile Include="Screens\Play\ReplayPlayer.cs" />
<Compile Include="Screens\Play\HUD\ReplaySettingsOverlay.cs" />
<Compile Include="Screens\Play\ReplaySettings\CollectionSettings.cs" />
<Compile Include="Screens\Play\ReplaySettings\DiscussionSettings.cs" />
<Compile Include="Screens\Play\ReplaySettings\PlaybackSettings.cs" />
<Compile Include="Screens\Play\ReplaySettings\ReplayCheckbox.cs" />
<Compile Include="Screens\Play\ReplaySettings\ReplayGroup.cs" />
<Compile Include="Screens\Play\ReplaySettings\ReplaySliderBar.cs" />
<Compile Include="Screens\Play\HUD\PlayerSettingsOverlay.cs" />
<Compile Include="Screens\Play\PlayerSettings\CollectionSettings.cs" />
<Compile Include="Screens\Play\PlayerSettings\DiscussionSettings.cs" />
<Compile Include="Screens\Play\PlayerSettings\PlaybackSettings.cs" />
<Compile Include="Screens\Play\PlayerSettings\PlayerCheckbox.cs" />
<Compile Include="Screens\Play\PlayerSettings\PlayerSettingsGroup.cs" />
<Compile Include="Screens\Play\PlayerSettings\PlayerSliderBar.cs" />
<Compile Include="Screens\Play\SkipButton.cs" />
<Compile Include="Screens\Play\SongProgress.cs" />
<Compile Include="Screens\Play\SongProgressBar.cs" />

View File

@ -47,6 +47,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste
<package id="Humanizer.Core.zh-CN" version="2.2.0" targetFramework="net461" />
<package id="Humanizer.Core.zh-Hans" version="2.2.0" targetFramework="net461" />
<package id="Humanizer.Core.zh-Hant" version="2.2.0" targetFramework="net461" />
<package id="JetBrains.Annotations" version="11.1.0" targetFramework="net461" />
<package id="Microsoft.CSharp" version="4.4.0" targetFramework="net461" />
<package id="Microsoft.Data.Sqlite.Core" version="2.0.0" targetFramework="net461" />
<package id="Microsoft.EntityFrameworkCore" version="2.0.0" targetFramework="net461" />