mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 10:33:30 +08:00
Merge branch 'master' into notepieces
This commit is contained in:
commit
3fbabdfed2
6
.gitignore
vendored
6
.gitignore
vendored
@ -252,7 +252,11 @@ paket-files/
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
.idea/.idea.osu/.idea/*.xml
|
||||
.idea/.idea.osu/.idea/codeStyles/*.xml
|
||||
.idea/.idea.osu/.idea/dataSources/*.xml
|
||||
.idea/.idea.osu/.idea/dictionaries/*.xml
|
||||
.idea/.idea.osu/*.iml
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 9ee64e369fe6fdafc6aed40f5a35b5f01eb82c53
|
||||
Subproject commit 651e598b016b43e31ab1c1b29d5b30c92361b8d9
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Tests.Visual;
|
||||
@ -37,13 +38,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
beatmap.HitObjects.Add(new JuiceStream
|
||||
{
|
||||
X = 0.5f - width / 2,
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(width * CatchPlayfield.BASE_WIDTH, 0)
|
||||
},
|
||||
PathType = PathType.Linear,
|
||||
Distance = width * CatchPlayfield.BASE_WIDTH,
|
||||
}),
|
||||
StartTime = i * 2000,
|
||||
NewCombo = i % 8 == 0
|
||||
});
|
||||
|
@ -34,9 +34,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
{
|
||||
StartTime = obj.StartTime,
|
||||
Samples = obj.Samples,
|
||||
ControlPoints = curveData.ControlPoints,
|
||||
PathType = curveData.PathType,
|
||||
Distance = curveData.Distance,
|
||||
Path = curveData.Path,
|
||||
NodeSamples = curveData.NodeSamples,
|
||||
RepeatCount = curveData.RepeatCount,
|
||||
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
|
||||
|
@ -10,7 +10,6 @@ using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Objects
|
||||
{
|
||||
@ -138,28 +137,18 @@ namespace osu.Game.Rulesets.Catch.Objects
|
||||
|
||||
public double Duration => EndTime - StartTime;
|
||||
|
||||
public double Distance
|
||||
private SliderPath path;
|
||||
|
||||
public SliderPath Path
|
||||
{
|
||||
get { return Path.Distance; }
|
||||
set { Path.Distance = value; }
|
||||
get => path;
|
||||
set => path = value;
|
||||
}
|
||||
|
||||
public SliderPath Path { get; } = new SliderPath();
|
||||
|
||||
public Vector2[] ControlPoints
|
||||
{
|
||||
get { return Path.ControlPoints; }
|
||||
set { Path.ControlPoints = value; }
|
||||
}
|
||||
public double Distance => Path.Distance;
|
||||
|
||||
public List<List<SampleInfo>> NodeSamples { get; set; } = new List<List<SampleInfo>>();
|
||||
|
||||
public PathType PathType
|
||||
{
|
||||
get { return Path.PathType; }
|
||||
set { Path.PathType = value; }
|
||||
}
|
||||
|
||||
public double? LegacyLastTickOffset { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawable;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -21,14 +20,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
private readonly CatcherArea catcherArea;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Constant;
|
||||
|
||||
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> getVisualRepresentation)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Down;
|
||||
|
||||
Container explodingFruitContainer;
|
||||
|
||||
Anchor = Anchor.TopCentre;
|
||||
@ -55,8 +48,6 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
HitObjectContainer
|
||||
}
|
||||
};
|
||||
|
||||
VisibleTimeRange.Value = BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
|
||||
}
|
||||
|
||||
public bool CheckIfWeCanCatch(CatchHitObject obj) => catcherArea.AttemptCatch(obj);
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawable;
|
||||
@ -18,9 +19,15 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class CatchRulesetContainer : ScrollingRulesetContainer<CatchPlayfield, CatchHitObject>
|
||||
{
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Constant;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Down;
|
||||
TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450);
|
||||
}
|
||||
|
||||
public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this);
|
||||
|
@ -1,33 +0,0 @@
|
||||
// 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.Configuration;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which provides a <see cref="IScrollingInfo"/> to children.
|
||||
/// </summary>
|
||||
public class ScrollingTestContainer : Container
|
||||
{
|
||||
[Cached(Type = typeof(IScrollingInfo))]
|
||||
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
|
||||
|
||||
public ScrollingTestContainer(ScrollingDirection direction)
|
||||
{
|
||||
scrollingInfo.Direction.Value = direction;
|
||||
}
|
||||
|
||||
public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up;
|
||||
}
|
||||
|
||||
public class TestScrollingInfo : IScrollingInfo
|
||||
{
|
||||
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mania.UI.Components;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
@ -93,7 +94,6 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
Height = 0.85f,
|
||||
AccentColour = Color4.OrangeRed,
|
||||
Action = { Value = action },
|
||||
VisibleTimeRange = { Value = 2000 }
|
||||
};
|
||||
|
||||
columns.Add(column);
|
||||
@ -104,6 +104,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
TimeRange = 2000,
|
||||
Child = column
|
||||
};
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Tests.Visual;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
@ -122,7 +123,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
var specialAction = ManiaAction.Special1;
|
||||
|
||||
var stage = new ManiaStage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction) { VisibleTimeRange = { Value = 2000 } };
|
||||
var stage = new ManiaStage(0, new StageDefinition { Columns = 2 }, ref action, ref specialAction);
|
||||
stages.Add(stage);
|
||||
|
||||
return new ScrollingTestContainer(direction)
|
||||
@ -131,6 +132,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
TimeRange = 2000,
|
||||
Child = stage
|
||||
};
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
|
@ -6,11 +6,14 @@ using OpenTK;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public class ManiaEditRulesetContainer : ManiaRulesetContainer
|
||||
{
|
||||
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
|
||||
|
||||
public ManiaEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
|
@ -10,31 +10,32 @@ using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Edit.Blueprints;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public class ManiaHitObjectComposer : HitObjectComposer<ManiaHitObject>
|
||||
{
|
||||
protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config;
|
||||
|
||||
public ManiaHitObjectComposer(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
{
|
||||
}
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
dependencies.CacheAs<IScrollingInfo>(new ManiaScrollingInfo(Config));
|
||||
return dependencies;
|
||||
}
|
||||
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
protected override RulesetContainer<ManiaHitObject> CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||
=> new ManiaEditRulesetContainer(ruleset, beatmap);
|
||||
{
|
||||
var rulesetContainer = new ManiaEditRulesetContainer(ruleset, beatmap);
|
||||
|
||||
// This is the earliest we can cache the scrolling info to ourselves, before masks are added to the hierarchy and inject it
|
||||
dependencies.CacheAs(rulesetContainer.ScrollingInfo);
|
||||
|
||||
return rulesetContainer;
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<HitObjectCompositionTool> CompositionTools => Array.Empty<HitObjectCompositionTool>();
|
||||
|
||||
|
@ -5,7 +5,6 @@ using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
|
||||
|
@ -1,12 +1,12 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Input.Bindings;
|
||||
@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public class Column : ManiaScrollingPlayfield, IKeyBindingHandler<ManiaAction>, IHasAccentColour
|
||||
public class Column : ScrollingPlayfield, IKeyBindingHandler<ManiaAction>, IHasAccentColour
|
||||
{
|
||||
private const float column_width = 45;
|
||||
private const float special_column_width = 70;
|
||||
@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
hitObject.AccentColour = AccentColour;
|
||||
hitObject.OnNewResult += OnNewResult;
|
||||
|
||||
HitObjects.Add(hitObject);
|
||||
HitObjectContainer.Add(hitObject);
|
||||
}
|
||||
|
||||
internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result)
|
||||
@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
explosionContainer.Add(new HitExplosion(judgedObject)
|
||||
{
|
||||
Anchor = Direction == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre
|
||||
Anchor = Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre
|
||||
});
|
||||
}
|
||||
|
||||
@ -154,10 +154,10 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
return false;
|
||||
|
||||
var nextObject =
|
||||
HitObjects.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
|
||||
HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
|
||||
// fallback to non-alive objects to find next off-screen object
|
||||
HitObjects.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
|
||||
HitObjects.Objects.LastOrDefault();
|
||||
HitObjectContainer.Objects.FirstOrDefault(h => h.HitObject.StartTime > Time.Current) ??
|
||||
HitObjectContainer.Objects.LastOrDefault();
|
||||
|
||||
nextObject?.PlaySamples();
|
||||
|
||||
|
@ -1,20 +1,19 @@
|
||||
// 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.Framework.Graphics.Containers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public class ManiaPlayfield : ManiaScrollingPlayfield
|
||||
public class ManiaPlayfield : ScrollingPlayfield
|
||||
{
|
||||
private readonly List<ManiaStage> stages = new List<ManiaStage>();
|
||||
|
||||
@ -41,7 +40,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
for (int i = 0; i < stageDefinitions.Count; i++)
|
||||
{
|
||||
var newStage = new ManiaStage(firstColumnIndex, stageDefinitions[i], ref normalColumnAction, ref specialColumnAction);
|
||||
newStage.VisibleTimeRange.BindTo(VisibleTimeRange);
|
||||
|
||||
playfieldGrid.Content[0][i] = newStage;
|
||||
|
||||
@ -68,11 +66,5 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ManiaConfigManager maniaConfig)
|
||||
{
|
||||
maniaConfig.BindWith(ManiaSetting.ScrollTime, VisibleTimeRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
@ -35,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config;
|
||||
|
||||
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||
|
||||
public ManiaRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
@ -70,18 +73,11 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private void load()
|
||||
{
|
||||
BarLines.ForEach(Playfield.Add);
|
||||
}
|
||||
|
||||
private DependencyContainer dependencies;
|
||||
Config.BindWith(ManiaSetting.ScrollDirection, configDirection);
|
||||
configDirection.BindValueChanged(v => Direction.Value = (ScrollingDirection)v, true);
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
if (dependencies.Get<ManiaScrollingInfo>() == null)
|
||||
dependencies.CacheAs<IScrollingInfo>(new ManiaScrollingInfo(Config));
|
||||
|
||||
return dependencies;
|
||||
Config.BindWith(ManiaSetting.ScrollTime, TimeRange);
|
||||
}
|
||||
|
||||
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages)
|
||||
|
@ -1,23 +0,0 @@
|
||||
// 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.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public class ManiaScrollingInfo : IScrollingInfo
|
||||
{
|
||||
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||
|
||||
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
|
||||
|
||||
public ManiaScrollingInfo(ManiaConfigManager config)
|
||||
{
|
||||
config.BindWith(ManiaSetting.ScrollDirection, configDirection);
|
||||
configDirection.BindValueChanged(v => Direction.Value = (ScrollingDirection)v, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// 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.Configuration;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
public abstract class ManiaScrollingPlayfield : ScrollingPlayfield
|
||||
{
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IScrollingInfo scrollingInfo)
|
||||
{
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
direction.BindValueChanged(direction => Direction.Value = direction, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// <summary>
|
||||
/// A collection of <see cref="Column"/>s.
|
||||
/// </summary>
|
||||
public class ManiaStage : ManiaScrollingPlayfield
|
||||
public class ManiaStage : ScrollingPlayfield
|
||||
{
|
||||
public const float HIT_TARGET_POSITION = 50;
|
||||
|
||||
@ -144,8 +144,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
public void AddColumn(Column c)
|
||||
{
|
||||
c.VisibleTimeRange.BindTo(VisibleTimeRange);
|
||||
|
||||
topLevelContainer.Add(c.TopLevelContainer.CreateProxy());
|
||||
columnFlow.Add(c);
|
||||
AddNested(c);
|
||||
|
@ -18,6 +18,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
|
||||
@ -108,13 +109,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(239, 176),
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(154, 28),
|
||||
new Vector2(52, -34)
|
||||
},
|
||||
Distance = 700,
|
||||
}, 700),
|
||||
RepeatCount = repeats,
|
||||
NodeSamples = createEmptySamples(repeats),
|
||||
StackHeight = 10
|
||||
@ -141,12 +141,11 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-(distance / 2), 0),
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(distance, 0),
|
||||
},
|
||||
Distance = distance,
|
||||
}, distance),
|
||||
RepeatCount = repeats,
|
||||
NodeSamples = createEmptySamples(repeats),
|
||||
StackHeight = stackHeight
|
||||
@ -161,13 +160,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-200, 0),
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.PerfectCurve, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(200, 200),
|
||||
new Vector2(400, 0)
|
||||
},
|
||||
Distance = 600,
|
||||
}, 600),
|
||||
RepeatCount = repeats,
|
||||
NodeSamples = createEmptySamples(repeats)
|
||||
};
|
||||
@ -181,10 +179,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
PathType = PathType.Linear,
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-200, 0),
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(150, 75),
|
||||
@ -192,8 +189,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new Vector2(300, -200),
|
||||
new Vector2(400, 0),
|
||||
new Vector2(430, 0)
|
||||
},
|
||||
Distance = 793.4417,
|
||||
}),
|
||||
RepeatCount = repeats,
|
||||
NodeSamples = createEmptySamples(repeats)
|
||||
};
|
||||
@ -207,18 +203,16 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
PathType = PathType.Bezier,
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-200, 0),
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.Bezier, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(150, 75),
|
||||
new Vector2(200, 100),
|
||||
new Vector2(300, -200),
|
||||
new Vector2(430, 0)
|
||||
},
|
||||
Distance = 480,
|
||||
}),
|
||||
RepeatCount = repeats,
|
||||
NodeSamples = createEmptySamples(repeats)
|
||||
};
|
||||
@ -232,10 +226,9 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
var slider = new Slider
|
||||
{
|
||||
PathType = PathType.Linear,
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(0, 0),
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(-200, 0),
|
||||
@ -243,8 +236,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
new Vector2(0, -200),
|
||||
new Vector2(-200, -200),
|
||||
new Vector2(0, -200)
|
||||
},
|
||||
Distance = 1000,
|
||||
}),
|
||||
RepeatCount = repeats,
|
||||
NodeSamples = createEmptySamples(repeats)
|
||||
};
|
||||
@ -264,15 +256,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
StartTime = Time.Current + 1000,
|
||||
Position = new Vector2(-100, 0),
|
||||
PathType = PathType.Catmull,
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.Catmull, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(50, -50),
|
||||
new Vector2(150, 50),
|
||||
new Vector2(200, 0)
|
||||
},
|
||||
Distance = 300,
|
||||
}),
|
||||
RepeatCount = repeats,
|
||||
NodeSamples = repeatSamples
|
||||
};
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
@ -35,14 +36,12 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
var slider = new Slider
|
||||
{
|
||||
Position = new Vector2(256, 192),
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.Bezier, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(150, 150),
|
||||
new Vector2(300, 0)
|
||||
},
|
||||
PathType = PathType.Bezier,
|
||||
Distance = 350
|
||||
})
|
||||
};
|
||||
|
||||
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
|
||||
|
@ -35,9 +35,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
||||
{
|
||||
StartTime = original.StartTime,
|
||||
Samples = original.Samples,
|
||||
ControlPoints = curveData.ControlPoints,
|
||||
PathType = curveData.PathType,
|
||||
Distance = curveData.Distance,
|
||||
Path = curveData.Path,
|
||||
NodeSamples = curveData.NodeSamples,
|
||||
RepeatCount = curveData.RepeatCount,
|
||||
Position = positionData?.Position ?? Vector2.Zero,
|
||||
|
@ -1,7 +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 System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -9,6 +8,7 @@ using osu.Framework.Graphics.Lines;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using OpenTK;
|
||||
|
||||
@ -55,16 +55,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Position = slider.StackedPosition + slider.ControlPoints[index];
|
||||
Position = slider.StackedPosition + slider.Path.ControlPoints[index];
|
||||
|
||||
marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow;
|
||||
|
||||
path.ClearVertices();
|
||||
|
||||
if (index != slider.ControlPoints.Length - 1)
|
||||
if (index != slider.Path.ControlPoints.Length - 1)
|
||||
{
|
||||
path.AddVertex(Vector2.Zero);
|
||||
path.AddVertex(slider.ControlPoints[index + 1] - slider.ControlPoints[index]);
|
||||
path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]);
|
||||
}
|
||||
|
||||
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
|
||||
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override bool OnDrag(DragEvent e)
|
||||
{
|
||||
var newControlPoints = slider.ControlPoints.ToArray();
|
||||
var newControlPoints = slider.Path.ControlPoints.ToArray();
|
||||
|
||||
if (index == 0)
|
||||
{
|
||||
@ -96,8 +96,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
if (isSegmentSeparatorWithPrevious)
|
||||
newControlPoints[index - 1] = newControlPoints[index];
|
||||
|
||||
slider.ControlPoints = newControlPoints;
|
||||
slider.Path.Calculate(true);
|
||||
slider.Path = new SliderPath(slider.Path.Type, newControlPoints);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -106,8 +105,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
|
||||
|
||||
private bool isSegmentSeparatorWithNext => index < slider.ControlPoints.Length - 1 && slider.ControlPoints[index + 1] == slider.ControlPoints[index];
|
||||
private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index];
|
||||
|
||||
private bool isSegmentSeparatorWithPrevious => index > 0 && slider.ControlPoints[index - 1] == slider.ControlPoints[index];
|
||||
private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index];
|
||||
}
|
||||
}
|
||||
|
@ -19,15 +19,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
InternalChild = pieces = new Container<PathControlPointPiece> { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
slider.ControlPointsChanged += _ => updatePathControlPoints();
|
||||
slider.PathChanged += _ => updatePathControlPoints();
|
||||
updatePathControlPoints();
|
||||
}
|
||||
|
||||
private void updatePathControlPoints()
|
||||
{
|
||||
while (slider.ControlPoints.Length > pieces.Count)
|
||||
while (slider.Path.ControlPoints.Length > pieces.Count)
|
||||
pieces.Add(new PathControlPointPiece(slider, pieces.Count));
|
||||
while (slider.ControlPoints.Length < pieces.Count)
|
||||
while (slider.Path.ControlPoints.Length < pieces.Count)
|
||||
pieces.Remove(pieces[pieces.Count - 1]);
|
||||
}
|
||||
}
|
||||
|
@ -45,8 +45,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
base.Update();
|
||||
|
||||
slider.Path.Calculate();
|
||||
|
||||
var vertices = new List<Vector2>();
|
||||
slider.Path.GetPathToProgress(vertices, 0, 1);
|
||||
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
this.slider = slider;
|
||||
this.position = position;
|
||||
|
||||
slider.ControlPointsChanged += _ => UpdatePosition();
|
||||
slider.PathChanged += _ => UpdatePosition();
|
||||
}
|
||||
|
||||
protected override void UpdatePosition()
|
||||
|
@ -1,16 +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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
|
||||
using OpenTK;
|
||||
@ -119,12 +118,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private void updateSlider()
|
||||
{
|
||||
for (int i = 0; i < segments.Count; i++)
|
||||
segments[i].Calculate(i == segments.Count - 1 ? (Vector2?)cursor : null);
|
||||
|
||||
HitObject.ControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
|
||||
HitObject.PathType = HitObject.ControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear;
|
||||
HitObject.Distance = segments.Sum(s => s.Distance);
|
||||
var newControlPoints = segments.SelectMany(s => s.ControlPoints).Concat(cursor.Yield()).ToArray();
|
||||
HitObject.Path = new SliderPath(newControlPoints.Length > 2 ? PathType.Bezier : PathType.Linear, newControlPoints);
|
||||
}
|
||||
|
||||
private void setState(PlacementState newState)
|
||||
@ -140,41 +135,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private class Segment
|
||||
{
|
||||
public float Distance { get; private set; }
|
||||
|
||||
public readonly List<Vector2> ControlPoints = new List<Vector2>();
|
||||
|
||||
public Segment(Vector2 offset)
|
||||
{
|
||||
ControlPoints.Add(offset);
|
||||
}
|
||||
|
||||
public void Calculate(Vector2? cursor = null)
|
||||
{
|
||||
Span<Vector2> allControlPoints = stackalloc Vector2[ControlPoints.Count + (cursor.HasValue ? 1 : 0)];
|
||||
|
||||
for (int i = 0; i < ControlPoints.Count; i++)
|
||||
allControlPoints[i] = ControlPoints[i];
|
||||
if (cursor.HasValue)
|
||||
allControlPoints[allControlPoints.Length - 1] = cursor.Value;
|
||||
|
||||
List<Vector2> result;
|
||||
|
||||
switch (allControlPoints.Length)
|
||||
{
|
||||
case 1:
|
||||
case 2:
|
||||
result = PathApproximator.ApproximateLinear(allControlPoints);
|
||||
break;
|
||||
default:
|
||||
result = PathApproximator.ApproximateBezier(allControlPoints);
|
||||
break;
|
||||
}
|
||||
|
||||
Distance = 0;
|
||||
for (int i = 0; i < result.Count - 1; i++)
|
||||
Distance += Vector2.Distance(result[i], result[i + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,12 +32,11 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
slider.NestedHitObjects.OfType<RepeatPoint>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
|
||||
|
||||
var newControlPoints = new Vector2[slider.ControlPoints.Length];
|
||||
for (int i = 0; i < slider.ControlPoints.Length; i++)
|
||||
newControlPoints[i] = new Vector2(slider.ControlPoints[i].X, -slider.ControlPoints[i].Y);
|
||||
var newControlPoints = new Vector2[slider.Path.ControlPoints.Length];
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Length; i++)
|
||||
newControlPoints[i] = new Vector2(slider.Path.ControlPoints[i].X, -slider.Path.ControlPoints[i].Y);
|
||||
|
||||
slider.ControlPoints = newControlPoints;
|
||||
slider.Path?.Calculate(); // Recalculate the slider curve
|
||||
slider.Path = new SliderPath(slider.Path.Type, newControlPoints, slider.Path.ExpectedDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
Ball.Scale = new Vector2(HitObject.Scale);
|
||||
};
|
||||
|
||||
slider.ControlPointsChanged += _ => Body.Refresh();
|
||||
slider.PathChanged += _ => Body.Refresh();
|
||||
}
|
||||
|
||||
public override Color4 AccentColour
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
this.slider = slider;
|
||||
|
||||
h.PositionChanged += _ => updatePosition();
|
||||
slider.ControlPointsChanged += _ => updatePosition();
|
||||
slider.PathChanged += _ => updatePosition();
|
||||
|
||||
updatePosition();
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
AlwaysPresent = true;
|
||||
|
||||
hitCircle.PositionChanged += _ => updatePosition();
|
||||
slider.ControlPointsChanged += _ => updatePosition();
|
||||
slider.PathChanged += _ => updatePosition();
|
||||
|
||||
updatePosition();
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
private const float base_scoring_distance = 100;
|
||||
|
||||
public event Action<Vector2[]> ControlPointsChanged;
|
||||
public event Action<SliderPath> PathChanged;
|
||||
|
||||
public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
|
||||
public double Duration => EndTime - StartTime;
|
||||
@ -52,35 +52,23 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
}
|
||||
}
|
||||
|
||||
public SliderPath Path { get; } = new SliderPath();
|
||||
private SliderPath path;
|
||||
|
||||
public Vector2[] ControlPoints
|
||||
public SliderPath Path
|
||||
{
|
||||
get => Path.ControlPoints;
|
||||
get => path;
|
||||
set
|
||||
{
|
||||
if (Path.ControlPoints == value)
|
||||
return;
|
||||
Path.ControlPoints = value;
|
||||
path = value;
|
||||
|
||||
ControlPointsChanged?.Invoke(value);
|
||||
PathChanged?.Invoke(value);
|
||||
|
||||
if (TailCircle != null)
|
||||
TailCircle.Position = EndPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public PathType PathType
|
||||
{
|
||||
get { return Path.PathType; }
|
||||
set { Path.PathType = value; }
|
||||
}
|
||||
|
||||
public double Distance
|
||||
{
|
||||
get { return Path.Distance; }
|
||||
set { Path.Distance = value; }
|
||||
}
|
||||
public double Distance => Path.Distance;
|
||||
|
||||
public override Vector2 Position
|
||||
{
|
||||
|
@ -8,7 +8,6 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
@ -39,10 +38,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
/// </summary>
|
||||
private const float left_area_size = 240;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Overlapping;
|
||||
|
||||
private readonly Container<HitExplosion> hitExplosionContainer;
|
||||
private readonly Container<KiaiHitExplosion> kiaiExplosionContainer;
|
||||
private readonly JudgementContainer<DrawableTaikoJudgement> judgementContainer;
|
||||
@ -59,8 +54,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public TaikoPlayfield(ControlPointInfo controlPoints)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Left;
|
||||
|
||||
InternalChild = new PlayfieldAdjustmentContainer
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
@ -200,8 +193,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
VisibleTimeRange.Value = 7000;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
@ -14,6 +14,7 @@ using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.Taiko.Replays;
|
||||
using System.Linq;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
|
||||
@ -21,9 +22,15 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
public class TaikoRulesetContainer : ScrollingRulesetContainer<TaikoPlayfield, TaikoHitObject>
|
||||
{
|
||||
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||
|
||||
protected override bool UserScrollSpeedAdjustment => false;
|
||||
|
||||
public TaikoRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
Direction.Value = ScrollingDirection.Left;
|
||||
TimeRange.Value = 7000;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
|
54
osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs
Normal file
54
osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs
Normal file
@ -0,0 +1,54 @@
|
||||
// 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.Rulesets.UI.Scrolling.Algorithms;
|
||||
|
||||
namespace osu.Game.Tests.ScrollAlgorithms
|
||||
{
|
||||
[TestFixture]
|
||||
public class ConstantScrollTest
|
||||
{
|
||||
private IScrollAlgorithm algorithm;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
algorithm = new ConstantScrollAlgorithm();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplayStartTime()
|
||||
{
|
||||
Assert.AreEqual(-8000, algorithm.GetDisplayStartTime(2000, 10000));
|
||||
Assert.AreEqual(-3000, algorithm.GetDisplayStartTime(2000, 5000));
|
||||
Assert.AreEqual(2000, algorithm.GetDisplayStartTime(7000, 5000));
|
||||
Assert.AreEqual(7000, algorithm.GetDisplayStartTime(17000, 10000));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLength()
|
||||
{
|
||||
Assert.AreEqual(1f / 5, algorithm.GetLength(0, 1000, 5000, 1));
|
||||
Assert.AreEqual(1f / 5, algorithm.GetLength(6000, 7000, 5000, 1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPosition()
|
||||
{
|
||||
Assert.AreEqual(1f / 5, algorithm.PositionAt(1000, 0, 5000, 1));
|
||||
Assert.AreEqual(1f / 5, algorithm.PositionAt(6000, 5000, 5000, 1));
|
||||
}
|
||||
|
||||
[TestCase(1000)]
|
||||
[TestCase(10000)]
|
||||
[TestCase(15000)]
|
||||
[TestCase(20000)]
|
||||
[TestCase(25000)]
|
||||
public void TestTime(double time)
|
||||
{
|
||||
Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 0, 5000, 1), 0, 5000, 1), 0.001);
|
||||
Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 5000, 5000, 1), 5000, 5000, 1), 0.001);
|
||||
}
|
||||
}
|
||||
}
|
67
osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs
Normal file
67
osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs
Normal file
@ -0,0 +1,67 @@
|
||||
// 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.Framework.Lists;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
|
||||
|
||||
namespace osu.Game.Tests.ScrollAlgorithms
|
||||
{
|
||||
[TestFixture]
|
||||
public class OverlappingScrollTest
|
||||
{
|
||||
private IScrollAlgorithm algorithm;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var controlPoints = new SortedList<MultiplierControlPoint>
|
||||
{
|
||||
new MultiplierControlPoint(0) { Velocity = 1 },
|
||||
new MultiplierControlPoint(10000) { Velocity = 2f },
|
||||
new MultiplierControlPoint(20000) { Velocity = 0.5f }
|
||||
};
|
||||
|
||||
algorithm = new OverlappingScrollAlgorithm(controlPoints);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplayStartTime()
|
||||
{
|
||||
Assert.AreEqual(1000, algorithm.GetDisplayStartTime(2000, 1000)); // Like constant
|
||||
Assert.AreEqual(10000, algorithm.GetDisplayStartTime(10500, 1000)); // 10500 - (1000 * 0.5)
|
||||
Assert.AreEqual(20000, algorithm.GetDisplayStartTime(22000, 1000)); // 23000 - (1000 / 0.5)
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLength()
|
||||
{
|
||||
Assert.AreEqual(1f / 5, algorithm.GetLength(0, 1000, 5000, 1)); // Like constant
|
||||
Assert.AreEqual(1f / 5, algorithm.GetLength(10000, 10500, 5000, 1)); // (10500 - 10000) / 0.5 / 5000
|
||||
Assert.AreEqual(1f / 5, algorithm.GetLength(20000, 22000, 5000, 1)); // (22000 - 20000) * 0.5 / 5000
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPosition()
|
||||
{
|
||||
// Basically same calculations as TestLength()
|
||||
Assert.AreEqual(1f / 5, algorithm.PositionAt(1000, 0, 5000, 1));
|
||||
Assert.AreEqual(1f / 5, algorithm.PositionAt(10500, 10000, 5000, 1));
|
||||
Assert.AreEqual(1f / 5, algorithm.PositionAt(22000, 20000, 5000, 1));
|
||||
}
|
||||
|
||||
[TestCase(1000)]
|
||||
[TestCase(10000)]
|
||||
[TestCase(15000)]
|
||||
[TestCase(20000)]
|
||||
[TestCase(25000)]
|
||||
[Ignore("Disabled for now because overlapping control points have multiple time values under the same position."
|
||||
+ "Ideally, scrolling should be changed to constant or sequential during editing of hitobjects.")]
|
||||
public void TestTime(double time)
|
||||
{
|
||||
Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 0, 5000, 1), 0, 5000, 1), 0.001);
|
||||
Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 5000, 5000, 1), 5000, 5000, 1), 0.001);
|
||||
}
|
||||
}
|
||||
}
|
64
osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs
Normal file
64
osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs
Normal file
@ -0,0 +1,64 @@
|
||||
// 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.Framework.Lists;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
|
||||
|
||||
namespace osu.Game.Tests.ScrollAlgorithms
|
||||
{
|
||||
[TestFixture]
|
||||
public class SequentialScrollTest
|
||||
{
|
||||
private IScrollAlgorithm algorithm;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var controlPoints = new SortedList<MultiplierControlPoint>
|
||||
{
|
||||
new MultiplierControlPoint(0) { Velocity = 1 },
|
||||
new MultiplierControlPoint(10000) { Velocity = 2f },
|
||||
new MultiplierControlPoint(20000) { Velocity = 0.5f }
|
||||
};
|
||||
|
||||
algorithm = new SequentialScrollAlgorithm(controlPoints);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisplayStartTime()
|
||||
{
|
||||
// Sequential scroll algorithm approximates the start time
|
||||
// This should be fixed in the future
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLength()
|
||||
{
|
||||
Assert.AreEqual(1f / 5, algorithm.GetLength(0, 1000, 5000, 1)); // Like constant
|
||||
Assert.AreEqual(1f / 5, algorithm.GetLength(10000, 10500, 5000, 1)); // (10500 - 10000) / 0.5 / 5000
|
||||
Assert.AreEqual(1f / 5, algorithm.GetLength(20000, 22000, 5000, 1)); // (22000 - 20000) * 0.5 / 5000
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPosition()
|
||||
{
|
||||
// Basically same calculations as TestLength()
|
||||
Assert.AreEqual(1f / 5, algorithm.PositionAt(1000, 0, 5000, 1));
|
||||
Assert.AreEqual(1f / 5, algorithm.PositionAt(10500, 10000, 5000, 1));
|
||||
Assert.AreEqual(1f / 5, algorithm.PositionAt(22000, 20000, 5000, 1));
|
||||
}
|
||||
|
||||
[TestCase(1000)]
|
||||
[TestCase(10000)]
|
||||
[TestCase(15000)]
|
||||
[TestCase(20000)]
|
||||
[TestCase(25000)]
|
||||
public void TestTime(double time)
|
||||
{
|
||||
Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 0, 5000, 1), 0, 5000, 1), 0.001);
|
||||
Assert.AreEqual(time, algorithm.TimeAt(algorithm.PositionAt(time, 5000, 5000, 1), 5000, 5000, 1), 0.001);
|
||||
}
|
||||
}
|
||||
}
|
123
osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
Normal file
123
osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
Normal file
@ -0,0 +1,123 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays.Chat.Tabs;
|
||||
using osu.Game.Users;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public class TestCaseChannelTabControl : OsuTestCase
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
typeof(ChannelTabControl),
|
||||
};
|
||||
|
||||
private readonly ChannelTabControl channelTabControl;
|
||||
|
||||
public TestCaseChannelTabControl()
|
||||
{
|
||||
SpriteText currentText;
|
||||
Add(new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
channelTabControl = new ChannelTabControl
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Height = 50
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.1f),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 50,
|
||||
Depth = -1,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Add(new Container
|
||||
{
|
||||
Origin = Anchor.TopLeft,
|
||||
Anchor = Anchor.TopLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
currentText = new SpriteText
|
||||
{
|
||||
Text = "Currently selected channel:"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
channelTabControl.OnRequestLeave += channel => channelTabControl.RemoveChannel(channel);
|
||||
channelTabControl.Current.ValueChanged += channel => currentText.Text = "Currently selected channel: " + channel.ToString();
|
||||
|
||||
AddStep("Add random private channel", addRandomUser);
|
||||
AddAssert("There is only one channels", () => channelTabControl.Items.Count() == 2);
|
||||
AddRepeatStep("Add 3 random private channels", addRandomUser, 3);
|
||||
AddAssert("There are four channels", () => channelTabControl.Items.Count() == 5);
|
||||
AddStep("Add random public channel", () => addChannel(RNG.Next().ToString()));
|
||||
|
||||
AddRepeatStep("Select a random channel", () => channelTabControl.Current.Value = channelTabControl.Items.ElementAt(RNG.Next(channelTabControl.Items.Count())), 20);
|
||||
}
|
||||
|
||||
private List<User> users;
|
||||
|
||||
private void addRandomUser()
|
||||
{
|
||||
channelTabControl.AddChannel(new Channel
|
||||
{
|
||||
Users =
|
||||
{
|
||||
users?.Count > 0
|
||||
? users[RNG.Next(0, users.Count - 1)]
|
||||
: new User
|
||||
{
|
||||
Id = RNG.Next(),
|
||||
Username = "testuser" + RNG.Next(1000)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addChannel(string name)
|
||||
{
|
||||
channelTabControl.AddChannel(new Channel
|
||||
{
|
||||
Type = ChannelType.Public,
|
||||
Name = name
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IAPIProvider api)
|
||||
{
|
||||
GetUsersRequest req = new GetUsersRequest();
|
||||
req.Success += list => users = list.Select(e => e.User).ToList();
|
||||
|
||||
api.Queue(req);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +1,45 @@
|
||||
// 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.ComponentModel;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Chat;
|
||||
using osu.Game.Overlays.Chat.Tabs;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
[Description("Testing chat api and overlay")]
|
||||
public class TestCaseChatDisplay : OsuTestCase
|
||||
{
|
||||
public TestCaseChatDisplay()
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[]
|
||||
{
|
||||
Add(new ChatOverlay
|
||||
typeof(ChatOverlay),
|
||||
typeof(ChatLine),
|
||||
typeof(DrawableChannel),
|
||||
typeof(ChannelSelectorTabItem),
|
||||
typeof(ChannelTabControl),
|
||||
typeof(ChannelTabItem),
|
||||
typeof(PrivateChannelTabItem),
|
||||
typeof(TabCloseButton)
|
||||
};
|
||||
|
||||
[Cached]
|
||||
private readonly ChannelManager channelManager = new ChannelManager();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
State = Visibility.Visible
|
||||
});
|
||||
channelManager,
|
||||
new ChatOverlay { State = Visibility.Visible }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,14 +50,13 @@ namespace osu.Game.Tests.Visual
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
linkColour = colours.Blue;
|
||||
Dependencies.Cache(new ChatOverlay
|
||||
{
|
||||
AvailableChannels =
|
||||
{
|
||||
new Channel { Name = "#english" },
|
||||
new Channel { Name = "#japanese" }
|
||||
}
|
||||
});
|
||||
|
||||
var chatManager = new ChannelManager();
|
||||
chatManager.AvailableChannels.Add(new Channel { Name = "#english"});
|
||||
chatManager.AvailableChannels.Add(new Channel { Name = "#japanese" });
|
||||
Dependencies.Cache(chatManager);
|
||||
|
||||
Dependencies.Cache(new ChatOverlay());
|
||||
|
||||
testLinksGeneral();
|
||||
testEcho();
|
||||
|
@ -11,6 +11,7 @@ using OpenTK;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Edit;
|
||||
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
|
||||
@ -53,12 +54,11 @@ namespace osu.Game.Tests.Visual
|
||||
new Slider
|
||||
{
|
||||
Position = new Vector2(128, 256),
|
||||
ControlPoints = new[]
|
||||
Path = new SliderPath(PathType.Linear, new[]
|
||||
{
|
||||
Vector2.Zero,
|
||||
new Vector2(216, 0),
|
||||
},
|
||||
Distance = 216,
|
||||
}),
|
||||
Scale = 0.5f,
|
||||
}
|
||||
},
|
||||
|
@ -9,6 +9,7 @@ using OpenTK;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
@ -22,6 +23,7 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(Playfield) };
|
||||
|
||||
private readonly ScrollingTestContainer[] scrollContainers = new ScrollingTestContainer[4];
|
||||
private readonly TestPlayfield[] playfields = new TestPlayfield[4];
|
||||
|
||||
public TestCaseScrollingHitObjects()
|
||||
@ -33,18 +35,38 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
playfields[0] = new TestPlayfield(ScrollingDirection.Up),
|
||||
playfields[1] = new TestPlayfield(ScrollingDirection.Down)
|
||||
scrollContainers[0] = new ScrollingTestContainer(ScrollingDirection.Up)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = playfields[0] = new TestPlayfield()
|
||||
},
|
||||
scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Up)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = playfields[1] = new TestPlayfield()
|
||||
},
|
||||
},
|
||||
new Drawable[]
|
||||
{
|
||||
playfields[2] = new TestPlayfield(ScrollingDirection.Left),
|
||||
playfields[3] = new TestPlayfield(ScrollingDirection.Right)
|
||||
scrollContainers[2] = new ScrollingTestContainer(ScrollingDirection.Up)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = playfields[2] = new TestPlayfield()
|
||||
},
|
||||
scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Up)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = playfields[3] = new TestPlayfield()
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddSliderStep("Time range", 100, 10000, 5000, v => playfields.ForEach(p => p.VisibleTimeRange.Value = v));
|
||||
AddStep("Constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
|
||||
AddStep("Overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
|
||||
AddStep("Sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
|
||||
|
||||
AddSliderStep("Time range", 100, 10000, 5000, v => scrollContainers.ForEach(c => c.TimeRange = v));
|
||||
AddStep("Add control point", () => addControlPoint(Time.Current + 5000));
|
||||
}
|
||||
|
||||
@ -52,7 +74,7 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
playfields.ForEach(p => p.HitObjects.AddControlPoint(new MultiplierControlPoint(0)));
|
||||
scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0)));
|
||||
|
||||
for (int i = 0; i <= 5000; i += 1000)
|
||||
addHitObject(Time.Current + i);
|
||||
@ -73,12 +95,15 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
private void addControlPoint(double time)
|
||||
{
|
||||
scrollContainers.ForEach(c =>
|
||||
{
|
||||
c.ControlPoints.Add(new MultiplierControlPoint(time) { DifficultyPoint = { SpeedMultiplier = 3 } });
|
||||
c.ControlPoints.Add(new MultiplierControlPoint(time + 2000) { DifficultyPoint = { SpeedMultiplier = 2 } });
|
||||
c.ControlPoints.Add(new MultiplierControlPoint(time + 3000) { DifficultyPoint = { SpeedMultiplier = 1 } });
|
||||
});
|
||||
|
||||
playfields.ForEach(p =>
|
||||
{
|
||||
p.HitObjects.AddControlPoint(new MultiplierControlPoint(time) { DifficultyPoint = { SpeedMultiplier = 3 } });
|
||||
p.HitObjects.AddControlPoint(new MultiplierControlPoint(time + 2000) { DifficultyPoint = { SpeedMultiplier = 2 } });
|
||||
p.HitObjects.AddControlPoint(new MultiplierControlPoint(time + 3000) { DifficultyPoint = { SpeedMultiplier = 1 } });
|
||||
|
||||
TestDrawableControlPoint createDrawablePoint(double t)
|
||||
{
|
||||
var obj = new TestDrawableControlPoint(p.Direction, t);
|
||||
@ -111,15 +136,14 @@ namespace osu.Game.Tests.Visual
|
||||
}
|
||||
}
|
||||
|
||||
private void setScrollAlgorithm(ScrollVisualisationMethod algorithm) => scrollContainers.ForEach(c => c.ScrollAlgorithm = algorithm);
|
||||
|
||||
private class TestPlayfield : ScrollingPlayfield
|
||||
{
|
||||
public new readonly ScrollingDirection Direction;
|
||||
public new ScrollingDirection Direction => base.Direction.Value;
|
||||
|
||||
public TestPlayfield(ScrollingDirection direction)
|
||||
public TestPlayfield()
|
||||
{
|
||||
Direction = direction;
|
||||
|
||||
Padding = new MarginPadding(2);
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
|
@ -5,7 +5,7 @@ using System.ComponentModel;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
{
|
||||
public enum SpeedChangeVisualisationMethod
|
||||
public enum ScrollVisualisationMethod
|
||||
{
|
||||
[Description("Sequential")]
|
||||
Sequential,
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@ -21,16 +22,17 @@ namespace osu.Game.Graphics.Containers
|
||||
}
|
||||
|
||||
private OsuGame game;
|
||||
|
||||
private ChannelManager channelManager;
|
||||
private Action showNotImplementedError;
|
||||
private GameHost host;
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuGame game, NotificationOverlay notifications, GameHost host)
|
||||
private void load(OsuGame game, NotificationOverlay notifications, GameHost host, ChannelManager channelManager)
|
||||
{
|
||||
// will be null in tests
|
||||
this.game = game;
|
||||
this.host = host;
|
||||
this.channelManager = channelManager;
|
||||
|
||||
showNotImplementedError = () => notifications?.Post(new SimpleNotification
|
||||
{
|
||||
@ -80,7 +82,15 @@ namespace osu.Game.Graphics.Containers
|
||||
game?.ShowBeatmapSet(setId);
|
||||
break;
|
||||
case LinkAction.OpenChannel:
|
||||
game?.OpenChannel(linkArgument);
|
||||
try
|
||||
{
|
||||
channelManager?.OpenChannel(linkArgument);
|
||||
}
|
||||
catch (ChannelNotFoundException e)
|
||||
{
|
||||
Logger.Log($"The requested channel \"{linkArgument}\" does not exist");
|
||||
}
|
||||
|
||||
break;
|
||||
case LinkAction.OpenEditorTimestamp:
|
||||
case LinkAction.JoinMultiplayerMatch:
|
||||
|
28
osu.Game/Online/API/APIMessagesRequest.cs
Normal file
28
osu.Game/Online/API/APIMessagesRequest.cs
Normal file
@ -0,0 +1,28 @@
|
||||
// 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.IO.Network;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Online.API
|
||||
{
|
||||
public abstract class APIMessagesRequest : APIRequest<List<Message>>
|
||||
{
|
||||
private readonly long? sinceId;
|
||||
|
||||
protected APIMessagesRequest(long? sinceId)
|
||||
{
|
||||
this.sinceId = sinceId;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
|
||||
if (sinceId.HasValue) req.AddParameter(@"since", sinceId.Value.ToString());
|
||||
|
||||
return req;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// 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.Net.Http;
|
||||
using osu.Framework.IO.Network;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class CreateNewPrivateMessageRequest : APIRequest<CreateNewPrivateMessageResponse>
|
||||
{
|
||||
private readonly User user;
|
||||
private readonly Message message;
|
||||
|
||||
public CreateNewPrivateMessageRequest(User user, Message message)
|
||||
{
|
||||
this.user = user;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
protected override WebRequest CreateWebRequest()
|
||||
{
|
||||
var req = base.CreateWebRequest();
|
||||
req.Method = HttpMethod.Post;
|
||||
req.AddParameter(@"target_id", user.Id.ToString());
|
||||
req.AddParameter(@"message", message.Content);
|
||||
req.AddParameter(@"is_action", message.IsAction.ToString().ToLowerInvariant());
|
||||
return req;
|
||||
}
|
||||
|
||||
protected override string Target => @"chat/new";
|
||||
}
|
||||
}
|
@ -0,0 +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 Newtonsoft.Json;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public class CreateNewPrivateMessageResponse
|
||||
{
|
||||
[JsonProperty("new_channel_id")]
|
||||
public int ChannelID;
|
||||
|
||||
public Message Message;
|
||||
}
|
||||
}
|
@ -3,15 +3,64 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
public class Channel
|
||||
{
|
||||
public readonly int MaxHistory = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Contains every joined user except the current logged in user. Currently only returned for PM channels.
|
||||
/// </summary>
|
||||
public readonly ObservableCollection<User> Users = new ObservableCollection<User>();
|
||||
|
||||
[JsonProperty(@"users")]
|
||||
private long[] userIds
|
||||
{
|
||||
set
|
||||
{
|
||||
foreach (var id in value)
|
||||
Users.Add(new User { Id = id });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains all the messages send in the channel.
|
||||
/// </summary>
|
||||
public readonly SortedList<Message> Messages = new SortedList<Message>(Comparer<Message>.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Contains all the messages that are still pending for submission to the server.
|
||||
/// </summary>
|
||||
private readonly List<LocalEchoMessage> pendingMessages = new List<LocalEchoMessage>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An event that fires when new messages arrived.
|
||||
/// </summary>
|
||||
public event Action<IEnumerable<Message>> NewMessagesArrived;
|
||||
|
||||
/// <summary>
|
||||
/// An event that fires when a pending message gets resolved.
|
||||
/// </summary>
|
||||
public event Action<LocalEchoMessage, Message> PendingMessageResolved;
|
||||
|
||||
/// <summary>
|
||||
/// An event that fires when a pending message gets removed.
|
||||
/// </summary>
|
||||
public event Action<Message> MessageRemoved;
|
||||
|
||||
public bool ReadOnly => false; //todo not yet used.
|
||||
|
||||
public override string ToString() => Name;
|
||||
|
||||
[JsonProperty(@"name")]
|
||||
public string Name;
|
||||
|
||||
@ -22,19 +71,16 @@ namespace osu.Game.Online.Chat
|
||||
public ChannelType Type;
|
||||
|
||||
[JsonProperty(@"channel_id")]
|
||||
public int Id;
|
||||
public long Id;
|
||||
|
||||
[JsonProperty(@"last_message_id")]
|
||||
public long? LastMessageId;
|
||||
|
||||
public readonly SortedList<Message> Messages = new SortedList<Message>(Comparer<Message>.Default);
|
||||
|
||||
private readonly List<LocalEchoMessage> pendingMessages = new List<LocalEchoMessage>();
|
||||
|
||||
/// <summary>
|
||||
/// Signalles if the current user joined this channel or not. Defaults to false.
|
||||
/// </summary>
|
||||
public Bindable<bool> Joined = new Bindable<bool>();
|
||||
|
||||
public bool ReadOnly => false;
|
||||
|
||||
public const int MAX_HISTORY = 300;
|
||||
|
||||
[JsonConstructor]
|
||||
@ -42,10 +88,10 @@ namespace osu.Game.Online.Chat
|
||||
{
|
||||
}
|
||||
|
||||
public event Action<IEnumerable<Message>> NewMessagesArrived;
|
||||
public event Action<LocalEchoMessage, Message> PendingMessageResolved;
|
||||
public event Action<Message> MessageRemoved;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the argument message as a local echo. When this local echo is resolved <see cref="PendingMessageResolved"/> will get called.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public void AddLocalEcho(LocalEchoMessage message)
|
||||
{
|
||||
pendingMessages.Add(message);
|
||||
@ -54,8 +100,12 @@ namespace osu.Game.Online.Chat
|
||||
NewMessagesArrived?.Invoke(new[] { message });
|
||||
}
|
||||
|
||||
public bool MessagesLoaded { get; private set; }
|
||||
public bool MessagesLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// Adds new messages to the channel and purges old messages. Triggers the <see cref="NewMessagesArrived"/> event.
|
||||
/// </summary>
|
||||
/// <param name="messages"></param>
|
||||
public void AddNewMessages(params Message[] messages)
|
||||
{
|
||||
messages = messages.Except(Messages).ToArray();
|
||||
@ -63,7 +113,6 @@ namespace osu.Game.Online.Chat
|
||||
if (messages.Length == 0) return;
|
||||
|
||||
Messages.AddRange(messages);
|
||||
MessagesLoaded = true;
|
||||
|
||||
var maxMessageId = messages.Max(m => m.Id);
|
||||
if (maxMessageId > LastMessageId)
|
||||
@ -74,14 +123,6 @@ namespace osu.Game.Online.Chat
|
||||
NewMessagesArrived?.Invoke(messages);
|
||||
}
|
||||
|
||||
private void purgeOldMessages()
|
||||
{
|
||||
// never purge local echos
|
||||
int messageCount = Messages.Count - pendingMessages.Count;
|
||||
if (messageCount > MAX_HISTORY)
|
||||
Messages.RemoveRange(0, messageCount - MAX_HISTORY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace or remove a message from the channel.
|
||||
/// </summary>
|
||||
@ -101,17 +142,18 @@ namespace osu.Game.Online.Chat
|
||||
}
|
||||
|
||||
if (Messages.Contains(final))
|
||||
{
|
||||
// message already inserted, so let's throw away this update.
|
||||
// we may want to handle this better in the future, but for the time being api requests are single-threaded so order is assumed.
|
||||
MessageRemoved?.Invoke(echo);
|
||||
return;
|
||||
}
|
||||
throw new InvalidOperationException("Attempted to add the same message again");
|
||||
|
||||
Messages.Add(final);
|
||||
PendingMessageResolved?.Invoke(echo, final);
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
private void purgeOldMessages()
|
||||
{
|
||||
// never purge local echos
|
||||
int messageCount = Messages.Count - pendingMessages.Count;
|
||||
if (messageCount > MaxHistory)
|
||||
Messages.RemoveRange(0, messageCount - MaxHistory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
409
osu.Game/Online/Chat/ChannelManager.cs
Normal file
409
osu.Game/Online/Chat/ChannelManager.cs
Normal file
@ -0,0 +1,409 @@
|
||||
// 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.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages everything channel related
|
||||
/// </summary>
|
||||
public class ChannelManager : Component, IOnlineComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// The channels the player joins on startup
|
||||
/// </summary>
|
||||
private readonly string[] defaultChannels =
|
||||
{
|
||||
@"#lazer",
|
||||
@"#osu",
|
||||
@"#lobby"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The currently opened channel
|
||||
/// </summary>
|
||||
public Bindable<Channel> CurrentChannel { get; } = new Bindable<Channel>();
|
||||
|
||||
/// <summary>
|
||||
/// The Channels the player has joined
|
||||
/// </summary>
|
||||
public ObservableCollection<Channel> JoinedChannels { get; } = new ObservableCollection<Channel>(); //todo: should be publicly readonly
|
||||
|
||||
/// <summary>
|
||||
/// The channels available for the player to join
|
||||
/// </summary>
|
||||
public ObservableCollection<Channel> AvailableChannels { get; } = new ObservableCollection<Channel>(); //todo: should be publicly readonly
|
||||
|
||||
private IAPIProvider api;
|
||||
private ScheduledDelegate fetchMessagesScheduleder;
|
||||
|
||||
public ChannelManager()
|
||||
{
|
||||
CurrentChannel.ValueChanged += currentChannelChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a channel or switches to the channel if already opened.
|
||||
/// </summary>
|
||||
/// <exception cref="ChannelNotFoundException">If the name of the specifed channel was not found this exception will be thrown.</exception>
|
||||
/// <param name="name"></param>
|
||||
public void OpenChannel(string name)
|
||||
{
|
||||
if (name == null)
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
|
||||
CurrentChannel.Value = AvailableChannels.FirstOrDefault(c => c.Name == name) ?? throw new ChannelNotFoundException(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a new private channel.
|
||||
/// </summary>
|
||||
/// <param name="user">The user the private channel is opened with.</param>
|
||||
public void OpenPrivateChannel(User user)
|
||||
{
|
||||
if (user == null)
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
|
||||
CurrentChannel.Value = JoinedChannels.FirstOrDefault(c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Users.Any(u => u.Id == user.Id))
|
||||
?? new Channel { Name = user.Username, Users = { user } };
|
||||
}
|
||||
|
||||
private void currentChannelChanged(Channel channel) => JoinChannel(channel);
|
||||
|
||||
/// <summary>
|
||||
/// Ensure we run post actions in sequence, once at a time.
|
||||
/// </summary>
|
||||
private readonly Queue<Action> postQueue = new Queue<Action>();
|
||||
|
||||
/// <summary>
|
||||
/// Posts a message to the currently opened channel.
|
||||
/// </summary>
|
||||
/// <param name="text">The message text that is going to be posted</param>
|
||||
/// <param name="isAction">Is true if the message is an action, e.g.: user is currently eating </param>
|
||||
public void PostMessage(string text, bool isAction = false)
|
||||
{
|
||||
if (CurrentChannel.Value == null)
|
||||
return;
|
||||
|
||||
var currentChannel = CurrentChannel.Value;
|
||||
|
||||
void dequeueAndRun()
|
||||
{
|
||||
if (postQueue.Count > 0)
|
||||
postQueue.Dequeue().Invoke();
|
||||
}
|
||||
|
||||
postQueue.Enqueue(() =>
|
||||
{
|
||||
if (!api.IsLoggedIn)
|
||||
{
|
||||
currentChannel.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!"));
|
||||
return;
|
||||
}
|
||||
|
||||
var message = new LocalEchoMessage
|
||||
{
|
||||
Sender = api.LocalUser.Value,
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
ChannelId = CurrentChannel.Value.Id,
|
||||
IsAction = isAction,
|
||||
Content = text
|
||||
};
|
||||
|
||||
currentChannel.AddLocalEcho(message);
|
||||
|
||||
// if this is a PM and the first message, we need to do a special request to create the PM channel
|
||||
if (currentChannel.Type == ChannelType.PM && !currentChannel.Joined)
|
||||
{
|
||||
var createNewPrivateMessageRequest = new CreateNewPrivateMessageRequest(currentChannel.Users.First(), message);
|
||||
|
||||
createNewPrivateMessageRequest.Success += createRes =>
|
||||
{
|
||||
currentChannel.Id = createRes.ChannelID;
|
||||
currentChannel.ReplaceMessage(message, createRes.Message);
|
||||
dequeueAndRun();
|
||||
};
|
||||
|
||||
createNewPrivateMessageRequest.Failure += exception =>
|
||||
{
|
||||
Logger.Error(exception, "Posting message failed.");
|
||||
currentChannel.ReplaceMessage(message, null);
|
||||
dequeueAndRun();
|
||||
};
|
||||
|
||||
api.Queue(createNewPrivateMessageRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
var req = new PostMessageRequest(message);
|
||||
|
||||
req.Success += m =>
|
||||
{
|
||||
currentChannel.ReplaceMessage(message, m);
|
||||
dequeueAndRun();
|
||||
};
|
||||
|
||||
req.Failure += exception =>
|
||||
{
|
||||
Logger.Error(exception, "Posting message failed.");
|
||||
currentChannel.ReplaceMessage(message, null);
|
||||
dequeueAndRun();
|
||||
};
|
||||
|
||||
api.Queue(req);
|
||||
});
|
||||
|
||||
// always run if the queue is empty
|
||||
if (postQueue.Count == 1)
|
||||
dequeueAndRun();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Posts a command locally. Commands like /help will result in a help message written in the current channel.
|
||||
/// </summary>
|
||||
/// <param name="text">the text containing the command identifier and command parameters.</param>
|
||||
public void PostCommand(string text)
|
||||
{
|
||||
if (CurrentChannel.Value == null)
|
||||
return;
|
||||
|
||||
var parameters = text.Split(new[] { ' ' }, 2);
|
||||
string command = parameters[0];
|
||||
string content = parameters.Length == 2 ? parameters[1] : string.Empty;
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "me":
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
CurrentChannel.Value.AddNewMessages(new ErrorMessage("Usage: /me [action]"));
|
||||
break;
|
||||
}
|
||||
|
||||
PostMessage(content, true);
|
||||
break;
|
||||
|
||||
case "help":
|
||||
CurrentChannel.Value.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action]"));
|
||||
break;
|
||||
|
||||
default:
|
||||
CurrentChannel.Value.AddNewMessages(new ErrorMessage($@"""/{command}"" is not supported! For a list of supported commands see /help"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChannelMessages(IEnumerable<Message> messages)
|
||||
{
|
||||
var channels = JoinedChannels.ToList();
|
||||
|
||||
foreach (var group in messages.GroupBy(m => m.ChannelId))
|
||||
channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
|
||||
}
|
||||
|
||||
private void initializeChannels()
|
||||
{
|
||||
var req = new ListChannelsRequest();
|
||||
|
||||
var joinDefaults = JoinedChannels.Count == 0;
|
||||
|
||||
req.Success += channels =>
|
||||
{
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
// add as available if not already
|
||||
if (AvailableChannels.All(c => c.Id != channel.Id))
|
||||
AvailableChannels.Add(channel);
|
||||
|
||||
// join any channels classified as "defaults"
|
||||
if (joinDefaults && defaultChannels.Any(c => c.Equals(channel.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
JoinChannel(channel);
|
||||
}
|
||||
};
|
||||
req.Failure += error =>
|
||||
{
|
||||
Logger.Error(error, "Fetching channel list failed");
|
||||
initializeChannels();
|
||||
};
|
||||
|
||||
api.Queue(req);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches inital messages of a channel
|
||||
///
|
||||
/// TODO: remove this when the API supports returning initial fetch messages for more than one channel by specifying the last message id per channel instead of one last message id globally.
|
||||
/// right now it caps out at 50 messages and therefore only returns one channel's worth of content.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel </param>
|
||||
private void fetchInitalMessages(Channel channel)
|
||||
{
|
||||
if (channel.Id <= 0) return;
|
||||
|
||||
var fetchInitialMsgReq = new GetMessagesRequest(channel);
|
||||
fetchInitialMsgReq.Success += messages =>
|
||||
{
|
||||
handleChannelMessages(messages);
|
||||
channel.MessagesLoaded = true; // this will mark the channel as having received messages even if there were none.
|
||||
};
|
||||
|
||||
api.Queue(fetchInitialMsgReq);
|
||||
}
|
||||
|
||||
public void JoinChannel(Channel channel)
|
||||
{
|
||||
if (channel == null) return;
|
||||
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
var existing = JoinedChannels.FirstOrDefault(c => c.Id == channel.Id);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
// if we already have this channel loaded, we don't want to make a second one.
|
||||
channel = existing;
|
||||
}
|
||||
else
|
||||
{
|
||||
var foundSelf = channel.Users.FirstOrDefault(u => u.Id == api.LocalUser.Value.Id);
|
||||
if (foundSelf != null)
|
||||
channel.Users.Remove(foundSelf);
|
||||
|
||||
JoinedChannels.Add(channel);
|
||||
|
||||
if (channel.Type == ChannelType.Public && !channel.Joined)
|
||||
{
|
||||
var req = new JoinChannelRequest(channel, api.LocalUser);
|
||||
req.Success += () =>
|
||||
{
|
||||
channel.Joined.Value = true;
|
||||
JoinChannel(channel);
|
||||
};
|
||||
req.Failure += ex => LeaveChannel(channel);
|
||||
api.Queue(req);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (CurrentChannel.Value == null)
|
||||
CurrentChannel.Value = channel;
|
||||
|
||||
if (!channel.MessagesLoaded)
|
||||
{
|
||||
// let's fetch a small number of messages to bring us up-to-date with the backlog.
|
||||
fetchInitalMessages(channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void LeaveChannel(Channel channel)
|
||||
{
|
||||
if (channel == null) return;
|
||||
|
||||
if (channel == CurrentChannel.Value) CurrentChannel.Value = null;
|
||||
|
||||
JoinedChannels.Remove(channel);
|
||||
|
||||
if (channel.Joined.Value)
|
||||
{
|
||||
api.Queue(new LeaveChannelRequest(channel, api.LocalUser));
|
||||
channel.Joined.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void APIStateChanged(APIAccess api, APIState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case APIState.Online:
|
||||
fetchUpdates();
|
||||
break;
|
||||
default:
|
||||
fetchMessagesScheduleder?.Cancel();
|
||||
fetchMessagesScheduleder = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private long lastMessageId;
|
||||
private const int update_poll_interval = 1000;
|
||||
|
||||
private bool channelsInitialised;
|
||||
|
||||
private void fetchUpdates()
|
||||
{
|
||||
fetchMessagesScheduleder?.Cancel();
|
||||
fetchMessagesScheduleder = Scheduler.AddDelayed(() =>
|
||||
{
|
||||
var fetchReq = new GetUpdatesRequest(lastMessageId);
|
||||
|
||||
fetchReq.Success += updates =>
|
||||
{
|
||||
if (updates?.Presence != null)
|
||||
{
|
||||
foreach (var channel in updates.Presence)
|
||||
{
|
||||
if (!channel.Joined.Value)
|
||||
{
|
||||
// we received this from the server so should mark the channel already joined.
|
||||
channel.Joined.Value = true;
|
||||
|
||||
JoinChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
if (!channelsInitialised)
|
||||
{
|
||||
channelsInitialised = true;
|
||||
// we want this to run after the first presence so we can see if the user is in any channels already.
|
||||
initializeChannels();
|
||||
}
|
||||
|
||||
//todo: handle left channels
|
||||
|
||||
handleChannelMessages(updates.Messages);
|
||||
|
||||
foreach (var group in updates.Messages.GroupBy(m => m.ChannelId))
|
||||
JoinedChannels.FirstOrDefault(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
|
||||
|
||||
lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId;
|
||||
}
|
||||
|
||||
fetchUpdates();
|
||||
};
|
||||
|
||||
fetchReq.Failure += delegate { fetchUpdates(); };
|
||||
|
||||
api.Queue(fetchReq);
|
||||
}, update_poll_interval);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IAPIProvider api)
|
||||
{
|
||||
this.api = api;
|
||||
api.Register(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An exception thrown when a channel could not been found.
|
||||
/// </summary>
|
||||
public class ChannelNotFoundException : Exception
|
||||
{
|
||||
public ChannelNotFoundException(string channelName)
|
||||
: base($"A channel with the name {channelName} could not be found.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Users;
|
||||
|
||||
@ -16,10 +15,10 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
//todo: this should be inside sender.
|
||||
[JsonProperty(@"sender_id")]
|
||||
public int UserId;
|
||||
public long UserId;
|
||||
|
||||
[JsonProperty(@"channel_id")]
|
||||
public int ChannelId;
|
||||
public long ChannelId;
|
||||
|
||||
[JsonProperty(@"is_action")]
|
||||
public bool IsAction;
|
||||
@ -69,12 +68,4 @@ namespace osu.Game.Online.Chat
|
||||
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
}
|
||||
|
||||
public enum TargetType
|
||||
{
|
||||
[Description(@"channel")]
|
||||
Channel,
|
||||
[Description(@"user")]
|
||||
User
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Skinning;
|
||||
using OpenTK.Graphics;
|
||||
@ -180,12 +181,6 @@ namespace osu.Game
|
||||
|
||||
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>
|
||||
@ -343,6 +338,11 @@ namespace osu.Game
|
||||
//overlay elements
|
||||
loadComponentSingleFile(direct = new DirectOverlay { Depth = -1 }, mainContent.Add);
|
||||
loadComponentSingleFile(social = new SocialOverlay { Depth = -1 }, mainContent.Add);
|
||||
loadComponentSingleFile(new ChannelManager(), channelManager =>
|
||||
{
|
||||
dependencies.Cache(channelManager);
|
||||
AddInternal(channelManager);
|
||||
});
|
||||
loadComponentSingleFile(chat = new ChatOverlay { Depth = -1 }, mainContent.Add);
|
||||
loadComponentSingleFile(settings = new MainSettings
|
||||
{
|
||||
|
@ -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;
|
||||
using System.Linq;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
@ -81,6 +82,8 @@ namespace osu.Game.Overlays.Chat
|
||||
Padding = new MarginPadding { Left = padding, Right = padding };
|
||||
}
|
||||
|
||||
private ChannelManager chatManager;
|
||||
|
||||
private Message message;
|
||||
private OsuSpriteText username;
|
||||
private LinkFlowContainer contentFlow;
|
||||
@ -104,9 +107,9 @@ namespace osu.Game.Overlays.Chat
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuColour colours, ChatOverlay chat)
|
||||
private void load(OsuColour colours, ChannelManager chatManager)
|
||||
{
|
||||
this.chat = chat;
|
||||
this.chatManager = chatManager;
|
||||
customUsernameColour = colours.ChatBlue;
|
||||
}
|
||||
|
||||
@ -215,8 +218,6 @@ namespace osu.Game.Overlays.Chat
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private ChatOverlay chat;
|
||||
|
||||
private void updateMessageContent()
|
||||
{
|
||||
this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint);
|
||||
@ -226,7 +227,7 @@ namespace osu.Game.Overlays.Chat
|
||||
username.Text = $@"{message.Sender.Username}" + (senderHasBackground || message.IsAction ? "" : ":");
|
||||
|
||||
// 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);
|
||||
message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument) != true);
|
||||
|
||||
contentFlow.Clear();
|
||||
contentFlow.AddLinks(message.DisplayContent, message.Links);
|
||||
@ -236,20 +237,24 @@ namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
private readonly User sender;
|
||||
|
||||
private Action startChatAction;
|
||||
|
||||
public MessageSender(User sender)
|
||||
{
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(UserProfileOverlay profile)
|
||||
private void load(UserProfileOverlay profile, ChannelManager chatManager)
|
||||
{
|
||||
Action = () => profile?.ShowUser(sender);
|
||||
startChatAction = () => chatManager?.OpenPrivateChannel(sender);
|
||||
}
|
||||
|
||||
public MenuItem[] ContextMenuItems => new MenuItem[]
|
||||
{
|
||||
new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action),
|
||||
new OsuMenuItem("Start Chat", MenuItemType.Standard, startChatAction),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using OpenTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@ -55,15 +54,11 @@ namespace osu.Game.Overlays.Chat
|
||||
Channel.PendingMessageResolved += pendingMessageResolved;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
newMessagesArrived(Channel.Messages);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
newMessagesArrived(Channel.Messages);
|
||||
scrollToEnd();
|
||||
}
|
||||
|
||||
@ -79,7 +74,7 @@ namespace osu.Game.Overlays.Chat
|
||||
private void newMessagesArrived(IEnumerable<Message> newMessages)
|
||||
{
|
||||
// Add up to last Channel.MAX_HISTORY messages
|
||||
var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY));
|
||||
var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MaxHistory));
|
||||
|
||||
flow.AddRange(displayMessages.Select(m => new ChatLine(m)));
|
||||
|
||||
@ -89,7 +84,7 @@ namespace osu.Game.Overlays.Chat
|
||||
scrollToEnd();
|
||||
|
||||
var staleMessages = flow.Children.Where(c => c.LifetimeEnd == double.MaxValue).ToArray();
|
||||
int count = staleMessages.Length - Channel.MAX_HISTORY;
|
||||
int count = staleMessages.Length - Channel.MaxHistory;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
|
@ -15,7 +15,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
namespace osu.Game.Overlays.Chat.Selection
|
||||
{
|
||||
public class ChannelListItem : OsuClickableContainer, IFilterable
|
||||
{
|
@ -9,7 +9,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
namespace osu.Game.Overlays.Chat.Selection
|
||||
{
|
||||
public class ChannelSection : Container, IHasFilterableChildren
|
||||
{
|
@ -18,7 +18,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Overlays.Chat
|
||||
namespace osu.Game.Overlays.Chat.Selection
|
||||
{
|
||||
public class ChannelSelectionOverlay : OsuFocusedOverlayContainer
|
||||
{
|
||||
@ -35,23 +35,6 @@ namespace osu.Game.Overlays.Chat
|
||||
public Action<Channel> OnRequestJoin;
|
||||
public Action<Channel> OnRequestLeave;
|
||||
|
||||
public IEnumerable<ChannelSection> Sections
|
||||
{
|
||||
set
|
||||
{
|
||||
sectionsFlow.ChildrenEnumerable = value;
|
||||
|
||||
foreach (ChannelSection s in sectionsFlow.Children)
|
||||
{
|
||||
foreach (ChannelListItem c in s.ChannelFlow.Children)
|
||||
{
|
||||
c.OnRequestJoin = channel => { OnRequestJoin?.Invoke(channel); };
|
||||
c.OnRequestLeave = channel => { OnRequestLeave?.Invoke(channel); };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ChannelSelectionOverlay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@ -140,6 +123,30 @@ namespace osu.Game.Overlays.Chat
|
||||
search.Current.ValueChanged += newValue => sectionsFlow.SearchTerm = newValue;
|
||||
}
|
||||
|
||||
public void UpdateAvailableChannels(IEnumerable<Channel> channels)
|
||||
{
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
sectionsFlow.ChildrenEnumerable = new[]
|
||||
{
|
||||
new ChannelSection
|
||||
{
|
||||
Header = "All Channels",
|
||||
Channels = channels,
|
||||
},
|
||||
};
|
||||
|
||||
foreach (ChannelSection s in sectionsFlow.Children)
|
||||
{
|
||||
foreach (ChannelListItem c in s.ChannelFlow.Children)
|
||||
{
|
||||
c.OnRequestJoin = channel => { OnRequestJoin?.Invoke(channel); };
|
||||
c.OnRequestLeave = channel => { OnRequestLeave?.Invoke(channel); };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
32
osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs
Normal file
32
osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs
Normal 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.Allocation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Chat;
|
||||
|
||||
namespace osu.Game.Overlays.Chat.Tabs
|
||||
{
|
||||
public class ChannelSelectorTabItem : ChannelTabItem
|
||||
{
|
||||
public override bool IsRemovable => false;
|
||||
|
||||
public ChannelSelectorTabItem(Channel value) : base(value)
|
||||
{
|
||||
Depth = float.MaxValue;
|
||||
Width = 45;
|
||||
|
||||
Icon.Alpha = 0;
|
||||
|
||||
Text.TextSize = 45;
|
||||
TextBold.TextSize = 45;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private new void load(OsuColour colour)
|
||||
{
|
||||
BackgroundInactive = colour.Gray2;
|
||||
BackgroundActive = colour.Gray3;
|
||||
}
|
||||
}
|
||||
}
|
123
osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs
Normal file
123
osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs
Normal file
@ -0,0 +1,123 @@
|
||||
// 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.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Chat;
|
||||
using OpenTK;
|
||||
using osu.Framework.Configuration;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Overlays.Chat.Tabs
|
||||
{
|
||||
public class ChannelTabControl : OsuTabControl<Channel>
|
||||
{
|
||||
public static readonly float SHEAR_WIDTH = 10;
|
||||
|
||||
public Action<Channel> OnRequestLeave;
|
||||
|
||||
public readonly Bindable<bool> ChannelSelectorActive = new Bindable<bool>();
|
||||
|
||||
private readonly ChannelSelectorTabItem selectorTab;
|
||||
|
||||
public ChannelTabControl()
|
||||
{
|
||||
TabContainer.Margin = new MarginPadding { Left = 50 };
|
||||
TabContainer.Spacing = new Vector2(-SHEAR_WIDTH, 0);
|
||||
TabContainer.Masking = false;
|
||||
|
||||
AddInternal(new SpriteIcon
|
||||
{
|
||||
Icon = FontAwesome.fa_comments,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Size = new Vector2(20),
|
||||
Margin = new MarginPadding(10),
|
||||
});
|
||||
|
||||
AddTabItem(selectorTab = new ChannelSelectorTabItem(new Channel { Name = "+" }));
|
||||
|
||||
ChannelSelectorActive.BindTo(selectorTab.Active);
|
||||
}
|
||||
|
||||
protected override void AddTabItem(TabItem<Channel> item, bool addToDropdown = true)
|
||||
{
|
||||
if (item != selectorTab && TabContainer.GetLayoutPosition(selectorTab) < float.MaxValue)
|
||||
// performTabSort might've made selectorTab's position wonky, fix it
|
||||
TabContainer.SetLayoutPosition(selectorTab, float.MaxValue);
|
||||
|
||||
base.AddTabItem(item, addToDropdown);
|
||||
}
|
||||
|
||||
protected override TabItem<Channel> CreateTabItem(Channel value)
|
||||
{
|
||||
switch (value.Type)
|
||||
{
|
||||
case ChannelType.Public:
|
||||
return new ChannelTabItem(value) { OnRequestClose = tabCloseRequested };
|
||||
case ChannelType.PM:
|
||||
return new PrivateChannelTabItem(value) { OnRequestClose = tabCloseRequested };
|
||||
default:
|
||||
throw new InvalidOperationException("Only TargetType User and Channel are supported.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a channel to the ChannelTabControl.
|
||||
/// The first channel added will automaticly selected.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel that is going to be added.</param>
|
||||
public void AddChannel(Channel channel)
|
||||
{
|
||||
if (!Items.Contains(channel))
|
||||
AddItem(channel);
|
||||
|
||||
if (Current.Value == null)
|
||||
Current.Value = channel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a channel from the ChannelTabControl.
|
||||
/// If the selected channel is the one that is beeing removed, the next available channel will be selected.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel that is going to be removed.</param>
|
||||
public void RemoveChannel(Channel channel)
|
||||
{
|
||||
RemoveItem(channel);
|
||||
|
||||
if (Current.Value == channel)
|
||||
Current.Value = Items.FirstOrDefault();
|
||||
}
|
||||
|
||||
protected override void SelectTab(TabItem<Channel> tab)
|
||||
{
|
||||
if (tab is ChannelSelectorTabItem)
|
||||
{
|
||||
tab.Active.Toggle();
|
||||
return;
|
||||
}
|
||||
|
||||
selectorTab.Active.Value = false;
|
||||
|
||||
base.SelectTab(tab);
|
||||
}
|
||||
|
||||
private void tabCloseRequested(TabItem<Channel> tab)
|
||||
{
|
||||
int totalTabs = TabContainer.Count - 1; // account for selectorTab
|
||||
int currentIndex = MathHelper.Clamp(TabContainer.IndexOf(tab), 1, totalTabs);
|
||||
|
||||
if (tab == SelectedTab && totalTabs > 1)
|
||||
// Select the tab after tab-to-be-removed's index, or the tab before if current == last
|
||||
SelectTab(TabContainer[currentIndex == totalTabs ? currentIndex - 1 : currentIndex + 1]);
|
||||
else if (totalTabs == 1 && !selectorTab.Active)
|
||||
// Open channel selection overlay if all channel tabs will be closed after removing this tab
|
||||
SelectTab(selectorTab);
|
||||
|
||||
OnRequestLeave?.Invoke(tab.Value);
|
||||
}
|
||||
}
|
||||
}
|
212
osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs
Normal file
212
osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs
Normal file
@ -0,0 +1,212 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Chat;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Chat.Tabs
|
||||
{
|
||||
public class ChannelTabItem : TabItem<Channel>
|
||||
{
|
||||
protected Color4 BackgroundInactive;
|
||||
private Color4 backgroundHover;
|
||||
protected Color4 BackgroundActive;
|
||||
|
||||
public override bool IsRemovable => !Pinned;
|
||||
|
||||
protected readonly SpriteText Text;
|
||||
protected readonly SpriteText TextBold;
|
||||
protected readonly ClickableContainer CloseButton;
|
||||
private readonly Box box;
|
||||
private readonly Box highlightBox;
|
||||
protected readonly SpriteIcon Icon;
|
||||
|
||||
public Action<ChannelTabItem> OnRequestClose;
|
||||
private readonly Container content;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
|
||||
public ChannelTabItem(Channel value)
|
||||
: base(value)
|
||||
{
|
||||
Width = 150;
|
||||
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
|
||||
Anchor = Anchor.BottomLeft;
|
||||
Origin = Anchor.BottomLeft;
|
||||
|
||||
Shear = new Vector2(ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0);
|
||||
|
||||
Masking = true;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
box = new Box
|
||||
{
|
||||
EdgeSmoothness = new Vector2(1, 0),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
highlightBox = new Box
|
||||
{
|
||||
Width = 5,
|
||||
Alpha = 0,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
EdgeSmoothness = new Vector2(1, 0),
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
content = new Container
|
||||
{
|
||||
Shear = new Vector2(-ChannelTabControl.SHEAR_WIDTH / ChatOverlay.TAB_AREA_HEIGHT, 0),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Icon = new SpriteIcon
|
||||
{
|
||||
Icon = DisplayIcon,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Colour = Color4.Black,
|
||||
X = -10,
|
||||
Alpha = 0.2f,
|
||||
Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT),
|
||||
},
|
||||
Text = new OsuSpriteText
|
||||
{
|
||||
Margin = new MarginPadding(5),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Text = value.ToString(),
|
||||
TextSize = 18,
|
||||
},
|
||||
TextBold = new OsuSpriteText
|
||||
{
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding(5),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Text = value.ToString(),
|
||||
Font = @"Exo2.0-Bold",
|
||||
TextSize = 18,
|
||||
},
|
||||
CloseButton = new TabCloseButton
|
||||
{
|
||||
Alpha = 0,
|
||||
Margin = new MarginPadding { Right = 20 },
|
||||
Origin = Anchor.CentreRight,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Action = delegate
|
||||
{
|
||||
if (IsRemovable) OnRequestClose?.Invoke(this);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual FontAwesome DisplayIcon => FontAwesome.fa_hashtag;
|
||||
|
||||
protected virtual bool ShowCloseOnHover => true;
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
if (IsRemovable && ShowCloseOnHover)
|
||||
CloseButton.FadeIn(200, Easing.OutQuint);
|
||||
|
||||
if (!Active)
|
||||
box.FadeColour(backgroundHover, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
CloseButton.FadeOut(200, Easing.OutQuint);
|
||||
updateState();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
BackgroundActive = colours.ChatBlue;
|
||||
BackgroundInactive = colours.Gray4;
|
||||
backgroundHover = colours.Gray7;
|
||||
|
||||
highlightBox.Colour = colours.Yellow;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
updateState();
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (Active)
|
||||
FadeActive();
|
||||
else
|
||||
FadeInactive();
|
||||
}
|
||||
|
||||
protected const float TRANSITION_LENGTH = 400;
|
||||
|
||||
private readonly EdgeEffectParameters activateEdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 15,
|
||||
Colour = Color4.Black.Opacity(0.4f),
|
||||
};
|
||||
|
||||
private readonly EdgeEffectParameters deactivateEdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 10,
|
||||
Colour = Color4.Black.Opacity(0.2f),
|
||||
};
|
||||
|
||||
protected virtual void FadeActive()
|
||||
{
|
||||
this.ResizeHeightTo(1.1f, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
||||
TweenEdgeEffectTo(activateEdgeEffect, TRANSITION_LENGTH);
|
||||
|
||||
box.FadeColour(BackgroundActive, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
highlightBox.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
||||
Text.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
TextBold.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected virtual void FadeInactive()
|
||||
{
|
||||
this.ResizeHeightTo(1, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
||||
TweenEdgeEffectTo(deactivateEdgeEffect, TRANSITION_LENGTH);
|
||||
|
||||
box.FadeColour(BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
|
||||
Text.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
TextBold.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void OnActivated() => updateState();
|
||||
protected override void OnDeactivated() => updateState();
|
||||
}
|
||||
}
|
97
osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
Normal file
97
osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
Normal file
@ -0,0 +1,97 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Users;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Overlays.Chat.Tabs
|
||||
{
|
||||
public class PrivateChannelTabItem : ChannelTabItem
|
||||
{
|
||||
private readonly OsuSpriteText username;
|
||||
private readonly Avatar avatarContainer;
|
||||
|
||||
protected override FontAwesome DisplayIcon => FontAwesome.fa_at;
|
||||
|
||||
public PrivateChannelTabItem(Channel value)
|
||||
: base(value)
|
||||
{
|
||||
if (value.Type != ChannelType.PM)
|
||||
throw new ArgumentException("Argument value needs to have the targettype user!");
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Horizontal = 3
|
||||
},
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new CircularContainer
|
||||
{
|
||||
Scale = new Vector2(0.95f),
|
||||
Size = new Vector2(ChatOverlay.TAB_AREA_HEIGHT),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Masking = true,
|
||||
Child = new DelayedLoadWrapper(new Avatar(value.Users.First())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
OnLoadComplete = d => d.FadeInFromZero(300, Easing.OutQuint),
|
||||
})
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Text.X = ChatOverlay.TAB_AREA_HEIGHT;
|
||||
TextBold.X = ChatOverlay.TAB_AREA_HEIGHT;
|
||||
}
|
||||
|
||||
protected override bool ShowCloseOnHover => false;
|
||||
|
||||
protected override void FadeActive()
|
||||
{
|
||||
base.FadeActive();
|
||||
|
||||
this.ResizeWidthTo(200, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
CloseButton.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
|
||||
protected override void FadeInactive()
|
||||
{
|
||||
base.FadeInactive();
|
||||
|
||||
this.ResizeWidthTo(ChatOverlay.TAB_AREA_HEIGHT + 10, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
CloseButton.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
var user = Value.Users.First();
|
||||
|
||||
BackgroundActive = user.Colour != null ? OsuColour.FromHex(user.Colour) : colours.BlueDark;
|
||||
BackgroundInactive = BackgroundActive.Darken(0.5f);
|
||||
}
|
||||
}
|
||||
}
|
55
osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs
Normal file
55
osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs
Normal file
@ -0,0 +1,55 @@
|
||||
// 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.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Chat.Tabs
|
||||
{
|
||||
public class TabCloseButton : OsuClickableContainer
|
||||
{
|
||||
private readonly SpriteIcon icon;
|
||||
|
||||
public TabCloseButton()
|
||||
{
|
||||
Size = new Vector2(20);
|
||||
|
||||
Child = icon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(0.75f),
|
||||
Icon = FontAwesome.fa_close,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
icon.ScaleTo(0.5f, 1000, Easing.OutQuint);
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
icon.ScaleTo(0.75f, 1000, Easing.OutElastic);
|
||||
return base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
icon.FadeColour(Color4.Red, 200, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
icon.FadeColour(Color4.White, 200, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
// 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.Linq;
|
||||
using System.Collections.Specialized;
|
||||
using OpenTK;
|
||||
using OpenTK.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
@ -13,42 +12,38 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays.Chat;
|
||||
using osu.Game.Overlays.Chat.Selection;
|
||||
using osu.Game.Overlays.Chat.Tabs;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
public class ChatOverlay : OsuFocusedOverlayContainer, IOnlineComponent
|
||||
public class ChatOverlay : OsuFocusedOverlayContainer
|
||||
{
|
||||
private const float textbox_height = 60;
|
||||
private const float channel_selection_min_height = 0.3f;
|
||||
|
||||
private ScheduledDelegate messageRequest;
|
||||
private ChannelManager channelManager;
|
||||
|
||||
private readonly Container<DrawableChannel> currentChannelContainer;
|
||||
private readonly List<DrawableChannel> loadedChannels = new List<DrawableChannel>();
|
||||
|
||||
private readonly LoadingAnimation loading;
|
||||
|
||||
private readonly FocusedTextBox textbox;
|
||||
|
||||
private APIAccess api;
|
||||
|
||||
private const int transition_length = 500;
|
||||
|
||||
public const float DEFAULT_HEIGHT = 0.4f;
|
||||
|
||||
public const float TAB_AREA_HEIGHT = 50;
|
||||
|
||||
private GetUpdatesRequest fetchReq;
|
||||
|
||||
private readonly ChatTabControl channelTabs;
|
||||
private readonly ChannelTabControl channelTabControl;
|
||||
|
||||
private readonly Container chatContainer;
|
||||
private readonly TabsArea tabsArea;
|
||||
@ -57,7 +52,6 @@ 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;
|
||||
|
||||
@ -154,10 +148,12 @@ namespace osu.Game.Overlays
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4.Black,
|
||||
},
|
||||
channelTabs = new ChatTabControl
|
||||
channelTabControl = new ChannelTabControl
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
OnRequestLeave = removeChannel,
|
||||
OnRequestLeave = channel => channelManager.LeaveChannel(channel)
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -165,11 +161,11 @@ namespace osu.Game.Overlays
|
||||
},
|
||||
};
|
||||
|
||||
channelTabs.Current.ValueChanged += newChannel => CurrentChannel = newChannel;
|
||||
channelTabs.ChannelSelectorActive.ValueChanged += value => channelSelection.State = value ? Visibility.Visible : Visibility.Hidden;
|
||||
channelTabControl.Current.ValueChanged += chat => channelManager.CurrentChannel.Value = chat;
|
||||
channelTabControl.ChannelSelectorActive.ValueChanged += value => channelSelection.State = value ? Visibility.Visible : Visibility.Hidden;
|
||||
channelSelection.StateChanged += state =>
|
||||
{
|
||||
channelTabs.ChannelSelectorActive.Value = state == Visibility.Visible;
|
||||
channelTabControl.ChannelSelectorActive.Value = state == Visibility.Visible;
|
||||
|
||||
if (state == Visibility.Visible)
|
||||
{
|
||||
@ -180,13 +176,75 @@ namespace osu.Game.Overlays
|
||||
else
|
||||
textbox.HoldFocus = true;
|
||||
};
|
||||
|
||||
channelSelection.OnRequestJoin = channel => channelManager.JoinChannel(channel);
|
||||
channelSelection.OnRequestLeave = channel => channelManager.LeaveChannel(channel);
|
||||
}
|
||||
|
||||
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
switch (args.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (Channel newChannel in args.NewItems)
|
||||
{
|
||||
channelTabControl.AddChannel(newChannel);
|
||||
}
|
||||
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (Channel removedChannel in args.OldItems)
|
||||
{
|
||||
channelTabControl.RemoveChannel(removedChannel);
|
||||
loadedChannels.Remove(loadedChannels.Find(c => c.Channel == removedChannel));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void currentChannelChanged(Channel channel)
|
||||
{
|
||||
if (channel == null)
|
||||
{
|
||||
textbox.Current.Disabled = true;
|
||||
currentChannelContainer.Clear(false);
|
||||
channelTabControl.Current.Value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
textbox.Current.Disabled = channel.ReadOnly;
|
||||
|
||||
if (channelTabControl.Current.Value != channel)
|
||||
Scheduler.Add(() => channelTabControl.Current.Value = channel);
|
||||
|
||||
var loaded = loadedChannels.Find(d => d.Channel == channel);
|
||||
if (loaded == null)
|
||||
{
|
||||
currentChannelContainer.FadeOut(500, Easing.OutQuint);
|
||||
loading.Show();
|
||||
|
||||
loaded = new DrawableChannel(channel);
|
||||
loadedChannels.Add(loaded);
|
||||
LoadComponentAsync(loaded, l =>
|
||||
{
|
||||
loading.Hide();
|
||||
|
||||
currentChannelContainer.Clear(false);
|
||||
currentChannelContainer.Add(loaded);
|
||||
currentChannelContainer.FadeIn(500, Easing.OutQuint);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
currentChannelContainer.Clear(false);
|
||||
Scheduler.Add(() => currentChannelContainer.Add(loaded));
|
||||
}
|
||||
}
|
||||
|
||||
private double startDragChatHeight;
|
||||
private bool isDragging;
|
||||
|
||||
public void OpenChannel(Channel channel) => addChannel(channel);
|
||||
|
||||
protected override bool OnDragStart(DragStartEvent e)
|
||||
{
|
||||
isDragging = tabsArea.IsHovered;
|
||||
@ -220,19 +278,6 @@ namespace osu.Game.Overlays
|
||||
return base.OnDragEnd(e);
|
||||
}
|
||||
|
||||
public void APIStateChanged(APIAccess api, APIState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case APIState.Online:
|
||||
initializeChannels();
|
||||
break;
|
||||
default:
|
||||
messageRequest?.Cancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool AcceptsFocus => true;
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
@ -261,11 +306,8 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(APIAccess api, OsuConfigManager config, OsuColour colours)
|
||||
private void load(OsuConfigManager config, OsuColour colours, ChannelManager channelManager)
|
||||
{
|
||||
this.api = api;
|
||||
api.Register(this);
|
||||
|
||||
ChatHeight = config.GetBindable<double>(OsuSetting.ChatDisplayHeight);
|
||||
ChatHeight.ValueChanged += h =>
|
||||
{
|
||||
@ -276,255 +318,49 @@ namespace osu.Game.Overlays
|
||||
ChatHeight.TriggerChange();
|
||||
|
||||
chatBackground.Colour = colours.ChatBlue;
|
||||
}
|
||||
|
||||
private long lastMessageId;
|
||||
|
||||
private readonly List<Channel> careChannels = new List<Channel>();
|
||||
|
||||
private readonly List<DrawableChannel> loadedChannels = new List<DrawableChannel>();
|
||||
|
||||
private void initializeChannels()
|
||||
{
|
||||
loading.Show();
|
||||
|
||||
messageRequest?.Cancel();
|
||||
this.channelManager = channelManager;
|
||||
channelManager.CurrentChannel.ValueChanged += currentChannelChanged;
|
||||
channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged;
|
||||
channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged;
|
||||
|
||||
ListChannelsRequest req = new ListChannelsRequest();
|
||||
req.Success += delegate(List<Channel> channels)
|
||||
{
|
||||
AvailableChannels = channels;
|
||||
|
||||
Scheduler.Add(delegate
|
||||
{
|
||||
//todo: decide how to handle default channels for a user now that they are saved server-side.
|
||||
addChannel(channels.Find(c => c.Name == @"#lazer"));
|
||||
addChannel(channels.Find(c => c.Name == @"#osu"));
|
||||
|
||||
channelSelection.OnRequestJoin = addChannel;
|
||||
channelSelection.OnRequestLeave = removeChannel;
|
||||
channelSelection.Sections = new[]
|
||||
{
|
||||
new ChannelSection
|
||||
{
|
||||
Header = "All Channels",
|
||||
Channels = channels,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
messageRequest = Scheduler.AddDelayed(fetchUpdates, 1000, true);
|
||||
};
|
||||
|
||||
api.Queue(req);
|
||||
//for the case that channelmanager was faster at fetching the channels than our attachment to CollectionChanged.
|
||||
channelSelection.UpdateAvailableChannels(channelManager.AvailableChannels);
|
||||
joinedChannelsChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, channelManager.JoinedChannels));
|
||||
}
|
||||
|
||||
private Channel currentChannel;
|
||||
|
||||
protected Channel CurrentChannel
|
||||
private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
get { return currentChannel; }
|
||||
channelSelection.UpdateAvailableChannels(channelManager.AvailableChannels);
|
||||
}
|
||||
|
||||
set
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (channelManager != null)
|
||||
{
|
||||
if (currentChannel == value) return;
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
currentChannel = null;
|
||||
textbox.Current.Disabled = true;
|
||||
currentChannelContainer.Clear(false);
|
||||
return;
|
||||
}
|
||||
|
||||
currentChannel = value;
|
||||
|
||||
textbox.Current.Disabled = currentChannel.ReadOnly;
|
||||
channelTabs.Current.Value = value;
|
||||
|
||||
var loaded = loadedChannels.Find(d => d.Channel == value);
|
||||
if (loaded == null)
|
||||
{
|
||||
currentChannelContainer.FadeOut(500, Easing.OutQuint);
|
||||
loading.Show();
|
||||
|
||||
loaded = new DrawableChannel(currentChannel);
|
||||
loadedChannels.Add(loaded);
|
||||
LoadComponentAsync(loaded, l =>
|
||||
{
|
||||
if (currentChannel.MessagesLoaded)
|
||||
loading.Hide();
|
||||
|
||||
currentChannelContainer.Clear(false);
|
||||
currentChannelContainer.Add(loaded);
|
||||
currentChannelContainer.FadeIn(500, Easing.OutQuint);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
currentChannelContainer.Clear(false);
|
||||
currentChannelContainer.Add(loaded);
|
||||
}
|
||||
channelManager.CurrentChannel.ValueChanged -= currentChannelChanged;
|
||||
channelManager.JoinedChannels.CollectionChanged -= joinedChannelsChanged;
|
||||
channelManager.AvailableChannels.CollectionChanged -= availableChannelsChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void addChannel(Channel channel)
|
||||
{
|
||||
if (channel == null) return;
|
||||
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
var existing = careChannels.Find(c => c.Id == channel.Id);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
// if we already have this channel loaded, we don't want to make a second one.
|
||||
channel = existing;
|
||||
}
|
||||
else
|
||||
{
|
||||
careChannels.Add(channel);
|
||||
channelTabs.AddItem(channel);
|
||||
|
||||
if (channel.Type == ChannelType.Public && !channel.Joined)
|
||||
{
|
||||
var req = new JoinChannelRequest(channel, api.LocalUser);
|
||||
req.Success += () => addChannel(channel);
|
||||
req.Failure += ex => removeChannel(channel);
|
||||
api.Queue(req);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// let's fetch a small number of messages to bring us up-to-date with the backlog.
|
||||
fetchInitialMessages(channel);
|
||||
|
||||
if (CurrentChannel == null)
|
||||
CurrentChannel = channel;
|
||||
|
||||
channel.Joined.Value = true;
|
||||
}
|
||||
|
||||
private void removeChannel(Channel channel)
|
||||
{
|
||||
if (channel == null) return;
|
||||
|
||||
if (channel == CurrentChannel) CurrentChannel = null;
|
||||
|
||||
careChannels.Remove(channel);
|
||||
loadedChannels.Remove(loadedChannels.Find(c => c.Channel == channel));
|
||||
channelTabs.RemoveItem(channel);
|
||||
|
||||
api.Queue(new LeaveChannelRequest(channel, api.LocalUser));
|
||||
channel.Joined.Value = false;
|
||||
}
|
||||
|
||||
private void fetchInitialMessages(Channel channel)
|
||||
{
|
||||
var req = new GetMessagesRequest(channel);
|
||||
req.Success += messages =>
|
||||
{
|
||||
channel.AddNewMessages(messages.ToArray());
|
||||
if (channel == currentChannel)
|
||||
loading.Hide();
|
||||
};
|
||||
|
||||
api.Queue(req);
|
||||
}
|
||||
|
||||
private void fetchUpdates()
|
||||
{
|
||||
if (fetchReq != null) return;
|
||||
|
||||
fetchReq = new GetUpdatesRequest(lastMessageId);
|
||||
|
||||
fetchReq.Success += updates =>
|
||||
{
|
||||
if (updates?.Presence != null)
|
||||
{
|
||||
foreach (var channel in updates.Presence)
|
||||
addChannel(AvailableChannels.Find(c => c.Id == channel.Id));
|
||||
|
||||
foreach (var group in updates.Messages.GroupBy(m => m.ChannelId))
|
||||
careChannels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
|
||||
|
||||
lastMessageId = updates.Messages.LastOrDefault()?.Id ?? lastMessageId;
|
||||
}
|
||||
|
||||
fetchReq = null;
|
||||
};
|
||||
|
||||
fetchReq.Failure += delegate { fetchReq = null; };
|
||||
|
||||
api.Queue(fetchReq);
|
||||
}
|
||||
|
||||
private void postMessage(TextBox textbox, bool newText)
|
||||
{
|
||||
var postText = textbox.Text;
|
||||
var text = textbox.Text.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
if (text[0] == '/')
|
||||
channelManager.PostCommand(text.Substring(1));
|
||||
else
|
||||
channelManager.PostMessage(text);
|
||||
|
||||
textbox.Text = string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(postText))
|
||||
return;
|
||||
|
||||
var target = currentChannel;
|
||||
|
||||
if (target == null) return;
|
||||
|
||||
if (!api.IsLoggedIn)
|
||||
{
|
||||
target.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!"));
|
||||
return;
|
||||
}
|
||||
|
||||
bool isAction = false;
|
||||
|
||||
if (postText[0] == '/')
|
||||
{
|
||||
string[] parameters = postText.Substring(1).Split(new[] { ' ' }, 2);
|
||||
string command = parameters[0];
|
||||
string content = parameters.Length == 2 ? parameters[1] : string.Empty;
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "me":
|
||||
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
currentChannel.AddNewMessages(new ErrorMessage("Usage: /me [action]"));
|
||||
return;
|
||||
}
|
||||
|
||||
isAction = true;
|
||||
postText = content;
|
||||
break;
|
||||
|
||||
case "help":
|
||||
currentChannel.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action]"));
|
||||
return;
|
||||
|
||||
default:
|
||||
currentChannel.AddNewMessages(new ErrorMessage($@"""/{command}"" is not supported! For a list of supported commands see /help"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var message = new LocalEchoMessage
|
||||
{
|
||||
Sender = api.LocalUser.Value,
|
||||
Timestamp = DateTimeOffset.Now,
|
||||
ChannelId = target.Id,
|
||||
IsAction = isAction,
|
||||
Content = postText
|
||||
};
|
||||
|
||||
var req = new PostMessageRequest(message);
|
||||
|
||||
target.AddLocalEcho(message);
|
||||
req.Failure += e => target.ReplaceMessage(message, null);
|
||||
req.Success += m => target.ReplaceMessage(message, m);
|
||||
|
||||
api.Queue(req);
|
||||
}
|
||||
|
||||
private class TabsArea : Container
|
||||
|
@ -245,10 +245,10 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (current?.TrackLoaded ?? false)
|
||||
{
|
||||
var track = current.Track;
|
||||
var track = current?.TrackLoaded ?? false ? current.Track : null;
|
||||
|
||||
if (track?.IsDummyDevice == false)
|
||||
{
|
||||
progressBar.EndTime = track.Length;
|
||||
progressBar.CurrentTime = track.CurrentTime;
|
||||
|
||||
@ -258,7 +258,11 @@ namespace osu.Game.Overlays
|
||||
next();
|
||||
}
|
||||
else
|
||||
{
|
||||
progressBar.CurrentTime = 0;
|
||||
progressBar.EndTime = 1;
|
||||
playButton.Icon = FontAwesome.fa_play_circle_o;
|
||||
}
|
||||
}
|
||||
|
||||
private void play()
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
layerBelowRuleset.Child = new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
var layerAboveRuleset = CreateLayerContainer();
|
||||
layerAboveRuleset.Child = new BlueprintContainer();
|
||||
layerAboveRuleset.Child = blueprintContainer = new BlueprintContainer();
|
||||
|
||||
layerContainers.Add(layerBelowRuleset);
|
||||
layerContainers.Add(layerAboveRuleset);
|
||||
|
@ -50,9 +50,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
X = position.X,
|
||||
NewCombo = FirstObject || newCombo,
|
||||
ComboOffset = comboOffset,
|
||||
ControlPoints = controlPoints,
|
||||
Distance = length,
|
||||
PathType = pathType,
|
||||
Path = new SliderPath(pathType, controlPoints, length),
|
||||
NodeSamples = nodeSamples,
|
||||
RepeatCount = repeatCount
|
||||
};
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using System.Collections.Generic;
|
||||
using OpenTK;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@ -20,11 +19,9 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
/// <summary>
|
||||
/// <see cref="ConvertSlider"/>s don't need a curve since they're converted to ruleset-specific hitobjects.
|
||||
/// </summary>
|
||||
public SliderPath Path { get; } = null;
|
||||
public Vector2[] ControlPoints { get; set; }
|
||||
public PathType PathType { get; set; }
|
||||
public SliderPath Path { get; set; }
|
||||
|
||||
public double Distance { get; set; }
|
||||
public double Distance => Path.Distance;
|
||||
|
||||
public List<List<SampleInfo>> NodeSamples { get; set; }
|
||||
public int RepeatCount { get; set; }
|
||||
|
@ -31,9 +31,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
||||
return new ConvertSlider
|
||||
{
|
||||
X = position.X,
|
||||
ControlPoints = controlPoints,
|
||||
Distance = length,
|
||||
PathType = pathType,
|
||||
Path = new SliderPath(pathType, controlPoints, length),
|
||||
NodeSamples = nodeSamples,
|
||||
RepeatCount = repeatCount
|
||||
};
|
||||
|
@ -51,9 +51,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||
Position = position,
|
||||
NewCombo = FirstObject || newCombo,
|
||||
ComboOffset = comboOffset,
|
||||
ControlPoints = controlPoints,
|
||||
Distance = Math.Max(0, length),
|
||||
PathType = pathType,
|
||||
Path = new SliderPath(pathType, controlPoints, Math.Max(0, length)),
|
||||
NodeSamples = nodeSamples,
|
||||
RepeatCount = repeatCount
|
||||
};
|
||||
|
@ -27,9 +27,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
||||
{
|
||||
return new ConvertSlider
|
||||
{
|
||||
ControlPoints = controlPoints,
|
||||
Distance = length,
|
||||
PathType = pathType,
|
||||
Path = new SliderPath(pathType, controlPoints, length),
|
||||
NodeSamples = nodeSamples,
|
||||
RepeatCount = repeatCount
|
||||
};
|
||||
|
@ -4,28 +4,140 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
public class SliderPath
|
||||
public struct SliderPath : IEquatable<SliderPath>
|
||||
{
|
||||
public double Distance;
|
||||
/// <summary>
|
||||
/// The user-set distance of the path. If non-null, <see cref="Distance"/> will match this value,
|
||||
/// and the path will be shortened/lengthened to match this length.
|
||||
/// </summary>
|
||||
public readonly double? ExpectedDistance;
|
||||
|
||||
public Vector2[] ControlPoints = Array.Empty<Vector2>();
|
||||
/// <summary>
|
||||
/// The type of path.
|
||||
/// </summary>
|
||||
public readonly PathType Type;
|
||||
|
||||
public PathType PathType = PathType.PerfectCurve;
|
||||
[JsonProperty]
|
||||
private Vector2[] controlPoints;
|
||||
|
||||
public Vector2 Offset;
|
||||
private List<Vector2> calculatedPath;
|
||||
private List<double> cumulativeLength;
|
||||
|
||||
private readonly List<Vector2> calculatedPath = new List<Vector2>();
|
||||
private readonly List<double> cumulativeLength = new List<double>();
|
||||
private bool isInitialised;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SliderPath"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The type of path.</param>
|
||||
/// <param name="controlPoints">The control points of the path.</param>
|
||||
/// <param name="expectedDistance">A user-set distance of the path that may be shorter or longer than the true distance between all
|
||||
/// <paramref name="controlPoints"/>. The path will be shortened/lengthened to match this length.
|
||||
/// If null, the path will use the true distance between all <paramref name="controlPoints"/>.</param>
|
||||
[JsonConstructor]
|
||||
public SliderPath(PathType type, Vector2[] controlPoints, double? expectedDistance = null)
|
||||
{
|
||||
this = default;
|
||||
this.controlPoints = controlPoints;
|
||||
|
||||
Type = type;
|
||||
ExpectedDistance = expectedDistance;
|
||||
|
||||
ensureInitialised();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The control points of the path.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public ReadOnlySpan<Vector2> ControlPoints
|
||||
{
|
||||
get
|
||||
{
|
||||
ensureInitialised();
|
||||
return controlPoints.AsSpan();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The distance of the path after lengthening/shortening to account for <see cref="ExpectedDistance"/>.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public double Distance
|
||||
{
|
||||
get
|
||||
{
|
||||
ensureInitialised();
|
||||
return cumulativeLength.Count == 0 ? 0 : cumulativeLength[cumulativeLength.Count - 1];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
|
||||
/// to 1 (end of the slider) and stores the generated path in the given list.
|
||||
/// </summary>
|
||||
/// <param name="path">The list to be filled with the computed path.</param>
|
||||
/// <param name="p0">Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
|
||||
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
|
||||
public void GetPathToProgress(List<Vector2> path, double p0, double p1)
|
||||
{
|
||||
ensureInitialised();
|
||||
|
||||
double d0 = progressToDistance(p0);
|
||||
double d1 = progressToDistance(p1);
|
||||
|
||||
path.Clear();
|
||||
|
||||
int i = 0;
|
||||
for (; i < calculatedPath.Count && cumulativeLength[i] < d0; ++i)
|
||||
{
|
||||
}
|
||||
|
||||
path.Add(interpolateVertices(i, d0));
|
||||
|
||||
for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i)
|
||||
path.Add(calculatedPath[i]);
|
||||
|
||||
path.Add(interpolateVertices(i, d1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the position on the slider at a given progress that ranges from 0 (beginning of the path)
|
||||
/// to 1 (end of the path).
|
||||
/// </summary>
|
||||
/// <param name="progress">Ranges from 0 (beginning of the path) to 1 (end of the path).</param>
|
||||
/// <returns></returns>
|
||||
public Vector2 PositionAt(double progress)
|
||||
{
|
||||
ensureInitialised();
|
||||
|
||||
double d = progressToDistance(progress);
|
||||
return interpolateVertices(indexOfDistance(d), d);
|
||||
}
|
||||
|
||||
private void ensureInitialised()
|
||||
{
|
||||
if (isInitialised)
|
||||
return;
|
||||
isInitialised = true;
|
||||
|
||||
controlPoints = controlPoints ?? Array.Empty<Vector2>();
|
||||
calculatedPath = new List<Vector2>();
|
||||
cumulativeLength = new List<double>();
|
||||
|
||||
calculatePath();
|
||||
calculateCumulativeLength();
|
||||
}
|
||||
|
||||
private List<Vector2> calculateSubpath(ReadOnlySpan<Vector2> subControlPoints)
|
||||
{
|
||||
switch (PathType)
|
||||
switch (Type)
|
||||
{
|
||||
case PathType.Linear:
|
||||
return PathApproximator.ApproximateLinear(subControlPoints);
|
||||
@ -66,7 +178,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
|
||||
if (i == ControlPoints.Length - 1 || ControlPoints[i] == ControlPoints[i + 1])
|
||||
{
|
||||
ReadOnlySpan<Vector2> cpSpan = ControlPoints.AsSpan().Slice(start, end - start);
|
||||
ReadOnlySpan<Vector2> cpSpan = ControlPoints.Slice(start, end - start);
|
||||
|
||||
foreach (Vector2 t in calculateSubpath(cpSpan))
|
||||
if (calculatedPath.Count == 0 || calculatedPath.Last() != t)
|
||||
@ -77,49 +189,6 @@ namespace osu.Game.Rulesets.Objects
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateCumulativeLengthAndTrimPath()
|
||||
{
|
||||
double l = 0;
|
||||
|
||||
cumulativeLength.Clear();
|
||||
cumulativeLength.Add(l);
|
||||
|
||||
for (int i = 0; i < calculatedPath.Count - 1; ++i)
|
||||
{
|
||||
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
|
||||
double d = diff.Length;
|
||||
|
||||
// Shorten slider paths that are too long compared to what's
|
||||
// in the .osu file.
|
||||
if (Distance - l < d)
|
||||
{
|
||||
calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((Distance - l) / d);
|
||||
calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i);
|
||||
|
||||
l = Distance;
|
||||
cumulativeLength.Add(l);
|
||||
break;
|
||||
}
|
||||
|
||||
l += d;
|
||||
cumulativeLength.Add(l);
|
||||
}
|
||||
|
||||
// Lengthen slider paths that are too short compared to what's
|
||||
// in the .osu file.
|
||||
if (l < Distance && calculatedPath.Count > 1)
|
||||
{
|
||||
Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2];
|
||||
double d = diff.Length;
|
||||
|
||||
if (d <= 0)
|
||||
return;
|
||||
|
||||
calculatedPath[calculatedPath.Count - 1] += diff * (float)((Distance - l) / d);
|
||||
cumulativeLength[calculatedPath.Count - 1] = Distance;
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateCumulativeLength()
|
||||
{
|
||||
double l = 0;
|
||||
@ -132,21 +201,33 @@ namespace osu.Game.Rulesets.Objects
|
||||
Vector2 diff = calculatedPath[i + 1] - calculatedPath[i];
|
||||
double d = diff.Length;
|
||||
|
||||
// Shorted slider paths that are too long compared to the expected distance
|
||||
if (ExpectedDistance.HasValue && ExpectedDistance - l < d)
|
||||
{
|
||||
calculatedPath[i + 1] = calculatedPath[i] + diff * (float)((ExpectedDistance - l) / d);
|
||||
calculatedPath.RemoveRange(i + 2, calculatedPath.Count - 2 - i);
|
||||
|
||||
l = ExpectedDistance.Value;
|
||||
cumulativeLength.Add(l);
|
||||
break;
|
||||
}
|
||||
|
||||
l += d;
|
||||
cumulativeLength.Add(l);
|
||||
}
|
||||
|
||||
Distance = l;
|
||||
}
|
||||
// Lengthen slider paths that are too short compared to the expected distance
|
||||
if (ExpectedDistance.HasValue && l < ExpectedDistance && calculatedPath.Count > 1)
|
||||
{
|
||||
Vector2 diff = calculatedPath[calculatedPath.Count - 1] - calculatedPath[calculatedPath.Count - 2];
|
||||
double d = diff.Length;
|
||||
|
||||
public void Calculate(bool updateDistance = false)
|
||||
{
|
||||
calculatePath();
|
||||
if (d <= 0)
|
||||
return;
|
||||
|
||||
if (!updateDistance)
|
||||
calculateCumulativeLengthAndTrimPath();
|
||||
else
|
||||
calculateCumulativeLength();
|
||||
calculatedPath[calculatedPath.Count - 1] += diff * (float)((ExpectedDistance - l) / d);
|
||||
cumulativeLength[calculatedPath.Count - 1] = ExpectedDistance.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private int indexOfDistance(double d)
|
||||
@ -169,7 +250,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
|
||||
if (i <= 0)
|
||||
return calculatedPath.First();
|
||||
else if (i >= calculatedPath.Count)
|
||||
if (i >= calculatedPath.Count)
|
||||
return calculatedPath.Last();
|
||||
|
||||
Vector2 p0 = calculatedPath[i - 1];
|
||||
@ -186,47 +267,20 @@ namespace osu.Game.Rulesets.Objects
|
||||
return p0 + (p1 - p0) * (float)w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the slider path until a given progress that ranges from 0 (beginning of the slider)
|
||||
/// to 1 (end of the slider) and stores the generated path in the given list.
|
||||
/// </summary>
|
||||
/// <param name="path">The list to be filled with the computed path.</param>
|
||||
/// <param name="p0">Start progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
|
||||
/// <param name="p1">End progress. Ranges from 0 (beginning of the slider) to 1 (end of the slider).</param>
|
||||
public void GetPathToProgress(List<Vector2> path, double p0, double p1)
|
||||
public bool Equals(SliderPath other)
|
||||
{
|
||||
if (calculatedPath.Count == 0 && ControlPoints.Length > 0)
|
||||
Calculate();
|
||||
if (ControlPoints == null && other.ControlPoints != null)
|
||||
return false;
|
||||
if (other.ControlPoints == null && ControlPoints != null)
|
||||
return false;
|
||||
|
||||
double d0 = progressToDistance(p0);
|
||||
double d1 = progressToDistance(p1);
|
||||
|
||||
path.Clear();
|
||||
|
||||
int i = 0;
|
||||
for (; i < calculatedPath.Count && cumulativeLength[i] < d0; ++i) { }
|
||||
|
||||
path.Add(interpolateVertices(i, d0) + Offset);
|
||||
|
||||
for (; i < calculatedPath.Count && cumulativeLength[i] <= d1; ++i)
|
||||
path.Add(calculatedPath[i] + Offset);
|
||||
|
||||
path.Add(interpolateVertices(i, d1) + Offset);
|
||||
return ControlPoints.SequenceEqual(other.ControlPoints) && ExpectedDistance.Equals(other.ExpectedDistance) && Type == other.Type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the position on the slider at a given progress that ranges from 0 (beginning of the path)
|
||||
/// to 1 (end of the path).
|
||||
/// </summary>
|
||||
/// <param name="progress">Ranges from 0 (beginning of the path) to 1 (end of the path).</param>
|
||||
/// <returns></returns>
|
||||
public Vector2 PositionAt(double progress)
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (calculatedPath.Count == 0 && ControlPoints.Length > 0)
|
||||
Calculate();
|
||||
|
||||
double d = progressToDistance(progress);
|
||||
return interpolateVertices(indexOfDistance(d), d) + Offset;
|
||||
if (ReferenceEquals(null, obj)) return false;
|
||||
return obj is SliderPath other && Equals(other);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,16 +14,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
/// The curve.
|
||||
/// </summary>
|
||||
SliderPath Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The control points that shape the curve.
|
||||
/// </summary>
|
||||
Vector2[] ControlPoints { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of curve.
|
||||
/// </summary>
|
||||
PathType PathType { get; }
|
||||
}
|
||||
|
||||
public static class HasCurveExtensions
|
||||
|
@ -0,0 +1,27 @@
|
||||
// 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.UI.Scrolling.Algorithms
|
||||
{
|
||||
public class ConstantScrollAlgorithm : IScrollAlgorithm
|
||||
{
|
||||
public double GetDisplayStartTime(double time, double timeRange) => time - timeRange;
|
||||
|
||||
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
|
||||
{
|
||||
// At the hitobject's end time, the hitobject will be positioned such that its end rests at the origin.
|
||||
// This results in a negative-position value, and the absolute of it indicates the length of the hitobject.
|
||||
return -PositionAt(startTime, endTime, timeRange, scrollLength);
|
||||
}
|
||||
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
|
||||
=> (float)((time - currentTime) / timeRange * scrollLength);
|
||||
|
||||
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
|
||||
=> position * timeRange / scrollLength + currentTime;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
// 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.UI.Scrolling.Algorithms
|
||||
{
|
||||
public interface IScrollAlgorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// Given a point in time, computes the time at which it enters the time range.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// E.g. For a constant time range of 5000ms, the time at which t=7000ms enters the time range is 2000ms.
|
||||
/// </remarks>
|
||||
/// <param name="time">The point in time.</param>
|
||||
/// <param name="timeRange">The amount of visible time.</param>
|
||||
/// <returns>The time at which <paramref name="time"/> enters <see cref="timeRange"/>.</returns>
|
||||
double GetDisplayStartTime(double time, double timeRange);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the spatial length within a start and end time.
|
||||
/// </summary>
|
||||
/// <param name="startTime">The start time.</param>
|
||||
/// <param name="endTime">The end time.</param>
|
||||
/// <param name="timeRange">The amount of visible time.</param>
|
||||
/// <param name="scrollLength">The absolute spatial length through <see cref="timeRange"/>.</param>
|
||||
/// <returns>The absolute spatial length.</returns>
|
||||
float GetLength(double startTime, double endTime, double timeRange, float scrollLength);
|
||||
|
||||
/// <summary>
|
||||
/// Given the current time, computes the spatial position of a point in time.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to compute the spatial position of.</param>
|
||||
/// <param name="currentTime">The current time.</param>
|
||||
/// <param name="timeRange">The amount of visible time.</param>
|
||||
/// <param name="scrollLength">The absolute spatial length through <see cref="timeRange"/>.</param>
|
||||
/// <returns>The absolute spatial position.</returns>
|
||||
float PositionAt(double time, double currentTime, double timeRange, float scrollLength);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the time which brings a point to a provided spatial position given the current time.
|
||||
/// </summary>
|
||||
/// <param name="position">The absolute spatial position.</param>
|
||||
/// <param name="currentTime">The current time.</param>
|
||||
/// <param name="timeRange">The amount of visible time.</param>
|
||||
/// <param name="scrollLength">The absolute spatial length through <see cref="timeRange"/>.</param>
|
||||
/// <returns>The time at which <see cref="PositionAt(t)"/> == <paramref name="position"/>.</returns>
|
||||
double TimeAt(float position, double currentTime, double timeRange, float scrollLength);
|
||||
|
||||
/// <summary>
|
||||
/// Resets this <see cref="IScrollAlgorithm"/> to a default state.
|
||||
/// </summary>
|
||||
void Reset();
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
// 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.Lists;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
{
|
||||
public class OverlappingScrollAlgorithm : IScrollAlgorithm
|
||||
{
|
||||
private readonly MultiplierControlPoint searchPoint;
|
||||
|
||||
private readonly SortedList<MultiplierControlPoint> controlPoints;
|
||||
|
||||
public OverlappingScrollAlgorithm(SortedList<MultiplierControlPoint> controlPoints)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
|
||||
searchPoint = new MultiplierControlPoint();
|
||||
}
|
||||
|
||||
public double GetDisplayStartTime(double time, double timeRange)
|
||||
{
|
||||
// The total amount of time that the hitobject will remain visible within the timeRange, which decreases as the speed multiplier increases
|
||||
double visibleDuration = timeRange / controlPointAt(time).Multiplier;
|
||||
return time - visibleDuration;
|
||||
}
|
||||
|
||||
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
|
||||
{
|
||||
// At the hitobject's end time, the hitobject will be positioned such that its end rests at the origin.
|
||||
// This results in a negative-position value, and the absolute of it indicates the length of the hitobject.
|
||||
return -PositionAt(startTime, endTime, timeRange, scrollLength);
|
||||
}
|
||||
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
|
||||
=> (float)((time - currentTime) / timeRange * controlPointAt(time).Multiplier * scrollLength);
|
||||
|
||||
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
|
||||
{
|
||||
// Find the control point relating to the position.
|
||||
// Note: Due to velocity adjustments, overlapping control points will provide multiple valid time values for a single position
|
||||
// As such, this operation provides unexpected results by using the latter of the control points.
|
||||
|
||||
int i = 0;
|
||||
float pos = 0;
|
||||
|
||||
for (; i < controlPoints.Count; i++)
|
||||
{
|
||||
float lastPos = pos;
|
||||
pos = PositionAt(controlPoints[i].StartTime, currentTime, timeRange, scrollLength);
|
||||
|
||||
if (pos > position)
|
||||
{
|
||||
i--;
|
||||
pos = lastPos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i = MathHelper.Clamp(i, 0, controlPoints.Count - 1);
|
||||
|
||||
return controlPoints[i].StartTime + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the <see cref="MultiplierControlPoint"/> which affects the speed of hitobjects at a specific time.
|
||||
/// </summary>
|
||||
/// <param name="time">The time which the <see cref="MultiplierControlPoint"/> should affect.</param>
|
||||
/// <returns>The <see cref="MultiplierControlPoint"/>.</returns>
|
||||
private MultiplierControlPoint controlPointAt(double time)
|
||||
{
|
||||
if (controlPoints.Count == 0)
|
||||
return new MultiplierControlPoint(double.NegativeInfinity);
|
||||
|
||||
if (time < controlPoints[0].StartTime)
|
||||
return controlPoints[0];
|
||||
|
||||
searchPoint.StartTime = time;
|
||||
int index = controlPoints.BinarySearch(searchPoint);
|
||||
|
||||
if (index < 0)
|
||||
index = ~index - 1;
|
||||
|
||||
return controlPoints[index];
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
// 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 osu.Game.Rulesets.Timing;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
|
||||
{
|
||||
public class SequentialScrollAlgorithm : IScrollAlgorithm
|
||||
{
|
||||
private readonly Dictionary<double, double> positionCache;
|
||||
|
||||
private readonly IReadOnlyList<MultiplierControlPoint> controlPoints;
|
||||
|
||||
public SequentialScrollAlgorithm(IReadOnlyList<MultiplierControlPoint> controlPoints)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
|
||||
positionCache = new Dictionary<double, double>();
|
||||
}
|
||||
|
||||
public double GetDisplayStartTime(double time, double timeRange) => time - timeRange - 1000;
|
||||
|
||||
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
|
||||
{
|
||||
var objectLength = relativePositionAtCached(endTime, timeRange) - relativePositionAtCached(startTime, timeRange);
|
||||
return (float)(objectLength * scrollLength);
|
||||
}
|
||||
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
|
||||
{
|
||||
// Caching is not used here as currentTime is unlikely to have been previously cached
|
||||
double timelinePosition = relativePositionAt(currentTime, timeRange);
|
||||
return (float)((relativePositionAtCached(time, timeRange) - timelinePosition) * scrollLength);
|
||||
}
|
||||
|
||||
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
|
||||
{
|
||||
// Convert the position to a length relative to time = 0
|
||||
double length = position / scrollLength + relativePositionAt(currentTime, timeRange);
|
||||
|
||||
// We need to consider all timing points until the specified time and not just the currently-active one,
|
||||
// since each timing point individually affects the positions of _all_ hitobjects after its start time
|
||||
for (int i = 0; i < controlPoints.Count; i++)
|
||||
{
|
||||
var current = controlPoints[i];
|
||||
var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null;
|
||||
|
||||
// Duration of the current control point
|
||||
var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime;
|
||||
|
||||
// Figure out the length of control point
|
||||
var currentLength = currentDuration / timeRange * current.Multiplier;
|
||||
|
||||
if (currentLength > length)
|
||||
{
|
||||
// The point is within this control point
|
||||
return current.StartTime + length * timeRange / current.Multiplier;
|
||||
}
|
||||
|
||||
length -= currentLength;
|
||||
}
|
||||
|
||||
return 0; // Should never occur
|
||||
}
|
||||
|
||||
private double relativePositionAtCached(double time, double timeRange)
|
||||
{
|
||||
if (!positionCache.TryGetValue(time, out double existing))
|
||||
positionCache[time] = existing = relativePositionAt(time, timeRange);
|
||||
return existing;
|
||||
}
|
||||
|
||||
public void Reset() => positionCache.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Finds the position which corresponds to a point in time.
|
||||
/// This is a non-linear operation that depends on all the control points up to and including the one active at the time value.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the position at.</param>
|
||||
/// <param name="timeRange">The amount of time visualised by the scrolling area.</param>
|
||||
/// <returns>A positive value indicating the position at <paramref name="time"/>.</returns>
|
||||
private double relativePositionAt(double time, double timeRange)
|
||||
{
|
||||
if (controlPoints.Count == 0)
|
||||
return time / timeRange;
|
||||
|
||||
double length = 0;
|
||||
|
||||
// We need to consider all timing points until the specified time and not just the currently-active one,
|
||||
// since each timing point individually affects the positions of _all_ hitobjects after its start time
|
||||
for (int i = 0; i < controlPoints.Count; i++)
|
||||
{
|
||||
var current = controlPoints[i];
|
||||
var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null;
|
||||
|
||||
// We don't need to consider any control points beyond the current time, since it will not yet
|
||||
// affect any hitobjects
|
||||
if (i > 0 && current.StartTime > time)
|
||||
continue;
|
||||
|
||||
// Duration of the current control point
|
||||
var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime;
|
||||
|
||||
// We want to consider the minimal amount of time that this control point has affected,
|
||||
// which may be either its duration, or the amount of time that has passed within it
|
||||
var durationInCurrent = Math.Min(currentDuration, time - current.StartTime);
|
||||
|
||||
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
|
||||
length += durationInCurrent / timeRange * current.Multiplier;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,9 +3,9 @@
|
||||
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.UI
|
||||
namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
public interface IScrollingInfo
|
||||
{
|
||||
@ -13,5 +13,15 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
/// The direction <see cref="HitObject"/>s should scroll in.
|
||||
/// </summary>
|
||||
IBindable<ScrollingDirection> Direction { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
IBindable<double> TimeRange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The algorithm which controls <see cref="HitObject"/> positions and sizes.
|
||||
/// </summary>
|
||||
IScrollAlgorithm Algorithm { get; }
|
||||
}
|
||||
}
|
@ -1,58 +1,39 @@
|
||||
// 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.Caching;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI.Scrolling.Visualisers;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
public class ScrollingHitObjectContainer : HitObjectContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// The duration required to scroll through one length of the <see cref="ScrollingHitObjectContainer"/> before any control point adjustments.
|
||||
/// </summary>
|
||||
public readonly BindableDouble TimeRange = new BindableDouble
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = double.MaxValue
|
||||
};
|
||||
private readonly IBindable<double> timeRange = new BindableDouble();
|
||||
|
||||
/// <summary>
|
||||
/// The control points that adjust the scrolling speed.
|
||||
/// </summary>
|
||||
protected readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>();
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
|
||||
private Cached initialStateCache = new Cached();
|
||||
|
||||
private readonly ISpeedChangeVisualiser speedChangeVisualiser;
|
||||
|
||||
public ScrollingHitObjectContainer(SpeedChangeVisualisationMethod visualisationMethod)
|
||||
public ScrollingHitObjectContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
TimeRange.ValueChanged += _ => initialStateCache.Invalidate();
|
||||
Direction.ValueChanged += _ => initialStateCache.Invalidate();
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
direction.BindTo(scrollingInfo.Direction);
|
||||
timeRange.BindTo(scrollingInfo.TimeRange);
|
||||
|
||||
switch (visualisationMethod)
|
||||
{
|
||||
case SpeedChangeVisualisationMethod.Sequential:
|
||||
speedChangeVisualiser = new SequentialSpeedChangeVisualiser(ControlPoints);
|
||||
break;
|
||||
case SpeedChangeVisualisationMethod.Overlapping:
|
||||
speedChangeVisualiser = new OverlappingSpeedChangeVisualiser(ControlPoints);
|
||||
break;
|
||||
case SpeedChangeVisualisationMethod.Constant:
|
||||
speedChangeVisualiser = new ConstantSpeedChangeVisualiser();
|
||||
break;
|
||||
}
|
||||
direction.ValueChanged += _ => initialStateCache.Invalidate();
|
||||
timeRange.ValueChanged += _ => initialStateCache.Invalidate();
|
||||
}
|
||||
|
||||
public override void Add(DrawableHitObject hitObject)
|
||||
@ -69,20 +50,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
return result;
|
||||
}
|
||||
|
||||
public void AddControlPoint(MultiplierControlPoint controlPoint)
|
||||
{
|
||||
ControlPoints.Add(controlPoint);
|
||||
initialStateCache.Invalidate();
|
||||
}
|
||||
|
||||
public bool RemoveControlPoint(MultiplierControlPoint controlPoint)
|
||||
{
|
||||
var result = ControlPoints.Remove(controlPoint);
|
||||
if (result)
|
||||
initialStateCache.Invalidate();
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
|
||||
{
|
||||
if ((invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo)) > 0)
|
||||
@ -91,23 +58,87 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
return base.Invalidate(invalidation, source, shallPropagate);
|
||||
}
|
||||
|
||||
private float scrollLength;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!initialStateCache.IsValid)
|
||||
{
|
||||
speedChangeVisualiser.ComputeInitialStates(Objects, Direction, TimeRange, DrawSize);
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
scrollLength = DrawSize.Y;
|
||||
break;
|
||||
default:
|
||||
scrollLength = DrawSize.X;
|
||||
break;
|
||||
}
|
||||
|
||||
scrollingInfo.Algorithm.Reset();
|
||||
|
||||
foreach (var obj in Objects)
|
||||
computeInitialStateRecursive(obj);
|
||||
initialStateCache.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
private void computeInitialStateRecursive(DrawableHitObject hitObject)
|
||||
{
|
||||
hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value);
|
||||
|
||||
if (hitObject.HitObject is IHasEndTime endTime)
|
||||
{
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
case ScrollingDirection.Right:
|
||||
hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, endTime.EndTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var obj in hitObject.NestedHitObjects)
|
||||
{
|
||||
computeInitialStateRecursive(obj);
|
||||
|
||||
// Nested hitobjects don't need to scroll, but they do need accurate positions
|
||||
updatePosition(obj, hitObject.HitObject.StartTime);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildrenLife()
|
||||
{
|
||||
base.UpdateAfterChildrenLife();
|
||||
|
||||
// We need to calculate this as soon as possible after lifetimes so that hitobjects get the final say in their positions
|
||||
speedChangeVisualiser.UpdatePositions(AliveObjects, Direction, Time.Current, TimeRange, DrawSize);
|
||||
// We need to calculate hitobject positions as soon as possible after lifetimes so that hitobjects get the final say in their positions
|
||||
foreach (var obj in AliveObjects)
|
||||
updatePosition(obj, Time.Current);
|
||||
}
|
||||
|
||||
private void updatePosition(DrawableHitObject hitObject, double currentTime)
|
||||
{
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
hitObject.Y = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
case ScrollingDirection.Down:
|
||||
hitObject.Y = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
hitObject.X = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
case ScrollingDirection.Right:
|
||||
hitObject.X = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,6 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling
|
||||
@ -14,88 +10,19 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// <summary>
|
||||
/// A type of <see cref="Playfield"/> specialized towards scrolling <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public abstract class ScrollingPlayfield : Playfield, IKeyBindingHandler<GlobalAction>
|
||||
public abstract class ScrollingPlayfield : Playfield
|
||||
{
|
||||
/// <summary>
|
||||
/// The default span of time visible by the length of the scrolling axes.
|
||||
/// This is clamped between <see cref="time_span_min"/> and <see cref="time_span_max"/>.
|
||||
/// </summary>
|
||||
private const double time_span_default = 1500;
|
||||
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
/// <summary>
|
||||
/// The minimum span of time that may be visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_min = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum span of time that may be visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_max = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_step = 200;
|
||||
|
||||
/// <summary>
|
||||
/// The span of time that is visible by the length of the scrolling axes.
|
||||
/// For example, only hit objects with start time less than or equal to 1000 will be visible with <see cref="VisibleTimeRange"/> = 1000.
|
||||
/// </summary>
|
||||
public readonly BindableDouble VisibleTimeRange = new BindableDouble(time_span_default)
|
||||
{
|
||||
Default = time_span_default,
|
||||
MinValue = time_span_min,
|
||||
MaxValue = time_span_max
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether the player can change <see cref="VisibleTimeRange"/>.
|
||||
/// </summary>
|
||||
protected virtual bool UserScrollSpeedAdjustment => true;
|
||||
|
||||
/// <summary>
|
||||
/// The container that contains the <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)HitObjectContainer;
|
||||
|
||||
/// <summary>
|
||||
/// The direction in which <see cref="DrawableHitObject"/>s in this <see cref="ScrollingPlayfield"/> should scroll.
|
||||
/// </summary>
|
||||
protected readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
protected virtual SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Sequential;
|
||||
[Resolved]
|
||||
private IScrollingInfo scrollingInfo { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
HitObjects.TimeRange.BindTo(VisibleTimeRange);
|
||||
Direction.BindTo(scrollingInfo.Direction);
|
||||
}
|
||||
|
||||
public bool OnPressed(GlobalAction action)
|
||||
{
|
||||
if (!UserScrollSpeedAdjustment)
|
||||
return false;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case GlobalAction.IncreaseScrollSpeed:
|
||||
this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 200, Easing.OutQuint);
|
||||
return true;
|
||||
case GlobalAction.DecreaseScrollSpeed:
|
||||
this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 200, Easing.OutQuint);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool OnReleased(GlobalAction action) => false;
|
||||
|
||||
protected sealed override HitObjectContainer CreateHitObjectContainer()
|
||||
{
|
||||
var container = new ScrollingHitObjectContainer(VisualisationMethod);
|
||||
container.Direction.BindTo(Direction);
|
||||
return container;
|
||||
}
|
||||
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
|
||||
}
|
||||
}
|
||||
|
@ -4,13 +4,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
@ -18,20 +23,82 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// A type of <see cref="RulesetContainer{TPlayfield,TObject}"/> that supports a <see cref="ScrollingPlayfield"/>.
|
||||
/// <see cref="HitObject"/>s inside this <see cref="RulesetContainer{TPlayfield,TObject}"/> will scroll within the playfield.
|
||||
/// </summary>
|
||||
public abstract class ScrollingRulesetContainer<TPlayfield, TObject> : RulesetContainer<TPlayfield, TObject>
|
||||
public abstract class ScrollingRulesetContainer<TPlayfield, TObject> : RulesetContainer<TPlayfield, TObject>, IKeyBindingHandler<GlobalAction>
|
||||
where TObject : HitObject
|
||||
where TPlayfield : ScrollingPlayfield
|
||||
{
|
||||
/// <summary>
|
||||
/// The default span of time visible by the length of the scrolling axes.
|
||||
/// This is clamped between <see cref="time_span_min"/> and <see cref="time_span_max"/>.
|
||||
/// </summary>
|
||||
private const double time_span_default = 1500;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum span of time that may be visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_min = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum span of time that may be visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_max = 10000;
|
||||
|
||||
/// <summary>
|
||||
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_step = 200;
|
||||
|
||||
protected readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
/// <summary>
|
||||
/// The span of time that is visible by the length of the scrolling axes.
|
||||
/// For example, only hit objects with start time less than or equal to 1000 will be visible with <see cref="TimeRange"/> = 1000.
|
||||
/// </summary>
|
||||
protected readonly BindableDouble TimeRange = new BindableDouble(time_span_default)
|
||||
{
|
||||
Default = time_span_default,
|
||||
MinValue = time_span_min,
|
||||
MaxValue = time_span_max
|
||||
};
|
||||
|
||||
protected virtual ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Sequential;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the player can change <see cref="VisibleTimeRange"/>.
|
||||
/// </summary>
|
||||
protected virtual bool UserScrollSpeedAdjustment => true;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the default <see cref="MultiplierControlPoint"/>s that adjust the scrolling rate of <see cref="HitObject"/>s
|
||||
/// inside this <see cref="RulesetContainer{TPlayfield,TObject}"/>.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected readonly SortedList<MultiplierControlPoint> DefaultControlPoints = new SortedList<MultiplierControlPoint>(Comparer<MultiplierControlPoint>.Default);
|
||||
private readonly SortedList<MultiplierControlPoint> controlPoints = new SortedList<MultiplierControlPoint>(Comparer<MultiplierControlPoint>.Default);
|
||||
|
||||
protected IScrollingInfo ScrollingInfo => scrollingInfo;
|
||||
|
||||
[Cached(Type = typeof(IScrollingInfo))]
|
||||
private readonly LocalScrollingInfo scrollingInfo;
|
||||
|
||||
protected ScrollingRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
scrollingInfo = new LocalScrollingInfo();
|
||||
scrollingInfo.Direction.BindTo(Direction);
|
||||
scrollingInfo.TimeRange.BindTo(TimeRange);
|
||||
|
||||
switch (VisualisationMethod)
|
||||
{
|
||||
case ScrollVisualisationMethod.Sequential:
|
||||
scrollingInfo.Algorithm = new SequentialScrollAlgorithm(controlPoints);
|
||||
break;
|
||||
case ScrollVisualisationMethod.Overlapping:
|
||||
scrollingInfo.Algorithm = new OverlappingScrollAlgorithm(controlPoints);
|
||||
break;
|
||||
case ScrollVisualisationMethod.Constant:
|
||||
scrollingInfo.Algorithm = new ConstantScrollAlgorithm();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -75,19 +142,40 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
// Collapse sections with the same start time
|
||||
.GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime);
|
||||
|
||||
DefaultControlPoints.AddRange(timingChanges);
|
||||
controlPoints.AddRange(timingChanges);
|
||||
|
||||
// If we have no control points, add a default one
|
||||
if (DefaultControlPoints.Count == 0)
|
||||
DefaultControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier });
|
||||
|
||||
DefaultControlPoints.ForEach(c => applySpeedAdjustment(c, Playfield));
|
||||
if (controlPoints.Count == 0)
|
||||
controlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier });
|
||||
}
|
||||
|
||||
private void applySpeedAdjustment(MultiplierControlPoint controlPoint, ScrollingPlayfield playfield)
|
||||
public bool OnPressed(GlobalAction action)
|
||||
{
|
||||
playfield.HitObjects.AddControlPoint(controlPoint);
|
||||
playfield.NestedPlayfields?.OfType<ScrollingPlayfield>().ForEach(p => applySpeedAdjustment(controlPoint, p));
|
||||
if (!UserScrollSpeedAdjustment)
|
||||
return false;
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case GlobalAction.IncreaseScrollSpeed:
|
||||
this.TransformBindableTo(TimeRange, TimeRange - time_span_step, 200, Easing.OutQuint);
|
||||
return true;
|
||||
case GlobalAction.DecreaseScrollSpeed:
|
||||
this.TransformBindableTo(TimeRange, TimeRange + time_span_step, 200, Easing.OutQuint);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool OnReleased(GlobalAction action) => false;
|
||||
|
||||
private class LocalScrollingInfo : IScrollingInfo
|
||||
{
|
||||
public IBindable<ScrollingDirection> Direction { get; } = new Bindable<ScrollingDirection>();
|
||||
|
||||
public IBindable<double> TimeRange { get; } = new BindableDouble();
|
||||
|
||||
public IScrollAlgorithm Algorithm { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +0,0 @@
|
||||
// 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.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
|
||||
{
|
||||
public class ConstantSpeedChangeVisualiser : ISpeedChangeVisualiser
|
||||
{
|
||||
public void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length)
|
||||
{
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
obj.LifetimeStart = obj.HitObject.StartTime - timeRange;
|
||||
|
||||
if (obj.HitObject is IHasEndTime endTime)
|
||||
{
|
||||
var hitObjectLength = (endTime.EndTime - obj.HitObject.StartTime) / timeRange;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
obj.Height = (float)(hitObjectLength * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
case ScrollingDirection.Right:
|
||||
obj.Width = (float)(hitObjectLength * length.X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length);
|
||||
|
||||
// Nested hitobjects don't need to scroll, but they do need accurate positions
|
||||
UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length)
|
||||
{
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
var position = (obj.HitObject.StartTime - currentTime) / timeRange;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
obj.Y = (float)(position * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Down:
|
||||
obj.Y = (float)(-position * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
obj.X = (float)(position * length.X);
|
||||
break;
|
||||
case ScrollingDirection.Right:
|
||||
obj.X = (float)(-position * length.X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
// 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.Game.Rulesets.Objects.Drawables;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
|
||||
{
|
||||
public interface ISpeedChangeVisualiser
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the states of <see cref="DrawableHitObject"/>s that remain constant while scrolling, such as lifetime and spatial length.
|
||||
/// This is invoked once whenever <paramref name="timeRange"/> or <paramref name="length"/> changes.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The <see cref="DrawableHitObject"/>s whose states should be computed.</param>
|
||||
/// <param name="direction">The scrolling direction.</param>
|
||||
/// <param name="timeRange">The duration required to scroll through one length of the screen before any speed adjustments.</param>
|
||||
/// <param name="length">The length of the screen that is scrolled through.</param>
|
||||
void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the positions of <see cref="DrawableHitObject"/>s, depending on the current time. This is invoked once per frame.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The <see cref="DrawableHitObject"/>s whose positions should be computed.</param>
|
||||
/// <param name="direction">The scrolling direction.</param>
|
||||
/// <param name="currentTime">The current time.</param>
|
||||
/// <param name="timeRange">The duration required to scroll through one length of the screen before any speed adjustments.</param>
|
||||
/// <param name="length">The length of the screen that is scrolled through.</param>
|
||||
void UpdatePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length);
|
||||
}
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
// 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.Lists;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
|
||||
{
|
||||
public class OverlappingSpeedChangeVisualiser : ISpeedChangeVisualiser
|
||||
{
|
||||
private readonly SortedList<MultiplierControlPoint> controlPoints;
|
||||
|
||||
public OverlappingSpeedChangeVisualiser(SortedList<MultiplierControlPoint> controlPoints)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
}
|
||||
|
||||
public void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length)
|
||||
{
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
// The total amount of time that the hitobject will remain visible within the timeRange, which decreases as the speed multiplier increases
|
||||
double visibleDuration = timeRange / controlPointAt(obj.HitObject.StartTime).Multiplier;
|
||||
|
||||
obj.LifetimeStart = obj.HitObject.StartTime - visibleDuration;
|
||||
|
||||
if (obj.HitObject is IHasEndTime endTime)
|
||||
{
|
||||
// At the hitobject's end time, the hitobject will be positioned such that its end rests at the origin.
|
||||
// This results in a negative-position value, and the absolute of it indicates the length of the hitobject.
|
||||
var hitObjectLength = -hitObjectPositionAt(obj, endTime.EndTime, timeRange);
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
obj.Height = (float)(hitObjectLength * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
case ScrollingDirection.Right:
|
||||
obj.Width = (float)(hitObjectLength * length.X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length);
|
||||
|
||||
// Nested hitobjects don't need to scroll, but they do need accurate positions
|
||||
UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length)
|
||||
{
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
var position = hitObjectPositionAt(obj, currentTime, timeRange);
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
obj.Y = (float)(position * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Down:
|
||||
obj.Y = (float)(-position * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
obj.X = (float)(position * length.X);
|
||||
break;
|
||||
case ScrollingDirection.Right:
|
||||
obj.X = (float)(-position * length.X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the position of a <see cref="DrawableHitObject"/> at a point in time.
|
||||
/// <para>
|
||||
/// At t < startTime, position > 0. <br />
|
||||
/// At t = startTime, position = 0. <br />
|
||||
/// At t > startTime, position < 0.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="obj">The <see cref="DrawableHitObject"/>.</param>
|
||||
/// <param name="time">The time to find the position of <paramref name="obj"/> at.</param>
|
||||
/// <param name="timeRange">The amount of time visualised by the scrolling area.</param>
|
||||
/// <returns>The position of <paramref name="obj"/> in the scrolling area at time = <paramref name="time"/>.</returns>
|
||||
private double hitObjectPositionAt(DrawableHitObject obj, double time, double timeRange)
|
||||
=> (obj.HitObject.StartTime - time) / timeRange * controlPointAt(obj.HitObject.StartTime).Multiplier;
|
||||
|
||||
private readonly MultiplierControlPoint searchPoint = new MultiplierControlPoint();
|
||||
|
||||
/// <summary>
|
||||
/// Finds the <see cref="MultiplierControlPoint"/> which affects the speed of hitobjects at a specific time.
|
||||
/// </summary>
|
||||
/// <param name="time">The time which the <see cref="MultiplierControlPoint"/> should affect.</param>
|
||||
/// <returns>The <see cref="MultiplierControlPoint"/>.</returns>
|
||||
private MultiplierControlPoint controlPointAt(double time)
|
||||
{
|
||||
if (controlPoints.Count == 0)
|
||||
return new MultiplierControlPoint(double.NegativeInfinity);
|
||||
|
||||
if (time < controlPoints[0].StartTime)
|
||||
return controlPoints[0];
|
||||
|
||||
searchPoint.StartTime = time;
|
||||
int index = controlPoints.BinarySearch(searchPoint);
|
||||
|
||||
if (index < 0)
|
||||
index = ~index - 1;
|
||||
|
||||
return controlPoints[index];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
// 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 osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
|
||||
{
|
||||
public class SequentialSpeedChangeVisualiser : ISpeedChangeVisualiser
|
||||
{
|
||||
private readonly Dictionary<DrawableHitObject, double> hitObjectPositions = new Dictionary<DrawableHitObject, double>();
|
||||
|
||||
private readonly IReadOnlyList<MultiplierControlPoint> controlPoints;
|
||||
|
||||
public SequentialSpeedChangeVisualiser(IReadOnlyList<MultiplierControlPoint> controlPoints)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
}
|
||||
|
||||
public void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length)
|
||||
{
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
// To reduce iterations when updating hitobject positions later on, their initial positions are cached
|
||||
var startPosition = hitObjectPositions[obj] = positionAt(obj.HitObject.StartTime, timeRange);
|
||||
|
||||
// Todo: This is approximate and will be incorrect in the case of extreme speed changes
|
||||
obj.LifetimeStart = obj.HitObject.StartTime - timeRange - 1000;
|
||||
|
||||
if (obj.HitObject is IHasEndTime endTime)
|
||||
{
|
||||
var hitObjectLength = positionAt(endTime.EndTime, timeRange) - startPosition;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
obj.Height = (float)(hitObjectLength * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
case ScrollingDirection.Right:
|
||||
obj.Width = (float)(hitObjectLength * length.X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length);
|
||||
|
||||
// Nested hitobjects don't need to scroll, but they do need accurate positions
|
||||
UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdatePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length)
|
||||
{
|
||||
var timelinePosition = positionAt(currentTime, timeRange);
|
||||
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
var finalPosition = hitObjectPositions[obj] - timelinePosition;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
obj.Y = (float)(finalPosition * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Down:
|
||||
obj.Y = (float)(-finalPosition * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
obj.X = (float)(finalPosition * length.X);
|
||||
break;
|
||||
case ScrollingDirection.Right:
|
||||
obj.X = (float)(-finalPosition * length.X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the position which corresponds to a point in time.
|
||||
/// This is a non-linear operation that depends on all the control points up to and including the one active at the time value.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the position at.</param>
|
||||
/// <param name="timeRange">The amount of time visualised by the scrolling area.</param>
|
||||
/// <returns>A positive value indicating the position at <paramref name="time"/>.</returns>
|
||||
private double positionAt(double time, double timeRange)
|
||||
{
|
||||
if (controlPoints.Count == 0)
|
||||
return time / timeRange;
|
||||
|
||||
double length = 0;
|
||||
|
||||
// We need to consider all timing points until the specified time and not just the currently-active one,
|
||||
// since each timing point individually affects the positions of _all_ hitobjects after its start time
|
||||
for (int i = 0; i < controlPoints.Count; i++)
|
||||
{
|
||||
var current = controlPoints[i];
|
||||
var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null;
|
||||
|
||||
// We don't need to consider any control points beyond the current time, since it will not yet
|
||||
// affect any hitobjects
|
||||
if (i > 0 && current.StartTime > time)
|
||||
continue;
|
||||
|
||||
// Duration of the current control point
|
||||
var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime;
|
||||
|
||||
// We want to consider the minimal amount of time that this control point has affected,
|
||||
// which may be either its duration, or the amount of time that has passed within it
|
||||
var durationInCurrent = Math.Min(currentDuration, time - current.StartTime);
|
||||
|
||||
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
|
||||
length += durationInCurrent / timeRange * current.Multiplier;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
||||
}
|
95
osu.Game/Tests/Visual/ScrollingTestContainer.cs
Normal file
95
osu.Game/Tests/Visual/ScrollingTestContainer.cs
Normal file
@ -0,0 +1,95 @@
|
||||
// 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.Configuration;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
|
||||
|
||||
namespace osu.Game.Tests.Visual
|
||||
{
|
||||
/// <summary>
|
||||
/// A container which provides a <see cref="IScrollingInfo"/> to children.
|
||||
/// This should only be used when testing
|
||||
/// </summary>
|
||||
public class ScrollingTestContainer : Container
|
||||
{
|
||||
public SortedList<MultiplierControlPoint> ControlPoints => scrollingInfo.Algorithm.ControlPoints;
|
||||
|
||||
public ScrollVisualisationMethod ScrollAlgorithm { set => scrollingInfo.Algorithm.Algorithm = value; }
|
||||
|
||||
public double TimeRange { set => scrollingInfo.TimeRange.Value = value; }
|
||||
|
||||
[Cached(Type = typeof(IScrollingInfo))]
|
||||
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
|
||||
|
||||
public ScrollingTestContainer(ScrollingDirection direction)
|
||||
{
|
||||
scrollingInfo.Direction.Value = direction;
|
||||
}
|
||||
|
||||
public void Flip() => scrollingInfo.Direction.Value = scrollingInfo.Direction.Value == ScrollingDirection.Up ? ScrollingDirection.Down : ScrollingDirection.Up;
|
||||
|
||||
private class TestScrollingInfo : IScrollingInfo
|
||||
{
|
||||
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
|
||||
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
|
||||
|
||||
public readonly Bindable<double> TimeRange = new Bindable<double>(1000) { Value = 1000 };
|
||||
IBindable<double> IScrollingInfo.TimeRange => TimeRange;
|
||||
|
||||
public readonly TestScrollAlgorithm Algorithm = new TestScrollAlgorithm();
|
||||
IScrollAlgorithm IScrollingInfo.Algorithm => Algorithm;
|
||||
}
|
||||
|
||||
private class TestScrollAlgorithm : IScrollAlgorithm
|
||||
{
|
||||
public readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>();
|
||||
|
||||
private IScrollAlgorithm implementation;
|
||||
|
||||
public TestScrollAlgorithm()
|
||||
{
|
||||
Algorithm = ScrollVisualisationMethod.Constant;
|
||||
}
|
||||
|
||||
public ScrollVisualisationMethod Algorithm
|
||||
{
|
||||
set
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case ScrollVisualisationMethod.Constant:
|
||||
implementation = new ConstantScrollAlgorithm();
|
||||
break;
|
||||
case ScrollVisualisationMethod.Overlapping:
|
||||
implementation = new OverlappingScrollAlgorithm(ControlPoints);
|
||||
break;
|
||||
case ScrollVisualisationMethod.Sequential:
|
||||
implementation = new SequentialScrollAlgorithm(ControlPoints);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double GetDisplayStartTime(double time, double timeRange)
|
||||
=> implementation.GetDisplayStartTime(time, timeRange);
|
||||
|
||||
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
|
||||
=> implementation.GetLength(startTime, endTime, timeRange, scrollLength);
|
||||
|
||||
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
|
||||
=> implementation.PositionAt(time, currentTime, timeRange, scrollLength);
|
||||
|
||||
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
|
||||
=> implementation.TimeAt(position, currentTime, timeRange, scrollLength);
|
||||
|
||||
public void Reset()
|
||||
=> implementation.Reset();
|
||||
}
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2018.1105.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2018.1112.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.22.0" />
|
||||
<PackageReference Include="NUnit" Version="3.11.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
Loading…
Reference in New Issue
Block a user