diff --git a/.github/ISSUE_TEMPLATE/bug-issues.md b/.github/ISSUE_TEMPLATE/bug-issues.md
new file mode 100644
index 0000000000..8d85c92fec
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-issues.md
@@ -0,0 +1,14 @@
+---
+name: Bug Report
+about: For issues regarding encountered game bugs
+---
+
+
+
+**Describe your problem:**
+
+**Screenshots or videos showing encountered issue:**
+
+**osu!lazer version:**
+
+**Logs:**
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/crash-issues.md b/.github/ISSUE_TEMPLATE/crash-issues.md
new file mode 100644
index 0000000000..849f042c1f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/crash-issues.md
@@ -0,0 +1,16 @@
+---
+name: Crash Report
+about: For issues regarding game crashes or permanent freezes
+---
+
+
+
+**Describe your problem:**
+
+**Screenshots or videos showing encountered issue:**
+
+**osu!lazer version:**
+
+**Logs:**
+
+**Computer Specifications:**
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature-request-issues.md b/.github/ISSUE_TEMPLATE/feature-request-issues.md
new file mode 100644
index 0000000000..73c4f37a3e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request-issues.md
@@ -0,0 +1,10 @@
+---
+name: Feature Request
+about: Let us know what you would like to see in the game!
+---
+
+
+
+**Describe the feature:**
+
+**Proposal designs of the feature:**
diff --git a/.github/ISSUE_TEMPLATE/missing-for-live-issues.md b/.github/ISSUE_TEMPLATE/missing-for-live-issues.md
new file mode 100644
index 0000000000..ae3cf20a8c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/missing-for-live-issues.md
@@ -0,0 +1,10 @@
+---
+name: Missing for Live
+about: Let us know the features you need which are available in osu-stable but not lazer
+---
+
+
+
+**Describe the feature:**
+
+**Designs:**
diff --git a/.gitignore b/.gitignore
index 5138e940ed..f95a04e517 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,10 @@
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
+### Cake ###
+tools/*
+!tools/cakebuild.csproj
+
# Build results
bin/[Dd]ebug/
[Dd]ebugPublic/
@@ -98,6 +102,7 @@ $tf/
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
+inspectcode
# JustCode is a .NET coding add-in
.JustCode
@@ -247,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
@@ -257,3 +266,5 @@ paket-files/
__pycache__/
*.pyc
Staging/
+
+inspectcodereport.xml
diff --git a/README.md b/README.md
index dc36145337..baaba22726 100644
--- a/README.md
+++ b/README.md
@@ -25,6 +25,7 @@ Build and run
- Using Visual Studio 2017, Rider or Visual Studio Code (configurations are included)
- From command line using `dotnet run --project osu.Desktop`. When building for non-development purposes, add `-c Release` to gain higher performance.
+- To run with code analysis, instead use `powershell ./build.ps1` or `build.sh`. This is currently only supported under windows due to [resharper cli shortcomings](https://youtrack.jetbrains.com/issue/RSRP-410004). Alternative, you can install resharper or use rider to get inline support in your IDE of choice.
Note: If you run from command line under linux, you will need to prefix the output folder to your `LD_LIBRARY_PATH`. See `.vscode/launch.json` for an example
diff --git a/appveyor.yml b/appveyor.yml
index 8c06b42fd2..1f485485da 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,22 +1,8 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2017
-configuration: Debug
-cache:
- - C:\ProgramData\chocolatey\bin -> appveyor.yml
- - C:\ProgramData\chocolatey\lib -> appveyor.yml
+test: off
install:
- cmd: git submodule update --init --recursive --depth=5
- - cmd: choco install resharper-clt -y
- - cmd: choco install nvika -y
- - cmd: dotnet tool install CodeFileSanity --version 0.0.16 --global
-before_build:
- - cmd: CodeFileSanity
- - cmd: nuget restore -verbosity quiet
-build:
- project: osu.sln
- parallel: true
- verbosity: minimal
-after_build:
- - cmd: inspectcode --o="inspectcodereport.xml" --projects:osu.Game* --caches-home="inspectcode" osu.sln > NUL
- - cmd: NVika parsereport "inspectcodereport.xml" --treatwarningsaserrors
+build_script:
+ - cmd: PowerShell -Version 2.0 .\build.ps1
diff --git a/build.cake b/build.cake
new file mode 100644
index 0000000000..bc7dfafb8c
--- /dev/null
+++ b/build.cake
@@ -0,0 +1,72 @@
+#addin "nuget:?package=CodeFileSanity&version=0.0.21"
+#addin "nuget:?package=JetBrains.ReSharper.CommandLineTools&version=2018.2.2"
+#tool "nuget:?package=NVika.MSBuild&version=1.0.1"
+
+///////////////////////////////////////////////////////////////////////////////
+// ARGUMENTS
+///////////////////////////////////////////////////////////////////////////////
+
+var target = Argument("target", "Build");
+var configuration = Argument("configuration", "Release");
+
+var osuSolution = new FilePath("./osu.sln");
+
+///////////////////////////////////////////////////////////////////////////////
+// TASKS
+///////////////////////////////////////////////////////////////////////////////
+
+Task("Restore")
+ .Does(() => {
+ DotNetCoreRestore(osuSolution.FullPath);
+ });
+
+Task("Compile")
+ .IsDependentOn("Restore")
+ .Does(() => {
+ DotNetCoreBuild(osuSolution.FullPath, new DotNetCoreBuildSettings {
+ Configuration = configuration,
+ NoRestore = true,
+ });
+ });
+
+Task("Test")
+ .IsDependentOn("Compile")
+ .Does(() => {
+ var testAssemblies = GetFiles("**/*.Tests/bin/**/*.Tests.dll");
+
+ DotNetCoreVSTest(testAssemblies, new DotNetCoreVSTestSettings {
+ Logger = AppVeyor.IsRunningOnAppVeyor ? "Appveyor" : $"trx",
+ Parallel = true,
+ ToolTimeout = TimeSpan.FromMinutes(10),
+ });
+ });
+
+// windows only because both inspectcore and nvika depend on net45
+Task("InspectCode")
+ .WithCriteria(IsRunningOnWindows())
+ .IsDependentOn("Compile")
+ .Does(() => {
+ var nVikaToolPath = GetFiles("./tools/NVika.MSBuild.*/tools/NVika.exe").First();
+
+ InspectCode(osuSolution, new InspectCodeSettings {
+ CachesHome = "inspectcode",
+ OutputFile = "inspectcodereport.xml",
+ });
+
+ StartProcess(nVikaToolPath, @"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors");
+ });
+
+Task("CodeFileSanity")
+ .Does(() => {
+ ValidateCodeSanity(new ValidateCodeSanitySettings {
+ RootDirectory = ".",
+ IsAppveyorBuild = AppVeyor.IsRunningOnAppVeyor
+ });
+ });
+
+Task("Build")
+ .IsDependentOn("CodeFileSanity")
+ .IsDependentOn("InspectCode")
+ .IsDependentOn("Test");
+
+RunTarget(target);
\ No newline at end of file
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 0000000000..9968673c90
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,79 @@
+##########################################################################
+# This is a customized Cake bootstrapper script for PowerShell.
+##########################################################################
+
+<#
+
+.SYNOPSIS
+This is a Powershell script to bootstrap a Cake build.
+
+.DESCRIPTION
+This Powershell script restores NuGet tools (including Cake)
+and execute your Cake build script with the parameters you provide.
+
+.PARAMETER Script
+The build script to execute.
+.PARAMETER Target
+The build script target to run.
+.PARAMETER Configuration
+The build configuration to use.
+.PARAMETER Verbosity
+Specifies the amount of information to be displayed.
+.PARAMETER ShowDescription
+Shows description about tasks.
+.PARAMETER DryRun
+Performs a dry run.
+.PARAMETER ScriptArgs
+Remaining arguments are added here.
+
+.LINK
+https://cakebuild.net
+
+#>
+
+[CmdletBinding()]
+Param(
+ [string]$Script = "build.cake",
+ [string]$Target,
+ [string]$Configuration,
+ [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
+ [string]$Verbosity,
+ [switch]$ShowDescription,
+ [Alias("WhatIf", "Noop")]
+ [switch]$DryRun,
+ [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
+ [string[]]$ScriptArgs
+)
+
+Write-Host "Preparing to run build script..."
+
+# Determine the script root for resolving other paths.
+if(!$PSScriptRoot){
+ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
+}
+
+# Resolve the paths for resources used for debugging.
+$TOOLS_DIR = Join-Path $PSScriptRoot "tools"
+$CAKE_CSPROJ = Join-Path $TOOLS_DIR "cakebuild.csproj"
+
+# Install the required tools locally.
+Write-Host "Restoring cake tools..."
+Invoke-Expression "dotnet restore `"$CAKE_CSPROJ`" --packages `"$TOOLS_DIR`"" | Out-Null
+
+# Find the Cake executable
+$CAKE_EXECUTABLE = (Get-ChildItem -Path ./tools/cake.coreclr/ -Filter Cake.dll -Recurse).FullName
+
+# Build Cake arguments
+$cakeArguments = @("$Script");
+if ($Target) { $cakeArguments += "-target=$Target" }
+if ($Configuration) { $cakeArguments += "-configuration=$Configuration" }
+if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" }
+if ($ShowDescription) { $cakeArguments += "-showdescription" }
+if ($DryRun) { $cakeArguments += "-dryrun" }
+if ($Experimental) { $cakeArguments += "-experimental" }
+$cakeArguments += $ScriptArgs
+
+# Start Cake
+Write-Host "Running build script..."
+Invoke-Expression "dotnet `"$CAKE_EXECUTABLE`" $cakeArguments"
+exit $LASTEXITCODE
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000000..caf1702f41
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+##########################################################################
+# This is a customized Cake bootstrapper script for Shell.
+##########################################################################
+
+echo "Preparing to run build script..."
+
+SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+TOOLS_DIR=$SCRIPT_DIR/tools
+CAKE_BINARY_PATH=$TOOLS_DIR/"cake.coreclr"
+
+SCRIPT="build.cake"
+CAKE_CSPROJ=$TOOLS_DIR/"cakebuild.csproj"
+
+# Parse arguments.
+CAKE_ARGUMENTS=()
+for i in "$@"; do
+ case $1 in
+ -s|--script) SCRIPT="$2"; shift ;;
+ --) shift; CAKE_ARGUMENTS+=("$@"); break ;;
+ *) CAKE_ARGUMENTS+=("$1") ;;
+ esac
+ shift
+done
+
+# Install the required tools locally.
+echo "Restoring cake tools..."
+dotnet restore $CAKE_CSPROJ --packages $TOOLS_DIR > /dev/null 2>&1
+
+# Search for the CakeBuild binary.
+CAKE_BINARY=$(find $CAKE_BINARY_PATH -name "Cake.dll")
+
+# Start Cake
+echo "Running build script..."
+
+dotnet "$CAKE_BINARY" $SCRIPT "${CAKE_ARGUMENTS[@]}"
diff --git a/cake.config b/cake.config
new file mode 100644
index 0000000000..187d825591
--- /dev/null
+++ b/cake.config
@@ -0,0 +1,5 @@
+
+[Nuget]
+Source=https://api.nuget.org/v3/index.json
+UseInProcessClient=true
+LoadDependencies=true
diff --git a/osu-resources b/osu-resources
index c3848d8b1c..694cb03f19 160000
--- a/osu-resources
+++ b/osu-resources
@@ -1 +1 @@
-Subproject commit c3848d8b1c84966abe851d915bcca878415614b4
+Subproject commit 694cb03f19c93106ed0f2593f3e506e835fb652a
diff --git a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs
index 5e68acde94..bea64302c3 100644
--- a/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs
+++ b/osu.Game.Rulesets.Catch.Tests/TestCaseAutoJuiceStream.cs
@@ -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)
- },
- CurveType = CurveType.Linear,
- Distance = width * CatchPlayfield.BASE_WIDTH,
+ }),
StartTime = i * 2000,
NewCombo = i % 8 == 0
});
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 326791f506..b76f591239 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 15d4edc411..c3dc9499c2 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -34,10 +34,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
StartTime = obj.StartTime,
Samples = obj.Samples,
- ControlPoints = curveData.ControlPoints,
- CurveType = curveData.CurveType,
- Distance = curveData.Distance,
- RepeatSamples = curveData.RepeatSamples,
+ Path = curveData.Path,
+ NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
NewCombo = comboData?.NewCombo ?? false,
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
index 21e09f991c..6592b8b313 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModFlashlight.cs
@@ -1,12 +1,66 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using OpenTK;
namespace osu.Game.Rulesets.Catch.Mods
{
- public class CatchModFlashlight : ModFlashlight
+ public class CatchModFlashlight : ModFlashlight
{
public override double ScoreMultiplier => 1.12;
+
+ private const float default_flashlight_size = 350;
+
+ public override Flashlight CreateFlashlight() => new CatchFlashlight(playfield);
+
+ private CatchPlayfield playfield;
+
+ public override void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ {
+ playfield = (CatchPlayfield)rulesetContainer.Playfield;
+ base.ApplyToRulesetContainer(rulesetContainer);
+ }
+
+ private class CatchFlashlight : Flashlight
+ {
+ private readonly CatchPlayfield playfield;
+
+ public CatchFlashlight(CatchPlayfield playfield)
+ {
+ this.playfield = playfield;
+ FlashlightSize = new Vector2(0, getSizeFor(0));
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ var catcherArea = playfield.CatcherArea;
+
+ FlashlightPosition = catcherArea.ToSpaceOfOtherDrawable(catcherArea.MovableCatcher.DrawPosition, this);
+ }
+
+ private float getSizeFor(int combo)
+ {
+ if (combo > 200)
+ return default_flashlight_size * 0.8f;
+ else if (combo > 100)
+ return default_flashlight_size * 0.9f;
+ else
+ return default_flashlight_size;
+ }
+
+ protected override void OnComboChange(int newCombo)
+ {
+ this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(newCombo)), FLASHLIGHT_FADE_DURATION);
+ }
+
+ protected override string FragmentShader => "CircularFlashlight";
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 82e32d24d2..d8bd3e0edc 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -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
{
@@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Objects
if (TickDistance == 0)
return;
- var length = Curve.Distance;
+ var length = Path.Distance;
var tickDistance = Math.Min(TickDistance, length);
var spanDuration = length / Velocity;
@@ -95,7 +94,7 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new TinyDroplet
{
StartTime = t,
- X = X + Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
+ X = X + Path.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
Samples = new List(Samples.Select(s => new SampleInfo
{
Bank = s.Bank,
@@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.Catch.Objects
AddNested(new Droplet
{
StartTime = time,
- X = X + Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
+ X = X + Path.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
Samples = new List(Samples.Select(s => new SampleInfo
{
Bank = s.Bank,
@@ -127,38 +126,28 @@ namespace osu.Game.Rulesets.Catch.Objects
{
Samples = Samples,
StartTime = spanStartTime + spanDuration,
- X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
+ X = X + Path.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
});
}
}
- public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity;
+ public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
public double Duration => EndTime - StartTime;
- public double Distance
+ private SliderPath path;
+
+ public SliderPath Path
{
- get { return Curve.Distance; }
- set { Curve.Distance = value; }
+ get => path;
+ set => path = value;
}
- public SliderCurve Curve { get; } = new SliderCurve();
+ public double Distance => Path.Distance;
- public Vector2[] ControlPoints
- {
- get { return Curve.ControlPoints; }
- set { Curve.ControlPoints = value; }
- }
-
- public List> RepeatSamples { get; set; } = new List>();
-
- public CurveType CurveType
- {
- get { return Curve.CurveType; }
- set { Curve.CurveType = value; }
- }
+ public List> NodeSamples { get; set; } = new List>();
public double? LegacyLastTickOffset { get; set; }
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index 925e7aaac9..05b7cb23a2 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -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;
@@ -19,16 +18,10 @@ namespace osu.Game.Rulesets.Catch.UI
{
public const float BASE_WIDTH = 512;
- private readonly CatcherArea catcherArea;
-
- protected override bool UserScrollSpeedAdjustment => false;
-
- protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Constant;
+ internal readonly CatcherArea CatcherArea;
public CatchPlayfield(BeatmapDifficulty difficulty, Func> getVisualRepresentation)
{
- Direction.Value = ScrollingDirection.Down;
-
Container explodingFruitContainer;
Anchor = Anchor.TopCentre;
@@ -45,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
RelativeSizeAxes = Axes.Both,
},
- catcherArea = new CatcherArea(difficulty)
+ CatcherArea = new CatcherArea(difficulty)
{
GetVisualRepresentation = getVisualRepresentation,
ExplodingFruitTarget = explodingFruitContainer,
@@ -55,11 +48,9 @@ namespace osu.Game.Rulesets.Catch.UI
HitObjectContainer
}
};
-
- VisibleTimeRange.Value = BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
}
- public bool CheckIfWeCanCatch(CatchHitObject obj) => catcherArea.AttemptCatch(obj);
+ public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj);
public override void Add(DrawableHitObject h)
{
@@ -72,6 +63,6 @@ namespace osu.Game.Rulesets.Catch.UI
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
- => catcherArea.OnResult((DrawableCatchHitObject)judgedObject, result);
+ => CatcherArea.OnResult((DrawableCatchHitObject)judgedObject, result);
}
}
diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs
index bd0fec43a1..ef1bb7767f 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs
@@ -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
{
+ 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);
@@ -31,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.UI
public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
- protected override DrawableHitObject GetVisualRepresentation(CatchHitObject h)
+ public override DrawableHitObject GetVisualRepresentation(CatchHitObject h)
{
switch (h)
{
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
index 06453ac32d..8661a3c162 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
public const float CATCHER_SIZE = 100;
- protected readonly Catcher MovableCatcher;
+ protected internal readonly Catcher MovableCatcher;
public Func> GetVisualRepresentation;
diff --git a/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs
deleted file mode 100644
index 29663c2093..0000000000
--- a/osu.Game.Rulesets.Mania.Tests/ScrollingTestContainer.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// 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
-{
- ///
- /// A container which provides a to children.
- ///
- 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 Direction = new Bindable();
- IBindable IScrollingInfo.Direction => Direction;
- }
-}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs
index cceee718ca..d044b48553 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestCaseColumn.cs
@@ -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
};
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseHoldNoteSelectionBlueprint.cs
new file mode 100644
index 0000000000..993f7520e8
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestCaseHoldNoteSelectionBlueprint.cs
@@ -0,0 +1,57 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Edit.Blueprints;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestCaseHoldNoteSelectionBlueprint : SelectionBlueprintTestCase
+ {
+ private readonly DrawableHoldNote drawableObject;
+
+ protected override Container Content => content ?? base.Content;
+ private readonly Container content;
+
+ public TestCaseHoldNoteSelectionBlueprint()
+ {
+ var holdNote = new HoldNote { Column = 0, Duration = 1000 };
+ holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ AutoSizeAxes = Axes.Y,
+ Width = 50,
+ Child = drawableObject = new DrawableHoldNote(holdNote)
+ {
+ Height = 300,
+ AccentColour = OsuColour.Gray(0.3f)
+ }
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ foreach (var nested in drawableObject.NestedHitObjects)
+ {
+ double finalPosition = (nested.HitObject.StartTime - drawableObject.HitObject.StartTime) / drawableObject.HitObject.Duration;
+ nested.Y = (float)(-finalPosition * content.DrawHeight);
+ }
+ }
+
+ protected override SelectionBlueprint CreateBlueprint() => new HoldNoteSelectionBlueprint(drawableObject);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseNoteSelectionBlueprint.cs
new file mode 100644
index 0000000000..fd26b93e5c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestCaseNoteSelectionBlueprint.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Edit.Blueprints;
+using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Tests.Visual;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestCaseNoteSelectionBlueprint : SelectionBlueprintTestCase
+ {
+ private readonly DrawableNote drawableObject;
+
+ protected override Container Content => content ?? base.Content;
+ private readonly Container content;
+
+ public TestCaseNoteSelectionBlueprint()
+ {
+ var note = new Note { Column = 0 };
+ note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ base.Content.Child = content = new ScrollingTestContainer(ScrollingDirection.Down)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(50, 20),
+ Child = drawableObject = new DrawableNote(note)
+ };
+ }
+
+ protected override SelectionBlueprint CreateBlueprint() => new NoteSelectionBlueprint(drawableObject);
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs
index 5c5d955168..02d5b13100 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestCaseStage.cs
@@ -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
};
}
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index bf75ebbff8..98ad086c66 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 5d8480d969..d86ebc9a09 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -247,7 +247,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.SpanCount();
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
- return curveData.RepeatSamples[index];
+ return curveData.NodeSamples[index];
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index 37a8062d75..635004d2f6 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -470,7 +470,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
double segmentTime = (EndTime - HitObject.StartTime) / spanCount;
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
- return curveData.RepeatSamples[index];
+ return curveData.NodeSamples[index];
}
///
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs
new file mode 100644
index 0000000000..424ff1118c
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
+
+namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
+{
+ public class EditNotePiece : CompositeDrawable
+ {
+ public EditNotePiece()
+ {
+ Height = NotePiece.NOTE_HEIGHT;
+
+ CornerRadius = 5;
+ Masking = true;
+
+ InternalChild = new NotePiece();
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Colour = colours.Yellow;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/HoldNoteMask.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
similarity index 81%
rename from osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/HoldNoteMask.cs
rename to osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index 03d2ba19cb..35ce38dadb 100644
--- a/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/HoldNoteMask.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -4,18 +4,17 @@
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
-using osu.Game.Rulesets.Edit;
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;
-namespace osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays
+namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public class HoldNoteMask : HitObjectMask
+ public class HoldNoteSelectionBlueprint : ManiaSelectionBlueprint
{
public new DrawableHoldNote HitObject => (DrawableHoldNote)base.HitObject;
@@ -23,13 +22,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays
private readonly BodyPiece body;
- public HoldNoteMask(DrawableHoldNote hold)
+ public HoldNoteSelectionBlueprint(DrawableHoldNote hold)
: base(hold)
{
InternalChildren = new Drawable[]
{
- new HoldNoteNoteMask(hold.Head),
- new HoldNoteNoteMask(hold.Tail),
+ new HoldNoteNoteSelectionBlueprint(hold.Head),
+ new HoldNoteNoteSelectionBlueprint(hold.Tail),
body = new BodyPiece
{
AccentColour = Color4.Transparent
@@ -59,9 +58,11 @@ namespace osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays
Y -= HitObject.Tail.DrawHeight;
}
- private class HoldNoteNoteMask : NoteMask
+ public override Quad SelectionQuad => ScreenSpaceDrawQuad;
+
+ private class HoldNoteNoteSelectionBlueprint : NoteSelectionBlueprint
{
- public HoldNoteNoteMask(DrawableNote note)
+ public HoldNoteNoteSelectionBlueprint(DrawableNote note)
: base(note)
{
Select();
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
new file mode 100644
index 0000000000..53f9dd8752
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -0,0 +1,23 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Mania.Edit.Blueprints
+{
+ public class ManiaSelectionBlueprint : SelectionBlueprint
+ {
+ public ManiaSelectionBlueprint(DrawableHitObject hitObject)
+ : base(hitObject)
+ {
+ RelativeSizeAxes = Axes.None;
+ }
+
+ public override void AdjustPosition(DragEvent dragEvent)
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
new file mode 100644
index 0000000000..7df7924c51
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NoteSelectionBlueprint.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Mania.Edit.Blueprints
+{
+ public class NoteSelectionBlueprint : ManiaSelectionBlueprint
+ {
+ public NoteSelectionBlueprint(DrawableNote note)
+ : base(note)
+ {
+ AddInternal(new EditNotePiece { RelativeSizeAxes = Axes.X });
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Size = HitObject.DrawSize;
+ Position = Parent.ToLocalSpace(HitObject.ScreenSpaceDrawQuad.TopLeft);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/NoteMask.cs b/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/NoteMask.cs
deleted file mode 100644
index 78f876cb14..0000000000
--- a/osu.Game.Rulesets.Mania/Edit/Layers/Selection/Overlays/NoteMask.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-
-using osu.Framework.Allocation;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
-
-namespace osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays
-{
- public class NoteMask : HitObjectMask
- {
- public NoteMask(DrawableNote note)
- : base(note)
- {
- Scale = note.Scale;
-
- CornerRadius = 5;
- Masking = true;
-
- AddInternal(new NotePiece());
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- Colour = colours.Yellow;
- }
-
- protected override void Update()
- {
- base.Update();
-
- Size = HitObject.DrawSize;
- Position = Parent.ToLocalSpace(HitObject.ScreenSpaceDrawQuad.TopLeft);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs
index 138a2c0273..2404297cc3 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaEditRulesetContainer.cs
@@ -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)
{
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index f37d8134ce..3531b81e68 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -1,56 +1,55 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+using System;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
-using osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.UI;
using System.Collections.Generic;
using osu.Framework.Allocation;
-using osu.Game.Rulesets.Mania.Configuration;
-using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Mania.Edit.Blueprints;
+using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Edit
{
- public class ManiaHitObjectComposer : HitObjectComposer
+ public class ManiaHitObjectComposer : HitObjectComposer
{
- protected new ManiaConfigManager Config => (ManiaConfigManager)base.Config;
-
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
}
+ private DependencyContainer dependencies;
+
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
+ => dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
+
+ protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
{
- var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
- dependencies.CacheAs(new ManiaScrollingInfo(Config));
- return dependencies;
+ 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 RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new ManiaEditRulesetContainer(ruleset, beatmap);
+ protected override IReadOnlyList CompositionTools => Array.Empty();
- protected override IReadOnlyList CompositionTools => new ICompositionTool[]
- {
- new HitObjectCompositionTool("Note"),
- new HitObjectCompositionTool("Hold"),
- };
-
- public override HitObjectMask CreateMaskFor(DrawableHitObject hitObject)
+ public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
{
switch (hitObject)
{
case DrawableNote note:
- return new NoteMask(note);
+ return new NoteSelectionBlueprint(note);
case DrawableHoldNote holdNote:
- return new HoldNoteMask(holdNote);
+ return new HoldNoteSelectionBlueprint(holdNote);
}
- return base.CreateMaskFor(hitObject);
+ return base.CreateBlueprintFor(hitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
new file mode 100644
index 0000000000..81a2728ad4
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/Masks/ManiaSelectionBlueprint.cs
@@ -0,0 +1,18 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Mania.Edit.Masks
+{
+ public abstract class ManiaSelectionBlueprint : SelectionBlueprint
+ {
+ protected ManiaSelectionBlueprint(DrawableHitObject hitObject)
+ : base(hitObject)
+ {
+ RelativeSizeAxes = Axes.None;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs
index aecfb50fbe..4790a77cc0 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs
@@ -5,13 +5,11 @@ using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToRulesetContainer
+ public class ManiaModDualStages : Mod, IPlayfieldTypeMod, IApplicableToBeatmapConverter, IApplicableToBeatmap
{
public override string Name => "Dual Stages";
public override string ShortenedName => "DS";
@@ -34,22 +32,21 @@ namespace osu.Game.Rulesets.Mania.Mods
mbc.TargetColumns *= 2;
}
- public void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ public void ApplyToBeatmap(Beatmap beatmap)
{
- var mrc = (ManiaRulesetContainer)rulesetContainer;
-
- // Although this can work, for now let's not allow keymods for mania-specific beatmaps
if (isForCurrentRuleset)
return;
+ var maniaBeatmap = (ManiaBeatmap)beatmap;
+
var newDefinitions = new List();
- foreach (var existing in mrc.Beatmap.Stages)
+ foreach (var existing in maniaBeatmap.Stages)
{
newDefinitions.Add(new StageDefinition { Columns = existing.Columns / 2 });
newDefinitions.Add(new StageDefinition { Columns = existing.Columns / 2 });
}
- mrc.Beatmap.Stages = newDefinitions;
+ maniaBeatmap.Stages = newDefinitions;
}
public PlayfieldType PlayfieldType => PlayfieldType.Dual;
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
index 08815ede09..73942cbb53 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs
@@ -3,6 +3,7 @@
using System;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
@@ -16,6 +17,6 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Description => @"Keys appear out of nowhere!";
public override double ScoreMultiplier => 1;
public override bool Ranked => true;
- public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
+ public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
index d7a1bc4fbe..e0e395ce55 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs
@@ -2,13 +2,60 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
+using OpenTK;
namespace osu.Game.Rulesets.Mania.Mods
{
- public class ManiaModFlashlight : ModFlashlight
+ public class ManiaModFlashlight : ModFlashlight
{
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
+
+ private const float default_flashlight_size = 180;
+
+ public override Flashlight CreateFlashlight() => new ManiaFlashlight();
+
+ private class ManiaFlashlight : Flashlight
+ {
+ private readonly Cached flashlightProperties = new Cached();
+
+ public ManiaFlashlight()
+ {
+ FlashlightSize = new Vector2(0, default_flashlight_size);
+ }
+
+ public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
+ {
+ if ((invalidation & Invalidation.DrawSize) > 0)
+ {
+ flashlightProperties.Invalidate();
+ }
+
+ return base.Invalidate(invalidation, source, shallPropagate);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!flashlightProperties.IsValid)
+ {
+ FlashlightSize = new Vector2(DrawWidth, FlashlightSize.Y);
+
+ FlashlightPosition = DrawPosition + DrawSize / 2;
+ flashlightProperties.Validate();
+ }
+ }
+
+ protected override void OnComboChange(int newCombo)
+ {
+ }
+
+ protected override string FragmentShader => "RectangularFlashlight";
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
index 2ef68a35fa..9bc2502a8f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
@@ -10,6 +11,6 @@ namespace osu.Game.Rulesets.Mania.Mods
{
public override string Description => @"Keys fade out before you hit them!";
public override double ScoreMultiplier => 1;
- public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
+ public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) };
}
}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index cb6196a890..8c96c6dfe7 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -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;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs
index 2c74f5b168..b7c90e5144 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/NotePiece.cs
@@ -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
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 09976e5994..576af6d93a 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -1,12 +1,12 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// 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, IHasAccentColour
+ public class Column : ScrollingPlayfield, IKeyBindingHandler, 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();
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 5c3a618a19..c59917056d 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -1,20 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// 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 stages = new List();
@@ -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);
- }
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
index 49874f6dc1..321dd4e1cb 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs
@@ -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 configDirection = new Bindable();
+
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() == null)
- dependencies.CacheAs(new ManiaScrollingInfo(Config));
-
- return dependencies;
+ Config.BindWith(ManiaSetting.ScrollTime, TimeRange);
}
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages)
@@ -96,7 +92,7 @@ namespace osu.Game.Rulesets.Mania.UI
public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, Variant);
- protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h)
+ public override DrawableHitObject GetVisualRepresentation(ManiaHitObject h)
{
switch (h)
{
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs
deleted file mode 100644
index 624ea13e1b..0000000000
--- a/osu.Game.Rulesets.Mania/UI/ManiaScrollingInfo.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// 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 configDirection = new Bindable();
-
- public readonly Bindable Direction = new Bindable();
- IBindable IScrollingInfo.Direction => Direction;
-
- public ManiaScrollingInfo(ManiaConfigManager config)
- {
- config.BindWith(ManiaSetting.ScrollDirection, configDirection);
- configDirection.BindValueChanged(v => Direction.Value = (ScrollingDirection)v, true);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaScrollingPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaScrollingPlayfield.cs
deleted file mode 100644
index 8ee0fbf7fe..0000000000
--- a/osu.Game.Rulesets.Mania/UI/ManiaScrollingPlayfield.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// 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 direction = new Bindable();
-
- [BackgroundDependencyLoader]
- private void load(IScrollingInfo scrollingInfo)
- {
- direction.BindTo(scrollingInfo.Direction);
- direction.BindValueChanged(direction => Direction.Value = direction, true);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
index 8cf49686b9..19e930f530 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.UI
///
/// A collection of s.
///
- 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);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCirclePlacementBlueprint.cs
new file mode 100644
index 0000000000..313438a337
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCirclePlacementBlueprint.cs
@@ -0,0 +1,19 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestCaseHitCirclePlacementBlueprint : PlacementBlueprintTestCase
+ {
+ protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
+ protected override PlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircleSelectionBlueprint.cs
new file mode 100644
index 0000000000..9662e0018f
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseHitCircleSelectionBlueprint.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Tests.Visual;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestCaseHitCircleSelectionBlueprint : SelectionBlueprintTestCase
+ {
+ private readonly DrawableHitCircle drawableObject;
+
+ public TestCaseHitCircleSelectionBlueprint()
+ {
+ var hitCircle = new HitCircle { Position = new Vector2(256, 192) };
+ hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
+
+ Add(drawableObject = new DrawableHitCircle(hitCircle));
+ }
+
+ protected override SelectionBlueprint CreateBlueprint() => new HitCircleSelectionBlueprint(drawableObject);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs
index 300ac16155..5b638782fb 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSlider.cs
@@ -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,15 +109,14 @@ 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,
- RepeatSamples = createEmptySamples(repeats),
+ NodeSamples = createEmptySamples(repeats),
StackHeight = 10
};
@@ -141,14 +141,13 @@ 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,
- RepeatSamples = createEmptySamples(repeats),
+ NodeSamples = createEmptySamples(repeats),
StackHeight = stackHeight
};
@@ -161,15 +160,14 @@ 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,
- RepeatSamples = createEmptySamples(repeats)
+ NodeSamples = createEmptySamples(repeats)
};
addSlider(slider, 2, 3);
@@ -181,10 +179,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- CurveType = CurveType.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,10 +189,9 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(300, -200),
new Vector2(400, 0),
new Vector2(430, 0)
- },
- Distance = 793.4417,
+ }),
RepeatCount = repeats,
- RepeatSamples = createEmptySamples(repeats)
+ NodeSamples = createEmptySamples(repeats)
};
addSlider(slider, 2, 3);
@@ -207,20 +203,18 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- CurveType = CurveType.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,
- RepeatSamples = createEmptySamples(repeats)
+ NodeSamples = createEmptySamples(repeats)
};
addSlider(slider, 2, 3);
@@ -232,10 +226,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
var slider = new Slider
{
- CurveType = CurveType.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,10 +236,9 @@ namespace osu.Game.Rulesets.Osu.Tests
new Vector2(0, -200),
new Vector2(-200, -200),
new Vector2(0, -200)
- },
- Distance = 1000,
+ }),
RepeatCount = repeats,
- RepeatSamples = createEmptySamples(repeats)
+ NodeSamples = createEmptySamples(repeats)
};
addSlider(slider, 2, 3);
@@ -264,17 +256,15 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-100, 0),
- CurveType = CurveType.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,
- RepeatSamples = repeatSamples
+ NodeSamples = repeatSamples
};
addSlider(slider, 3, 1);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderPlacementBlueprint.cs
new file mode 100644
index 0000000000..1f693ad9f4
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderPlacementBlueprint.cs
@@ -0,0 +1,19 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestCaseSliderPlacementBlueprint : PlacementBlueprintTestCase
+ {
+ protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject);
+ protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionBlueprint.cs
new file mode 100644
index 0000000000..cacbcb2cd6
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSliderSelectionBlueprint.cs
@@ -0,0 +1,54 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System;
+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;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Tests.Visual;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestCaseSliderSelectionBlueprint : SelectionBlueprintTestCase
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(SliderSelectionBlueprint),
+ typeof(SliderCircleSelectionBlueprint),
+ typeof(SliderBodyPiece),
+ typeof(SliderCircle),
+ typeof(PathControlPointVisualiser),
+ typeof(PathControlPointPiece)
+ };
+
+ private readonly DrawableSlider drawableObject;
+
+ public TestCaseSliderSelectionBlueprint()
+ {
+ var slider = new Slider
+ {
+ Position = new Vector2(256, 192),
+ Path = new SliderPath(PathType.Bezier, new[]
+ {
+ Vector2.Zero,
+ new Vector2(150, 150),
+ new Vector2(300, 0)
+ })
+ };
+
+ slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
+
+ Add(drawableObject = new DrawableSlider(slider));
+ }
+
+ protected override SelectionBlueprint CreateBlueprint() => new SliderSelectionBlueprint(drawableObject);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSpinnerPlacementBlueprint.cs
new file mode 100644
index 0000000000..9a90be2582
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSpinnerPlacementBlueprint.cs
@@ -0,0 +1,20 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestCaseSpinnerPlacementBlueprint : PlacementBlueprintTestCase
+ {
+ protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
+
+ protected override PlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestCaseSpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestCaseSpinnerSelectionBlueprint.cs
new file mode 100644
index 0000000000..a0cfd4487e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/TestCaseSpinnerSelectionBlueprint.cs
@@ -0,0 +1,50 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Tests.Visual;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Osu.Tests
+{
+ public class TestCaseSpinnerSelectionBlueprint : SelectionBlueprintTestCase
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(SpinnerSelectionBlueprint),
+ typeof(SpinnerPiece)
+ };
+
+ private readonly DrawableSpinner drawableSpinner;
+
+ public TestCaseSpinnerSelectionBlueprint()
+ {
+ var spinner = new Spinner
+ {
+ Position = new Vector2(256, 256),
+ StartTime = -1000,
+ EndTime = 2000
+ };
+ spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 });
+
+ Add(new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.5f),
+ Child = drawableSpinner = new DrawableSpinner(spinner)
+ });
+ }
+
+ protected override SelectionBlueprint CreateBlueprint() => new SpinnerSelectionBlueprint(drawableSpinner) { Size = new Vector2(0.5f) };
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 23c6150b6a..6117812f45 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index b2914d4b82..4fc4f3edc3 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -35,10 +35,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
StartTime = original.StartTime,
Samples = original.Samples,
- ControlPoints = curveData.ControlPoints,
- CurveType = curveData.CurveType,
- Distance = curveData.Distance,
- RepeatSamples = curveData.RepeatSamples,
+ Path = curveData.Path,
+ NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount,
Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false,
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
index d6684f55af..39e3c009da 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs
@@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
progress = progress % 1;
// ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
- var diff = slider.StackedPosition + slider.Curve.PositionAt(progress) - slider.LazyEndPosition.Value;
+ var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value;
float dist = diff.Length;
if (dist > approxFollowCircleRadius)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs
new file mode 100644
index 0000000000..9c33435285
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs
@@ -0,0 +1,44 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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 osu.Game.Graphics;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
+{
+ public class HitCirclePiece : CompositeDrawable
+ {
+ private readonly HitCircle hitCircle;
+
+ public HitCirclePiece(HitCircle hitCircle)
+ {
+ this.hitCircle = hitCircle;
+ Origin = Anchor.Centre;
+
+ Size = new Vector2((float)OsuHitObject.OBJECT_RADIUS * 2);
+ Scale = new Vector2(hitCircle.Scale);
+ CornerRadius = Size.X / 2;
+
+ InternalChild = new RingPiece();
+
+ hitCircle.PositionChanged += _ => UpdatePosition();
+ hitCircle.StackHeightChanged += _ => UpdatePosition();
+ hitCircle.ScaleChanged += _ => Scale = new Vector2(hitCircle.Scale);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Colour = colours.Yellow;
+
+ UpdatePosition();
+ }
+
+ protected virtual void UpdatePosition() => Position = hitCircle.StackedPosition;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
new file mode 100644
index 0000000000..eddd399a4a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -0,0 +1,42 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
+{
+ public class HitCirclePlacementBlueprint : PlacementBlueprint
+ {
+ public new HitCircle HitObject => (HitCircle)base.HitObject;
+
+ public HitCirclePlacementBlueprint()
+ : base(new HitCircle())
+ {
+ InternalChild = new HitCirclePiece(HitObject);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ // Fixes a 1-frame position discrpancy due to the first mouse move event happening in the next frame
+ HitObject.Position = GetContainingInputManager().CurrentState.Mouse.Position;
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ HitObject.StartTime = EditorClock.CurrentTime;
+ EndPlacement();
+ return true;
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ HitObject.Position = e.MousePosition;
+ return true;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
new file mode 100644
index 0000000000..a59dac1834
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs
@@ -0,0 +1,18 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
+{
+ public class HitCircleSelectionBlueprint : OsuSelectionBlueprint
+ {
+ public HitCircleSelectionBlueprint(DrawableHitCircle hitCircle)
+ : base(hitCircle)
+ {
+ InternalChild = new HitCirclePiece((HitCircle)hitCircle.HitObject);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
new file mode 100644
index 0000000000..8431d5d5d0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints
+{
+ public class OsuSelectionBlueprint : SelectionBlueprint
+ {
+ protected OsuHitObject OsuObject => (OsuHitObject)HitObject.HitObject;
+
+ public OsuSelectionBlueprint(DrawableHitObject hitObject)
+ : base(hitObject)
+ {
+ }
+
+ public override void AdjustPosition(DragEvent dragEvent) => OsuObject.Position += dragEvent.Delta;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
new file mode 100644
index 0000000000..7100d9443e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -0,0 +1,112 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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 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;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
+{
+ public class PathControlPointPiece : CompositeDrawable
+ {
+ private readonly Slider slider;
+ private readonly int index;
+
+ private readonly Path path;
+ private readonly CircularContainer marker;
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ public PathControlPointPiece(Slider slider, int index)
+ {
+ this.slider = slider;
+ this.index = index;
+
+ Origin = Anchor.Centre;
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ path = new SmoothPath
+ {
+ Anchor = Anchor.Centre,
+ PathWidth = 1
+ },
+ marker = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(10),
+ Masking = true,
+ Child = new Box { RelativeSizeAxes = Axes.Both }
+ }
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Position = slider.StackedPosition + slider.Path.ControlPoints[index];
+
+ marker.Colour = isSegmentSeparator ? colours.Red : colours.Yellow;
+
+ path.ClearVertices();
+
+ if (index != slider.Path.ControlPoints.Length - 1)
+ {
+ path.AddVertex(Vector2.Zero);
+ path.AddVertex(slider.Path.ControlPoints[index + 1] - slider.Path.ControlPoints[index]);
+ }
+
+ path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
+
+ protected override bool OnDragStart(DragStartEvent e) => true;
+
+ protected override bool OnDrag(DragEvent e)
+ {
+ var newControlPoints = slider.Path.ControlPoints.ToArray();
+
+ if (index == 0)
+ {
+ // Special handling for the head - only the position of the slider changes
+ slider.Position += e.Delta;
+
+ // Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
+ for (int i = 1; i < newControlPoints.Length; i++)
+ newControlPoints[i] -= e.Delta;
+ }
+ else
+ newControlPoints[index] += e.Delta;
+
+ if (isSegmentSeparatorWithNext)
+ newControlPoints[index + 1] = newControlPoints[index];
+
+ if (isSegmentSeparatorWithPrevious)
+ newControlPoints[index - 1] = newControlPoints[index];
+
+ slider.Path = new SliderPath(slider.Path.Type, newControlPoints);
+
+ return true;
+ }
+
+ protected override bool OnDragEnd(DragEndEvent e) => true;
+
+ private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
+
+ private bool isSegmentSeparatorWithNext => index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[index + 1] == slider.Path.ControlPoints[index];
+
+ private bool isSegmentSeparatorWithPrevious => index > 0 && slider.Path.ControlPoints[index - 1] == slider.Path.ControlPoints[index];
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
new file mode 100644
index 0000000000..ab9d81574a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -0,0 +1,34 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
+{
+ public class PathControlPointVisualiser : CompositeDrawable
+ {
+ private readonly Slider slider;
+
+ private readonly Container pieces;
+
+ public PathControlPointVisualiser(Slider slider)
+ {
+ this.slider = slider;
+
+ InternalChild = pieces = new Container { RelativeSizeAxes = Axes.Both };
+
+ slider.PathChanged += _ => updatePathControlPoints();
+ updatePathControlPoints();
+ }
+
+ private void updatePathControlPoints()
+ {
+ while (slider.Path.ControlPoints.Length > pieces.Count)
+ pieces.Add(new PathControlPointPiece(slider, pieces.Count));
+ while (slider.Path.ControlPoints.Length < pieces.Count)
+ pieces.Remove(pieces[pieces.Count - 1]);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
new file mode 100644
index 0000000000..06bc265258
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -0,0 +1,57 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using OpenTK;
+using OpenTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
+{
+ public class SliderBodyPiece : CompositeDrawable
+ {
+ private readonly Slider slider;
+ private readonly ManualSliderBody body;
+
+ public SliderBodyPiece(Slider slider)
+ {
+ this.slider = slider;
+
+ InternalChild = body = new ManualSliderBody
+ {
+ AccentColour = Color4.Transparent,
+ PathWidth = slider.Scale * 64
+ };
+
+ slider.PositionChanged += _ => updatePosition();
+ slider.ScaleChanged += _ => body.PathWidth = slider.Scale * 64;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ body.BorderColour = colours.Yellow;
+
+ updatePosition();
+ }
+
+ private void updatePosition() => Position = slider.StackedPosition;
+
+ protected override void Update()
+ {
+ base.Update();
+
+ var vertices = new List();
+ slider.Path.GetPathToProgress(vertices, 0, 1);
+
+ body.SetVertices(vertices);
+
+ Size = body.Size;
+ OriginPosition = body.PathOffset;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderCirclePiece.cs
new file mode 100644
index 0000000000..1ee765f5e0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderCirclePiece.cs
@@ -0,0 +1,36 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
+{
+ public class SliderCirclePiece : HitCirclePiece
+ {
+ private readonly Slider slider;
+ private readonly SliderPosition position;
+
+ public SliderCirclePiece(Slider slider, SliderPosition position)
+ : base(slider.HeadCircle)
+ {
+ this.slider = slider;
+ this.position = position;
+
+ slider.PathChanged += _ => UpdatePosition();
+ }
+
+ protected override void UpdatePosition()
+ {
+ switch (position)
+ {
+ case SliderPosition.Start:
+ Position = slider.StackedPosition + slider.Path.PositionAt(0);
+ break;
+ case SliderPosition.End:
+ Position = slider.StackedPosition + slider.Path.PositionAt(1);
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
new file mode 100644
index 0000000000..4bac9d3556
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs
@@ -0,0 +1,30 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
+{
+ public class SliderCircleSelectionBlueprint : OsuSelectionBlueprint
+ {
+ private readonly Slider slider;
+
+ public SliderCircleSelectionBlueprint(DrawableOsuHitObject hitObject, Slider slider, SliderPosition position)
+ : base(hitObject)
+ {
+ this.slider = slider;
+
+ InternalChild = new SliderCirclePiece(slider, position);
+
+ Select();
+ }
+
+ // Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input.
+ public override bool HandlePositionalInput => false;
+
+ public override void AdjustPosition(DragEvent dragEvent) => slider.Position += dragEvent.Delta;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
new file mode 100644
index 0000000000..d59cd35f19
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -0,0 +1,146 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+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.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;
+using OpenTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
+{
+ public class SliderPlacementBlueprint : PlacementBlueprint
+ {
+ public new Objects.Slider HitObject => (Objects.Slider)base.HitObject;
+
+ private readonly List segments = new List();
+ private Vector2 cursor;
+
+ private PlacementState state;
+
+ public SliderPlacementBlueprint()
+ : base(new Objects.Slider())
+ {
+ RelativeSizeAxes = Axes.Both;
+ segments.Add(new Segment(Vector2.Zero));
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ InternalChildren = new Drawable[]
+ {
+ new SliderBodyPiece(HitObject),
+ new SliderCirclePiece(HitObject, SliderPosition.Start),
+ new SliderCirclePiece(HitObject, SliderPosition.End),
+ new PathControlPointVisualiser(HitObject),
+ };
+
+ setState(PlacementState.Initial);
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ switch (state)
+ {
+ case PlacementState.Initial:
+ HitObject.Position = e.MousePosition;
+ return true;
+ case PlacementState.Body:
+ cursor = e.MousePosition - HitObject.Position;
+ return true;
+ }
+
+ return false;
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ switch (state)
+ {
+ case PlacementState.Initial:
+ beginCurve();
+ break;
+ case PlacementState.Body:
+ switch (e.Button)
+ {
+ case MouseButton.Left:
+ segments.Last().ControlPoints.Add(cursor);
+ break;
+ }
+
+ break;
+ }
+
+ return true;
+ }
+
+ protected override bool OnMouseUp(MouseUpEvent e)
+ {
+ if (state == PlacementState.Body && e.Button == MouseButton.Right)
+ endCurve();
+ return base.OnMouseUp(e);
+ }
+
+ protected override bool OnDoubleClick(DoubleClickEvent e)
+ {
+ segments.Add(new Segment(segments[segments.Count - 1].ControlPoints.Last()));
+ return true;
+ }
+
+ private void beginCurve()
+ {
+ BeginPlacement();
+
+ HitObject.StartTime = EditorClock.CurrentTime;
+ setState(PlacementState.Body);
+ }
+
+ private void endCurve()
+ {
+ updateSlider();
+ EndPlacement();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+ updateSlider();
+ }
+
+ private void updateSlider()
+ {
+ 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)
+ {
+ state = newState;
+ }
+
+ private enum PlacementState
+ {
+ Initial,
+ Body,
+ }
+
+ private class Segment
+ {
+ public readonly List ControlPoints = new List();
+
+ public Segment(Vector2 offset)
+ {
+ ControlPoints.Add(offset);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPosition.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPosition.cs
new file mode 100644
index 0000000000..a117a7056e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPosition.cs
@@ -0,0 +1,11 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
+{
+ public enum SliderPosition
+ {
+ Start,
+ End
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
new file mode 100644
index 0000000000..4810d76bf8
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -0,0 +1,32 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
+{
+ public class SliderSelectionBlueprint : OsuSelectionBlueprint
+ {
+ private readonly SliderCircleSelectionBlueprint headBlueprint;
+
+ public SliderSelectionBlueprint(DrawableSlider slider)
+ : base(slider)
+ {
+ var sliderObject = (Slider)slider.HitObject;
+
+ InternalChildren = new Drawable[]
+ {
+ new SliderBodyPiece(sliderObject),
+ headBlueprint = new SliderCircleSelectionBlueprint(slider.HeadCircle, sliderObject, SliderPosition.Start),
+ new SliderCircleSelectionBlueprint(slider.TailCircle, sliderObject, SliderPosition.End),
+ new PathControlPointVisualiser(sliderObject),
+ };
+ }
+
+ public override Vector2 SelectionPoint => headBlueprint.SelectionPoint;
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
new file mode 100644
index 0000000000..bd63a3e607
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
@@ -0,0 +1,66 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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 osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
+{
+ public class SpinnerPiece : CompositeDrawable
+ {
+ private readonly Spinner spinner;
+ private readonly CircularContainer circle;
+
+ public SpinnerPiece(Spinner spinner)
+ {
+ this.spinner = spinner;
+
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.Both;
+ FillMode = FillMode.Fit;
+ Size = new Vector2(1.3f);
+
+ RingPiece ring;
+ InternalChildren = new Drawable[]
+ {
+ circle = new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Alpha = 0.5f,
+ Child = new Box { RelativeSizeAxes = Axes.Both }
+ },
+ ring = new RingPiece
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ }
+ };
+
+ ring.Scale = new Vector2(spinner.Scale);
+
+ spinner.PositionChanged += _ => updatePosition();
+ spinner.StackHeightChanged += _ => updatePosition();
+ spinner.ScaleChanged += _ => ring.Scale = new Vector2(spinner.Scale);
+
+ updatePosition();
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Colour = colours.Yellow;
+ }
+
+ private void updatePosition() => Position = spinner.Position;
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos);
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
new file mode 100644
index 0000000000..804a4fcba0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
@@ -0,0 +1,47 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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.Rulesets.Edit;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
+{
+ public class SpinnerPlacementBlueprint : PlacementBlueprint
+ {
+ public new Spinner HitObject => (Spinner)base.HitObject;
+
+ private readonly SpinnerPiece piece;
+
+ private bool isPlacingEnd;
+
+ public SpinnerPlacementBlueprint()
+ : base(new Spinner { Position = OsuPlayfield.BASE_SIZE / 2 })
+ {
+ InternalChild = piece = new SpinnerPiece(HitObject) { Alpha = 0.5f };
+ }
+
+ protected override bool OnClick(ClickEvent e)
+ {
+ if (isPlacingEnd)
+ {
+ HitObject.EndTime = EditorClock.CurrentTime;
+ EndPlacement();
+ }
+ else
+ {
+ HitObject.StartTime = EditorClock.CurrentTime;
+
+ isPlacingEnd = true;
+ piece.FadeTo(1f, 150, Easing.OutQuint);
+
+ BeginPlacement();
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs
new file mode 100644
index 0000000000..9e9cc87c5e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerSelectionBlueprint.cs
@@ -0,0 +1,29 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
+{
+ public class SpinnerSelectionBlueprint : OsuSelectionBlueprint
+ {
+ private readonly SpinnerPiece piece;
+
+ public SpinnerSelectionBlueprint(DrawableSpinner spinner)
+ : base(spinner)
+ {
+ InternalChild = piece = new SpinnerPiece((Spinner)spinner.HitObject);
+ }
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => piece.ReceivePositionalInputAt(screenSpacePos);
+
+ public override void AdjustPosition(DragEvent dragEvent)
+ {
+ // Spinners don't support position adjustments
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs
new file mode 100644
index 0000000000..ddab70d53a
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs
@@ -0,0 +1,20 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class HitCircleCompositionTool : HitObjectCompositionTool
+ {
+ public HitCircleCompositionTool()
+ : base(nameof(HitCircle))
+ {
+ }
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs
deleted file mode 100644
index a2aa639004..0000000000
--- a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-
-using osu.Framework.Graphics;
-using osu.Framework.Allocation;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-
-namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
-{
- public class HitCircleMask : HitObjectMask
- {
- public HitCircleMask(DrawableHitCircle hitCircle)
- : base(hitCircle)
- {
- Origin = Anchor.Centre;
-
- Position = hitCircle.Position;
- Size = hitCircle.Size;
- Scale = hitCircle.Scale;
-
- CornerRadius = Size.X / 2;
-
- AddInternal(new RingPiece());
-
- hitCircle.HitObject.PositionChanged += _ => Position = hitCircle.Position;
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- Colour = colours.Yellow;
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs
deleted file mode 100644
index 151564a2a8..0000000000
--- a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-
-using osu.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using OpenTK;
-
-namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
-{
- public class SliderCircleMask : HitObjectMask
- {
- public SliderCircleMask(DrawableHitCircle sliderHead, DrawableSlider slider)
- : this(sliderHead, Vector2.Zero, slider)
- {
- }
-
- public SliderCircleMask(DrawableSliderTail sliderTail, DrawableSlider slider)
- : this(sliderTail, ((Slider)slider.HitObject).Curve.PositionAt(1), slider)
- {
- }
-
- private readonly DrawableOsuHitObject hitObject;
-
- private SliderCircleMask(DrawableOsuHitObject hitObject, Vector2 position, DrawableSlider slider)
- : base(hitObject)
- {
- this.hitObject = hitObject;
-
- Origin = Anchor.Centre;
-
- Position = position;
- Size = slider.HeadCircle.Size;
- Scale = slider.HeadCircle.Scale;
-
- AddInternal(new RingPiece());
-
- Select();
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- Colour = colours.Yellow;
- }
-
- protected override void Update()
- {
- base.Update();
-
- RelativeAnchorPosition = hitObject.RelativeAnchorPosition;
- }
-
- // Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input.
- public override bool HandlePositionalInput => false;
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs
deleted file mode 100644
index aff42dd233..0000000000
--- a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// 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.Primitives;
-using osu.Game.Graphics;
-using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
-using OpenTK;
-using OpenTK.Graphics;
-
-namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
-{
- public class SliderMask : HitObjectMask
- {
- private readonly SliderBody body;
- private readonly DrawableSlider slider;
-
- public SliderMask(DrawableSlider slider)
- : base(slider)
- {
- this.slider = slider;
-
- Position = slider.Position;
-
- var sliderObject = (Slider)slider.HitObject;
-
- InternalChildren = new Drawable[]
- {
- body = new SliderBody(sliderObject)
- {
- AccentColour = Color4.Transparent,
- PathWidth = sliderObject.Scale * 64
- },
- new SliderCircleMask(slider.HeadCircle, slider),
- new SliderCircleMask(slider.TailCircle, slider),
- };
-
- sliderObject.PositionChanged += _ => Position = slider.Position;
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- body.BorderColour = colours.Yellow;
- }
-
- protected override void Update()
- {
- base.Update();
-
- Size = slider.Size;
- OriginPosition = slider.OriginPosition;
-
- // Need to cause one update
- body.UpdateProgress(0);
- }
-
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
-
- public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition);
- public override Quad SelectionQuad => body.PathDrawQuad;
- }
-}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index d6972d55d2..a706e1d4be 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -8,7 +8,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
@@ -16,35 +18,38 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Edit
{
- public class OsuHitObjectComposer : HitObjectComposer
+ public class OsuHitObjectComposer : HitObjectComposer
{
public OsuHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
}
- protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new OsuEditRulesetContainer(ruleset, beatmap);
+ protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap)
+ => new OsuEditRulesetContainer(ruleset, beatmap);
- protected override IReadOnlyList CompositionTools => new ICompositionTool[]
+ protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
{
- new HitObjectCompositionTool(),
- new HitObjectCompositionTool(),
- new HitObjectCompositionTool()
+ new HitCircleCompositionTool(),
+ new SliderCompositionTool(),
+ new SpinnerCompositionTool()
};
protected override Container CreateLayerContainer() => new PlayfieldAdjustmentContainer { RelativeSizeAxes = Axes.Both };
- public override HitObjectMask CreateMaskFor(DrawableHitObject hitObject)
+ public override SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject)
{
switch (hitObject)
{
case DrawableHitCircle circle:
- return new HitCircleMask(circle);
+ return new HitCircleSelectionBlueprint(circle);
case DrawableSlider slider:
- return new SliderMask(slider);
+ return new SliderSelectionBlueprint(slider);
+ case DrawableSpinner spinner:
+ return new SpinnerSelectionBlueprint(spinner);
}
- return base.CreateMaskFor(hitObject);
+ return base.CreateBlueprintFor(hitObject);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs
new file mode 100644
index 0000000000..6d4f67c597
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs
@@ -0,0 +1,20 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class SliderCompositionTool : HitObjectCompositionTool
+ {
+ public SliderCompositionTool()
+ : base(nameof(Slider))
+ {
+ }
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs
new file mode 100644
index 0000000000..5c19a1bac0
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs
@@ -0,0 +1,20 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
+using osu.Game.Rulesets.Osu.Objects;
+
+namespace osu.Game.Rulesets.Osu.Edit
+{
+ public class SpinnerCompositionTool : HitObjectCompositionTool
+ {
+ public SpinnerCompositionTool()
+ : base(nameof(Spinner))
+ {
+ }
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
index a337439593..f425b3c53d 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs
@@ -1,12 +1,52 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+using osu.Framework.Graphics;
+using osu.Framework.Input;
+using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using OpenTK;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModFlashlight : ModFlashlight
+ public class OsuModFlashlight : ModFlashlight
{
public override double ScoreMultiplier => 1.12;
+
+ private const float default_flashlight_size = 180;
+
+ public override Flashlight CreateFlashlight() => new OsuFlashlight();
+
+ private class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition
+ {
+ public OsuFlashlight()
+ {
+ FlashlightSize = new Vector2(0, getSizeFor(0));
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ FlashlightPosition = e.MousePosition;
+ return base.OnMouseMove(e);
+ }
+
+ private float getSizeFor(int combo)
+ {
+ if (combo > 200)
+ return default_flashlight_size * 0.8f;
+ else if (combo > 100)
+ return default_flashlight_size * 0.9f;
+ else
+ return default_flashlight_size;
+ }
+
+ protected override void OnComboChange(int newCombo)
+ {
+ this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(newCombo)), FLASHLIGHT_FADE_DURATION);
+ }
+
+ protected override string FragmentShader => "CircularFlashlight";
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
index 1b3725a15e..223e4df844 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs
@@ -32,12 +32,11 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType().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.Curve?.Calculate(); // Recalculate the slider curve
+ slider.Path = new SliderPath(slider.Path.Type, newControlPoints, slider.Path.ExpectedDistance);
}
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 4bdddcef11..e663989eeb 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -61,6 +61,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Size = circle.DrawSize;
HitObject.PositionChanged += _ => Position = HitObject.StackedPosition;
+ HitObject.StackHeightChanged += _ => Position = HitObject.StackedPosition;
+ HitObject.ScaleChanged += s => Scale = new Vector2(s);
}
public override Color4 AccentColour
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 89f380db4e..a90182cecb 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public readonly DrawableHitCircle HeadCircle;
public readonly DrawableSliderTail TailCircle;
- public readonly SliderBody Body;
+ public readonly SnakingSliderBody Body;
public readonly SliderBall Ball;
public DrawableSlider(Slider s)
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChildren = new Drawable[]
{
- Body = new SliderBody(s)
+ Body = new SnakingSliderBody(s)
{
PathWidth = s.Scale * 64,
},
@@ -85,6 +85,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
HitObject.PositionChanged += _ => Position = HitObject.StackedPosition;
+ HitObject.ScaleChanged += _ =>
+ {
+ Body.PathWidth = HitObject.Scale * 64;
+ Ball.Scale = new Vector2(HitObject.Scale);
+ };
+
+ slider.PathChanged += _ => Body.Refresh();
}
public override Color4 AccentColour
@@ -119,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
foreach (var c in components.OfType()) c.UpdateProgress(completionProgress);
- foreach (var c in components.OfType()) c.UpdateSnakingPosition(slider.Curve.PositionAt(Body.SnakedStart ?? 0), slider.Curve.PositionAt(Body.SnakedEnd ?? 0));
+ foreach (var c in components.OfType()) c.UpdateSnakingPosition(slider.Path.PositionAt(Body.SnakedStart ?? 0), slider.Path.PositionAt(Body.SnakedEnd ?? 0));
foreach (var t in components.OfType()) t.Tracking = Ball.Tracking;
Size = Body.Size;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index 6d6cba4936..b933364887 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -16,7 +16,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
this.slider = slider;
- Position = HitObject.Position - slider.Position;
+ h.PositionChanged += _ => updatePosition();
+ slider.PathChanged += _ => updatePosition();
+
+ updatePosition();
}
protected override void Update()
@@ -33,5 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Action OnShake;
protected override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
+
+ private void updatePosition() => Position = HitObject.Position - slider.Position;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
index 45c925b87a..6946a55d8e 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs
@@ -8,6 +8,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
{
+ private readonly Slider slider;
+
///
/// The judgement text is provided by the .
///
@@ -18,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
: base(hitCircle)
{
+ this.slider = slider;
+
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
@@ -25,7 +29,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AlwaysPresent = true;
- Position = HitObject.Position - slider.Position;
+ hitCircle.PositionChanged += _ => updatePosition();
+ slider.PathChanged += _ => updatePosition();
+
+ updatePosition();
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
@@ -33,5 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!userTriggered && timeOffset >= 0)
ApplyResult(r => r.Type = Tracking ? HitResult.Great : HitResult.Miss);
}
+
+ private void updatePosition() => Position = HitObject.Position - slider.Position;
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 51b1990a21..f3846bd52f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -112,6 +112,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Alpha = 0
}
};
+
+ s.PositionChanged += _ => Position = s.Position;
}
public float Progress => MathHelper.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1);
@@ -167,7 +169,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update()
{
- Disc.Tracking = OsuActionInputManager.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton);
+ Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false;
if (!spmCounter.IsPresent && Disc.Tracking)
spmCounter.FadeIn(HitObject.TimeFadeIn);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ManualSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ManualSliderBody.cs
new file mode 100644
index 0000000000..9d239c15f2
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ManualSliderBody.cs
@@ -0,0 +1,20 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Collections.Generic;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
+{
+ ///
+ /// A with the ability to set the drawn vertices manually.
+ ///
+ public class ManualSliderBody : SliderBody
+ {
+ public new void SetVertices(IReadOnlyList vertices)
+ {
+ base.SetVertices(vertices);
+ Size = Path.Size;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs
index 30140484de..acb3ee92ff 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs
@@ -4,7 +4,6 @@
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Sprites;
using OpenTK.Graphics;
using osu.Framework.Graphics.Shapes;
@@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class NumberPiece : Container
{
- private readonly SpriteText number;
+ private readonly SkinnableSpriteText number;
public string Text
{
@@ -41,15 +40,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
},
Child = new Box()
}, s => s.GetTexture("Play/osu/hitcircle") == null),
- number = new OsuSpriteText
+ number = new SkinnableSpriteText("Play/osu/number-text", _ => new OsuSpriteText
{
- Text = @"1",
Font = @"Venera",
UseFullGlyphHeight = false,
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
TextSize = 40,
- Alpha = 1
+ }, restrictSize: false)
+ {
+ Text = @"1"
}
};
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs
index f4ccf673e9..ca2daa3adb 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs
@@ -1,24 +1,22 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-using System;
using System.Collections.Generic;
-using osu.Framework.Allocation;
-using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
-using OpenTK.Graphics.ES30;
-using OpenTK.Graphics;
using osu.Framework.Graphics.Primitives;
-using osu.Game.Rulesets.Objects.Types;
using OpenTK;
+using OpenTK.Graphics;
+using OpenTK.Graphics.ES30;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
- public class SliderBody : Container, ISliderProgress
+ public abstract class SliderBody : CompositeDrawable
{
private readonly SliderPath path;
+ protected Path Path => path;
+
private readonly BufferedContainer container;
public float PathWidth
@@ -30,15 +28,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
///
/// Offset in absolute coordinates from the start of the curve.
///
- public Vector2 PathOffset { get; private set; }
-
- public readonly List CurrentCurve = new List();
-
- public readonly Bindable SnakingIn = new Bindable();
- public readonly Bindable SnakingOut = new Bindable();
-
- public double? SnakedStart { get; private set; }
- public double? SnakedEnd { get; private set; }
+ public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]);
///
/// Used to colour the path.
@@ -74,28 +64,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public Quad PathDrawQuad => container.ScreenSpaceDrawQuad;
- private Vector2 topLeftOffset;
-
- private readonly Slider slider;
-
- public SliderBody(Slider s)
+ protected SliderBody()
{
- slider = s;
-
- Children = new Drawable[]
+ InternalChild = container = new BufferedContainer
{
- container = new BufferedContainer
- {
- RelativeSizeAxes = Axes.Both,
- CacheDrawnFrameBuffer = true,
- Children = new Drawable[]
- {
- path = new SliderPath
- {
- Blending = BlendingMode.None,
- },
- }
- },
+ RelativeSizeAxes = Axes.Both,
+ CacheDrawnFrameBuffer = true,
+ Child = path = new SliderPath { Blending = BlendingMode.None }
};
container.Attach(RenderbufferInternalFormat.DepthComponent16);
@@ -103,80 +78,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => path.ReceivePositionalInputAt(screenSpacePos);
- public void SetRange(double p0, double p1)
+ ///
+ /// Sets the vertices of the path which should be drawn by this .
+ ///
+ /// The vertices
+ protected void SetVertices(IReadOnlyList vertices)
{
- if (p0 > p1)
- MathHelper.Swap(ref p0, ref p1);
-
- if (updateSnaking(p0, p1))
- {
- // The path is generated such that its size encloses it. This change of size causes the path
- // to move around while snaking, so we need to offset it to make sure it maintains the
- // same position as when it is fully snaked.
- var newTopLeftOffset = path.PositionInBoundingBox(Vector2.Zero);
- path.Position = topLeftOffset - newTopLeftOffset;
-
- container.ForceRedraw();
- }
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- computeSize();
- }
-
- private void computeSize()
- {
- // Generate the entire curve
- slider.Curve.GetPathToProgress(CurrentCurve, 0, 1);
- foreach (Vector2 p in CurrentCurve)
- path.AddVertex(p);
-
- Size = path.Size;
-
- topLeftOffset = path.PositionInBoundingBox(Vector2.Zero);
- PathOffset = path.PositionInBoundingBox(CurrentCurve[0]);
- }
-
- private bool updateSnaking(double p0, double p1)
- {
- if (SnakedStart == p0 && SnakedEnd == p1) return false;
-
- SnakedStart = p0;
- SnakedEnd = p1;
-
- slider.Curve.GetPathToProgress(CurrentCurve, p0, p1);
-
- path.ClearVertices();
- foreach (Vector2 p in CurrentCurve)
- path.AddVertex(p);
-
- return true;
- }
-
- public void UpdateProgress(double completionProgress)
- {
- var span = slider.SpanAt(completionProgress);
- var spanProgress = slider.ProgressAt(completionProgress);
-
- double start = 0;
- double end = SnakingIn ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadeIn, 0, 1) : 1;
-
- if (span >= slider.SpanCount() - 1)
- {
- if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1)
- {
- start = 0;
- end = SnakingOut ? spanProgress : 1;
- }
- else
- {
- start = SnakingOut ? spanProgress : 0;
- }
- }
-
- SetRange(start, end);
+ path.Vertices = vertices;
+ container.ForceRedraw();
}
private class SliderPath : SmoothPath
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs
new file mode 100644
index 0000000000..0e6f3ad16c
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs
@@ -0,0 +1,118 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System;
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Game.Rulesets.Objects.Types;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
+{
+ ///
+ /// A which changes its curve depending on the snaking progress.
+ ///
+ public class SnakingSliderBody : SliderBody, ISliderProgress
+ {
+ public readonly List CurrentCurve = new List();
+
+ public readonly Bindable SnakingIn = new Bindable();
+ public readonly Bindable SnakingOut = new Bindable();
+
+ public double? SnakedStart { get; private set; }
+ public double? SnakedEnd { get; private set; }
+
+ public override Vector2 PathOffset => snakedPathOffset;
+
+ ///
+ /// The top-left position of the path when fully snaked.
+ ///
+ private Vector2 snakedPosition;
+
+ ///
+ /// The offset of the path from when fully snaked.
+ ///
+ private Vector2 snakedPathOffset;
+
+ private readonly Slider slider;
+
+ public SnakingSliderBody(Slider slider)
+ {
+ this.slider = slider;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Refresh();
+ }
+
+ public void UpdateProgress(double completionProgress)
+ {
+ var span = slider.SpanAt(completionProgress);
+ var spanProgress = slider.ProgressAt(completionProgress);
+
+ double start = 0;
+ double end = SnakingIn ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadeIn, 0, 1) : 1;
+
+ if (span >= slider.SpanCount() - 1)
+ {
+ if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1)
+ {
+ start = 0;
+ end = SnakingOut ? spanProgress : 1;
+ }
+ else
+ {
+ start = SnakingOut ? spanProgress : 0;
+ }
+ }
+
+ setRange(start, end);
+ }
+
+ public void Refresh()
+ {
+ // Generate the entire curve
+ slider.Path.GetPathToProgress(CurrentCurve, 0, 1);
+ SetVertices(CurrentCurve);
+
+ // The body is sized to the full path size to avoid excessive autosize computations
+ Size = Path.Size;
+
+ snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
+ snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
+
+ var lastSnakedStart = SnakedStart ?? 0;
+ var lastSnakedEnd = SnakedEnd ?? 0;
+
+ SnakedStart = null;
+ SnakedEnd = null;
+
+ setRange(lastSnakedStart, lastSnakedEnd);
+ }
+
+ private void setRange(double p0, double p1)
+ {
+ if (p0 > p1)
+ MathHelper.Swap(ref p0, ref p1);
+
+ if (SnakedStart == p0 && SnakedEnd == p1) return;
+
+ SnakedStart = p0;
+ SnakedEnd = p1;
+
+ slider.Path.GetPathToProgress(CurrentCurve, p0, p1);
+
+ SetVertices(CurrentCurve);
+
+ // The bounding box of the path expands as it snakes, which in turn shifts the position of the path.
+ // Depending on the direction of expansion, it may appear as if the path is expanding towards the position of the slider
+ // rather than expanding out from the position of the slider.
+ // To remove this effect, the path's position is shifted towards its final snaked position
+
+ Path.Position = snakedPosition - Path.PositionInBoundingBox(Vector2.Zero);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
index fdf5aaffa8..61d199a7dc 100644
--- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
+++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs
@@ -7,22 +7,23 @@ using osu.Game.Rulesets.Objects;
using OpenTK;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Rulesets.Edit.Types;
namespace osu.Game.Rulesets.Osu.Objects
{
- public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasEditablePosition
+ public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition
{
public const double OBJECT_RADIUS = 64;
public event Action PositionChanged;
+ public event Action StackHeightChanged;
+ public event Action ScaleChanged;
public double TimePreempt = 600;
public double TimeFadeIn = 400;
private Vector2 position;
- public Vector2 Position
+ public virtual Vector2 Position
{
get => position;
set
@@ -44,13 +45,39 @@ namespace osu.Game.Rulesets.Osu.Objects
public Vector2 StackedEndPosition => EndPosition + StackOffset;
- public virtual int StackHeight { get; set; }
+ private int stackHeight;
+
+ public int StackHeight
+ {
+ get => stackHeight;
+ set
+ {
+ if (stackHeight == value)
+ return;
+ stackHeight = value;
+
+ StackHeightChanged?.Invoke(value);
+ }
+ }
public Vector2 StackOffset => new Vector2(StackHeight * Scale * -6.4f);
public double Radius => OBJECT_RADIUS * Scale;
- public float Scale { get; set; } = 1;
+ private float scale = 1;
+
+ public float Scale
+ {
+ get => scale;
+ set
+ {
+ if (scale == value)
+ return;
+ scale = value;
+
+ ScaleChanged?.Invoke(value);
+ }
+ }
public virtual bool NewCombo { get; set; }
@@ -72,8 +99,6 @@ namespace osu.Game.Rulesets.Osu.Objects
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}
- public virtual void OffsetPosition(Vector2 offset) => Position += offset;
-
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index 80be192b77..cf57f24b83 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -22,7 +22,9 @@ namespace osu.Game.Rulesets.Osu.Objects
///
private const float base_scoring_distance = 100;
- public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity;
+ public event Action PathChanged;
+
+ public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
public double Duration => EndTime - StartTime;
public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t);
@@ -50,24 +52,37 @@ namespace osu.Game.Rulesets.Osu.Objects
}
}
- public SliderCurve Curve { get; } = new SliderCurve();
+ private SliderPath path;
- public Vector2[] ControlPoints
+ public SliderPath Path
{
- get { return Curve.ControlPoints; }
- set { Curve.ControlPoints = value; }
+ get => path;
+ set
+ {
+ path = value;
+
+ PathChanged?.Invoke(value);
+
+ if (TailCircle != null)
+ TailCircle.Position = EndPosition;
+ }
}
- public CurveType CurveType
- {
- get { return Curve.CurveType; }
- set { Curve.CurveType = value; }
- }
+ public double Distance => Path.Distance;
- public double Distance
+ public override Vector2 Position
{
- get { return Curve.Distance; }
- set { Curve.Distance = value; }
+ get => base.Position;
+ set
+ {
+ base.Position = value;
+
+ if (HeadCircle != null)
+ HeadCircle.Position = value;
+
+ if (TailCircle != null)
+ TailCircle.Position = EndPosition;
+ }
}
public double? LegacyLastTickOffset { get; set; }
@@ -84,7 +99,8 @@ namespace osu.Game.Rulesets.Osu.Objects
///
internal float LazyTravelDistance;
- public List> RepeatSamples { get; set; } = new List>();
+ public List> NodeSamples { get; set; } = new List>();
+
public int RepeatCount { get; set; }
///
@@ -138,17 +154,17 @@ namespace osu.Game.Rulesets.Osu.Objects
private void createSliderEnds()
{
- HeadCircle = new SliderCircle(this)
+ HeadCircle = new SliderCircle
{
StartTime = StartTime,
Position = Position,
- Samples = Samples,
+ Samples = getNodeSamples(0),
SampleControlPoint = SampleControlPoint,
IndexInCurrentCombo = IndexInCurrentCombo,
ComboIndex = ComboIndex,
};
- TailCircle = new SliderTailCircle(this)
+ TailCircle = new SliderTailCircle
{
StartTime = EndTime,
Position = EndPosition,
@@ -162,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Objects
private void createTicks()
{
- var length = Curve.Distance;
+ var length = Path.Distance;
var tickDistance = MathHelper.Clamp(TickDistance, 0, length);
if (tickDistance == 0) return;
@@ -201,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Objects
SpanIndex = span,
SpanStartTime = spanStartTime,
StartTime = spanStartTime + timeProgress * SpanDuration,
- Position = Position + Curve.PositionAt(distanceProgress),
+ Position = Position + Path.PositionAt(distanceProgress),
StackHeight = StackHeight,
Scale = Scale,
Samples = sampleList
@@ -219,14 +235,21 @@ namespace osu.Game.Rulesets.Osu.Objects
RepeatIndex = repeatIndex,
SpanDuration = SpanDuration,
StartTime = StartTime + repeat * SpanDuration,
- Position = Position + Curve.PositionAt(repeat % 2),
+ Position = Position + Path.PositionAt(repeat % 2),
StackHeight = StackHeight,
Scale = Scale,
- Samples = new List(RepeatSamples[repeatIndex])
+ Samples = getNodeSamples(1 + repeatIndex)
});
}
}
+ private List getNodeSamples(int nodeIndex)
+ {
+ if (nodeIndex < NodeSamples.Count)
+ return NodeSamples[nodeIndex];
+ return Samples;
+ }
+
public override Judgement CreateJudgement() => new OsuJudgement();
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs
index 1bdd16c9df..deda951378 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs
@@ -1,19 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-using OpenTK;
-
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderCircle : HitCircle
{
- private readonly Slider slider;
-
- public SliderCircle(Slider slider)
- {
- this.slider = slider;
- }
-
- public override void OffsetPosition(Vector2 offset) => slider.OffsetPosition(offset);
}
}
diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
index 23616ea005..b567bd8423 100644
--- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs
@@ -8,11 +8,6 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderTailCircle : SliderCircle
{
- public SliderTailCircle(Slider slider)
- : base(slider)
- {
- }
-
public override Judgement CreateJudgement() => new OsuSliderTailJudgement();
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 398680cb8d..a94d1e9039 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -84,5 +84,7 @@ namespace osu.Game.Rulesets.Osu.UI
judgementLayer.Add(explosion);
}
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
index 0ea8d3ede8..ea5718bed2 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.UI
public override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo);
- protected override DrawableHitObject GetVisualRepresentation(OsuHitObject h)
+ public override DrawableHitObject GetVisualRepresentation(OsuHitObject h)
{
switch (h)
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 6ae9a018c5..3ba64398f3 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -4,7 +4,7 @@
-
+
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index c2cde332e8..c4a84f416e 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
{
- List> allSamples = curveData != null ? curveData.RepeatSamples : new List>(new[] { samples });
+ List> allSamples = curveData != null ? curveData.NodeSamples : new List>(new[] { samples });
int i = 0;
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
index f530b6725c..734d5d0ad7 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs
@@ -8,6 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
@@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (mods.Any(m => m is ModHidden))
strainValue *= 1.025;
- if (mods.Any(m => m is ModFlashlight))
+ if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
strainValue *= 1.05 * lengthBonus;
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index 49f7786f59..5e865d7727 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -1,12 +1,80 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.UI;
+using osu.Game.Rulesets.UI;
+using OpenTK;
namespace osu.Game.Rulesets.Taiko.Mods
{
- public class TaikoModFlashlight : ModFlashlight
+ public class TaikoModFlashlight : ModFlashlight
{
public override double ScoreMultiplier => 1.12;
+
+ private const float default_flashlight_size = 250;
+
+ public override Flashlight CreateFlashlight() => new TaikoFlashlight(playfield);
+
+ private TaikoPlayfield playfield;
+
+ public override void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ {
+ playfield = (TaikoPlayfield)rulesetContainer.Playfield;
+ base.ApplyToRulesetContainer(rulesetContainer);
+ }
+
+ private class TaikoFlashlight : Flashlight
+ {
+ private readonly Cached flashlightProperties = new Cached();
+ private readonly TaikoPlayfield taikoPlayfield;
+
+ public TaikoFlashlight(TaikoPlayfield taikoPlayfield)
+ {
+ this.taikoPlayfield = taikoPlayfield;
+ FlashlightSize = new Vector2(0, getSizeFor(0));
+ }
+
+ private float getSizeFor(int combo)
+ {
+ if (combo > 200)
+ return default_flashlight_size * 0.8f;
+ else if (combo > 100)
+ return default_flashlight_size * 0.9f;
+ else
+ return default_flashlight_size;
+ }
+
+ protected override void OnComboChange(int newCombo)
+ {
+ this.TransformTo(nameof(FlashlightSize), new Vector2(0, getSizeFor(newCombo)), FLASHLIGHT_FADE_DURATION);
+ }
+
+ protected override string FragmentShader => "CircularFlashlight";
+
+ public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
+ {
+ if ((invalidation & Invalidation.DrawSize) > 0)
+ {
+ flashlightProperties.Invalidate();
+ }
+
+ return base.Invalidate(invalidation, source, shallPropagate);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!flashlightProperties.IsValid)
+ {
+ FlashlightPosition = taikoPlayfield.HitTarget.ToSpaceOfOtherDrawable(taikoPlayfield.HitTarget.OriginPosition, this);
+ flashlightProperties.Validate();
+ }
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
index 40ed659bd6..84e48448e0 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs
@@ -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,13 +38,10 @@ namespace osu.Game.Rulesets.Taiko.UI
///
private const float left_area_size = 240;
- protected override bool UserScrollSpeedAdjustment => false;
-
- protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Overlapping;
-
private readonly Container hitExplosionContainer;
private readonly Container kiaiExplosionContainer;
private readonly JudgementContainer judgementContainer;
+ internal readonly HitTarget HitTarget;
private readonly Container topLevelHitContainer;
@@ -59,8 +55,6 @@ namespace osu.Game.Rulesets.Taiko.UI
public TaikoPlayfield(ControlPointInfo controlPoints)
{
- Direction.Value = ScrollingDirection.Left;
-
InternalChild = new PlayfieldAdjustmentContainer
{
Anchor = Anchor.CentreLeft,
@@ -109,7 +103,7 @@ namespace osu.Game.Rulesets.Taiko.UI
FillMode = FillMode.Fit,
Blending = BlendingMode.Additive,
},
- new HitTarget
+ HitTarget = new HitTarget
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
@@ -200,8 +194,6 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
};
-
- VisibleTimeRange.Value = 7000;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs
index 3d08bffe0f..99c83c243b 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs
@@ -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
{
+ 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]
@@ -78,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo);
- protected override DrawableHitObject GetVisualRepresentation(TaikoHitObject h)
+ public override DrawableHitObject GetVisualRepresentation(TaikoHitObject h)
{
switch (h)
{
diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
index af63a39662..464cfbf5e9 100644
--- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
+++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs
@@ -8,11 +8,14 @@ using OpenTK.Graphics;
using osu.Game.Tests.Resources;
using System.Linq;
using osu.Game.Audio;
+using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Legacy;
+using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Skinning;
@@ -21,6 +24,25 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestFixture]
public class LegacyBeatmapDecoderTest
{
+ [Test]
+ public void TestDecodeBeatmapVersion()
+ {
+ using (var resStream = Resource.OpenResource("beatmap-version.osu"))
+ using (var stream = new StreamReader(resStream))
+ {
+ var decoder = Decoder.GetDecoder(stream);
+
+ stream.BaseStream.Position = 0;
+ stream.DiscardBufferedData();
+
+ var working = new TestWorkingBeatmap(decoder.Decode(stream));
+
+ Assert.AreEqual(6, working.BeatmapInfo.BeatmapVersion);
+ Assert.AreEqual(6, working.Beatmap.BeatmapInfo.BeatmapVersion);
+ Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapVersion);
+ }
+ }
+
[Test]
public void TestDecodeBeatmapGeneral()
{
@@ -312,5 +334,61 @@ namespace osu.Game.Tests.Beatmaps.Formats
SampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]);
}
+
+ [Test]
+ public void TestDecodeSliderSamples()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+ using (var resStream = Resource.OpenResource("slider-samples.osu"))
+ using (var stream = new StreamReader(resStream))
+ {
+ var hitObjects = decoder.Decode(stream).HitObjects;
+
+ var slider1 = (ConvertSlider)hitObjects[0];
+
+ Assert.AreEqual(1, slider1.NodeSamples[0].Count);
+ Assert.AreEqual(SampleInfo.HIT_NORMAL, slider1.NodeSamples[0][0].Name);
+ Assert.AreEqual(1, slider1.NodeSamples[1].Count);
+ Assert.AreEqual(SampleInfo.HIT_NORMAL, slider1.NodeSamples[1][0].Name);
+ Assert.AreEqual(1, slider1.NodeSamples[2].Count);
+ Assert.AreEqual(SampleInfo.HIT_NORMAL, slider1.NodeSamples[2][0].Name);
+
+ var slider2 = (ConvertSlider)hitObjects[1];
+
+ Assert.AreEqual(2, slider2.NodeSamples[0].Count);
+ Assert.AreEqual(SampleInfo.HIT_NORMAL, slider2.NodeSamples[0][0].Name);
+ Assert.AreEqual(SampleInfo.HIT_CLAP, slider2.NodeSamples[0][1].Name);
+ Assert.AreEqual(2, slider2.NodeSamples[1].Count);
+ Assert.AreEqual(SampleInfo.HIT_NORMAL, slider2.NodeSamples[1][0].Name);
+ Assert.AreEqual(SampleInfo.HIT_CLAP, slider2.NodeSamples[1][1].Name);
+ Assert.AreEqual(2, slider2.NodeSamples[2].Count);
+ Assert.AreEqual(SampleInfo.HIT_NORMAL, slider2.NodeSamples[2][0].Name);
+ Assert.AreEqual(SampleInfo.HIT_CLAP, slider2.NodeSamples[2][1].Name);
+
+ var slider3 = (ConvertSlider)hitObjects[2];
+
+ Assert.AreEqual(2, slider3.NodeSamples[0].Count);
+ Assert.AreEqual(SampleInfo.HIT_NORMAL, slider3.NodeSamples[0][0].Name);
+ Assert.AreEqual(SampleInfo.HIT_WHISTLE, slider3.NodeSamples[0][1].Name);
+ Assert.AreEqual(1, slider3.NodeSamples[1].Count);
+ Assert.AreEqual(SampleInfo.HIT_NORMAL, slider3.NodeSamples[1][0].Name);
+ Assert.AreEqual(2, slider3.NodeSamples[2].Count);
+ Assert.AreEqual(SampleInfo.HIT_NORMAL, slider3.NodeSamples[2][0].Name);
+ Assert.AreEqual(SampleInfo.HIT_CLAP, slider3.NodeSamples[2][1].Name);
+ }
+ }
+
+ [Test]
+ public void TestDecodeHitObjectNullAdditionBank()
+ {
+ var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
+ using (var resStream = Resource.OpenResource("hitobject-no-addition-bank.osu"))
+ using (var stream = new StreamReader(resStream))
+ {
+ var hitObjects = decoder.Decode(stream).HitObjects;
+
+ Assert.AreEqual(hitObjects[0].Samples[0].Bank, hitObjects[0].Samples[1].Bank);
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Resources/beatmap-version.osu b/osu.Game.Tests/Resources/beatmap-version.osu
new file mode 100644
index 0000000000..5749054ac4
--- /dev/null
+++ b/osu.Game.Tests/Resources/beatmap-version.osu
@@ -0,0 +1 @@
+osu file format v6
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/hitobject-no-addition-bank.osu b/osu.Game.Tests/Resources/hitobject-no-addition-bank.osu
new file mode 100644
index 0000000000..43d0b8cc16
--- /dev/null
+++ b/osu.Game.Tests/Resources/hitobject-no-addition-bank.osu
@@ -0,0 +1,4 @@
+osu file format v14
+
+[HitObjects]
+444,320,1000,5,2,3:0:1:0:
\ No newline at end of file
diff --git a/osu.Game.Tests/Resources/slider-samples.osu b/osu.Game.Tests/Resources/slider-samples.osu
new file mode 100644
index 0000000000..7759a2e35f
--- /dev/null
+++ b/osu.Game.Tests/Resources/slider-samples.osu
@@ -0,0 +1,23 @@
+osu file format v14
+
+[General]
+SampleSet: Normal
+
+[Difficulty]
+
+SliderMultiplier:1.6
+SliderTickRate:2
+
+[TimingPoints]
+24735,389.61038961039,4,1,1,25,1,0
+
+[HitObjects]
+// Unified: Normal, Normal, Normal
+168,256,30579,6,0,B|248:320|320:248,2,160
+
+// Unified: Normal+Clap, Normal+Clap, Normal+Clap
+168,256,32137,6,8,B|248:320|320:248,2,160
+
+// Nodal: Normal+Whistle, Normal, Normal+Clap
+// Nodal sounds should override the unified clap sound
+168,256,33696,6,8,B|248:320|320:248,2,160,2|0|8
\ No newline at end of file
diff --git a/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs
new file mode 100644
index 0000000000..5e01213a48
--- /dev/null
+++ b/osu.Game.Tests/ScrollAlgorithms/ConstantScrollTest.cs
@@ -0,0 +1,54 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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);
+ }
+ }
+}
diff --git a/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs
new file mode 100644
index 0000000000..c1a5a0f3c9
--- /dev/null
+++ b/osu.Game.Tests/ScrollAlgorithms/OverlappingScrollTest.cs
@@ -0,0 +1,67 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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
+ {
+ 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);
+ }
+ }
+}
diff --git a/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs b/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs
new file mode 100644
index 0000000000..990fb92e6c
--- /dev/null
+++ b/osu.Game.Tests/ScrollAlgorithms/SequentialScrollTest.cs
@@ -0,0 +1,64 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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
+ {
+ 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);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/TestCaseBeatDivisorControl.cs b/osu.Game.Tests/Visual/TestCaseBeatDivisorControl.cs
index 1effa14e76..6c607acd11 100644
--- a/osu.Game.Tests/Visual/TestCaseBeatDivisorControl.cs
+++ b/osu.Game.Tests/Visual/TestCaseBeatDivisorControl.cs
@@ -5,7 +5,8 @@ using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Game.Screens.Edit.Screens.Compose;
+using osu.Game.Screens.Edit;
+using osu.Game.Screens.Edit.Compose.Components;
using OpenTK;
namespace osu.Game.Tests.Visual
diff --git a/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
new file mode 100644
index 0000000000..e6a04bf5c6
--- /dev/null
+++ b/osu.Game.Tests/Visual/TestCaseChannelTabControl.cs
@@ -0,0 +1,123 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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 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 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);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/TestCaseChatDisplay.cs b/osu.Game.Tests/Visual/TestCaseChatDisplay.cs
index c03b12bdc1..e3bd4026b3 100644
--- a/osu.Game.Tests/Visual/TestCaseChatDisplay.cs
+++ b/osu.Game.Tests/Visual/TestCaseChatDisplay.cs
@@ -1,21 +1,45 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// 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 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 }
+ };
}
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseChatLink.cs b/osu.Game.Tests/Visual/TestCaseChatLink.cs
index 6240062aab..37135a2a1c 100644
--- a/osu.Game.Tests/Visual/TestCaseChatLink.cs
+++ b/osu.Game.Tests/Visual/TestCaseChatLink.cs
@@ -52,15 +52,14 @@ namespace osu.Game.Tests.Visual
private void load(OsuColour colours)
{
linkColour = colours.Blue;
+
+ 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());
Dependencies.Cache(dialogOverlay);
- Dependencies.Cache(new ChatOverlay
- {
- AvailableChannels =
- {
- new Channel { Name = "#english" },
- new Channel { Name = "#japanese" }
- }
- });
testLinksGeneral();
testEcho();
diff --git a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs
index e7bcfbf500..5c53fdfac4 100644
--- a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs
+++ b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs
@@ -6,7 +6,7 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Osu;
-using osu.Game.Screens.Edit.Screens.Compose;
+using osu.Game.Screens.Edit.Compose;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual
@@ -14,13 +14,13 @@ namespace osu.Game.Tests.Visual
[TestFixture]
public class TestCaseEditorCompose : EditorClockTestCase
{
- public override IReadOnlyList RequiredTypes => new[] { typeof(Compose) };
+ public override IReadOnlyList RequiredTypes => new[] { typeof(ComposeScreen) };
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo);
- Child = new Compose();
+ Child = new ComposeScreen();
}
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs
index 09f390ab74..9df36b0bc1 100644
--- a/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs
+++ b/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs
@@ -5,7 +5,7 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Game.Screens.Edit.Screens.Compose.RadioButtons;
+using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Tests.Visual
{
diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs
index 9ad8bf7b92..d2c1127f4c 100644
--- a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs
+++ b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs
@@ -13,7 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
-using osu.Game.Screens.Edit.Screens.Compose.Timeline;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
using OpenTK.Graphics;
namespace osu.Game.Tests.Visual
diff --git a/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs b/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs
index cb4438b2ba..eab799011d 100644
--- a/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs
+++ b/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs
@@ -8,7 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
-using osu.Game.Screens.Edit.Menus;
+using osu.Game.Screens.Edit.Components.Menus;
namespace osu.Game.Tests.Visual
{
diff --git a/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs b/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs
index 825aef75c9..d894d2738e 100644
--- a/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/TestCaseHitObjectComposer.cs
@@ -11,27 +11,37 @@ 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;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Screens.Edit.Screens.Compose.Layers;
+using osu.Game.Screens.Edit.Compose;
+using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual
{
[TestFixture]
- public class TestCaseHitObjectComposer : OsuTestCase
+ [Cached(Type = typeof(IPlacementHandler))]
+ public class TestCaseHitObjectComposer : OsuTestCase, IPlacementHandler
{
public override IReadOnlyList RequiredTypes => new[]
{
- typeof(MaskSelection),
- typeof(DragLayer),
+ typeof(SelectionBox),
+ typeof(DragBox),
typeof(HitObjectComposer),
typeof(OsuHitObjectComposer),
- typeof(HitObjectMaskLayer),
- typeof(NotNullAttribute)
+ typeof(BlueprintContainer),
+ typeof(NotNullAttribute),
+ typeof(HitCirclePiece),
+ typeof(HitCircleSelectionBlueprint),
+ typeof(HitCirclePlacementBlueprint),
};
+ private HitObjectComposer composer;
+
[BackgroundDependencyLoader]
private void load()
{
@@ -44,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,
}
},
@@ -59,7 +68,15 @@ namespace osu.Game.Tests.Visual
Dependencies.CacheAs(clock);
Dependencies.CacheAs(clock);
- Child = new OsuHitObjectComposer(new OsuRuleset());
+ Child = composer = new OsuHitObjectComposer(new OsuRuleset());
}
+
+ public void BeginPlacement(HitObject hitObject)
+ {
+ }
+
+ public void EndPlacement(HitObject hitObject) => composer.Add(hitObject);
+
+ public void Delete(HitObject hitObject) => composer.Remove(hitObject);
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseQuitButton.cs b/osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs
similarity index 76%
rename from osu.Game.Tests/Visual/TestCaseQuitButton.cs
rename to osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs
index a427b7a20a..019472f127 100644
--- a/osu.Game.Tests/Visual/TestCaseQuitButton.cs
+++ b/osu.Game.Tests/Visual/TestCaseHoldForMenuButton.cs
@@ -13,25 +13,25 @@ using OpenTK.Input;
namespace osu.Game.Tests.Visual
{
[Description("'Hold to Quit' UI element")]
- public class TestCaseQuitButton : ManualInputManagerTestCase
+ public class TestCaseHoldForMenuButton : ManualInputManagerTestCase
{
private bool exitAction;
[BackgroundDependencyLoader]
private void load()
{
- QuitButton quitButton;
+ HoldForMenuButton holdForMenuButton;
- Add(quitButton = new QuitButton
+ Add(holdForMenuButton = new HoldForMenuButton
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
Action = () => exitAction = true
});
- var text = quitButton.Children.OfType().First();
+ var text = holdForMenuButton.Children.OfType().First();
- AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(quitButton));
+ AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(holdForMenuButton));
AddUntilStep(() => text.IsPresent && !exitAction, "Text visible");
AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One));
AddUntilStep(() => !text.IsPresent && !exitAction, "Text is not visible");
@@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual
AddStep("Trigger exit action", () =>
{
exitAction = false;
- InputManager.MoveMouseTo(quitButton);
+ InputManager.MoveMouseTo(holdForMenuButton);
InputManager.PressButton(MouseButton.Left);
});
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual
AddAssert("action not triggered", () => !exitAction);
AddStep("Trigger exit action", () => InputManager.PressButton(MouseButton.Left));
- AddUntilStep(() => exitAction, $"{nameof(quitButton.Action)} was triggered");
+ AddUntilStep(() => exitAction, $"{nameof(holdForMenuButton.Action)} was triggered");
}
}
}
diff --git a/osu.Game.Tests/Visual/TestCaseLabelledTextBox.cs b/osu.Game.Tests/Visual/TestCaseLabelledTextBox.cs
index d41739bfb5..e1470a860e 100644
--- a/osu.Game.Tests/Visual/TestCaseLabelledTextBox.cs
+++ b/osu.Game.Tests/Visual/TestCaseLabelledTextBox.cs
@@ -5,9 +5,9 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Screens.Edit.Screens.Setup.Components.LabelledComponents;
using System;
using System.Collections.Generic;
+using osu.Game.Screens.Edit.Setup.Components.LabelledComponents;
namespace osu.Game.Tests.Visual
{
diff --git a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs
index b254325472..a486abb9e8 100644
--- a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs
@@ -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 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[]
diff --git a/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs b/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs
index 8bd1b79a84..3bf809ebde 100644
--- a/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/TestCaseZoomableScrollContainer.cs
@@ -10,7 +10,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.MathUtils;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
-using osu.Game.Screens.Edit.Screens.Compose.Timeline;
+using osu.Game.Screens.Edit.Compose.Components.Timeline;
using OpenTK;
using OpenTK.Graphics;
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 520e0b8940..c0f0695ff8 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -5,7 +5,7 @@
-
+
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 1aa4818393..3e1f3bdf54 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -19,7 +19,6 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
public int ID { get; set; }
- //TODO: should be in database
public int BeatmapVersion;
private int? onlineBeatmapID;
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index fad94dcdfd..24c68d392b 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -350,7 +350,7 @@ namespace osu.Game.Beatmaps
OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID,
Beatmaps = new List(),
Hash = computeBeatmapSetHash(reader),
- Metadata = beatmap.Metadata
+ Metadata = beatmap.Metadata,
};
}
diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
index 25a76b52a7..5c129f76ec 100644
--- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs
@@ -26,15 +26,7 @@ namespace osu.Game.Beatmaps
Title = "no beatmaps available!"
},
BeatmapSet = new BeatmapSetInfo(),
- BaseDifficulty = new BeatmapDifficulty
- {
- DrainRate = 0,
- CircleSize = 0,
- OverallDifficulty = 0,
- ApproachRate = 0,
- SliderMultiplier = 0,
- SliderTickRate = 0,
- },
+ BaseDifficulty = new BeatmapDifficulty(),
Ruleset = new DummyRulesetInfo()
})
{
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index e0a22460ef..5b76122616 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -41,8 +41,13 @@ namespace osu.Game.Beatmaps
beatmap = new RecyclableLazy(() =>
{
var b = GetBeatmap() ?? new Beatmap();
- // use the database-backed info.
+
+ // The original beatmap version needs to be preserved as the database doesn't contain it
+ BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion;
+
+ // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc)
b.BeatmapInfo = BeatmapInfo;
+
return b;
});
diff --git a/osu.Game/Configuration/SpeedChangeVisualisationMethod.cs b/osu.Game/Configuration/ScrollVisualisationMethod.cs
similarity index 89%
rename from osu.Game/Configuration/SpeedChangeVisualisationMethod.cs
rename to osu.Game/Configuration/ScrollVisualisationMethod.cs
index 39c6e5649c..cc7dcdbc0e 100644
--- a/osu.Game/Configuration/SpeedChangeVisualisationMethod.cs
+++ b/osu.Game/Configuration/ScrollVisualisationMethod.cs
@@ -5,7 +5,7 @@ using System.ComponentModel;
namespace osu.Game.Configuration
{
- public enum SpeedChangeVisualisationMethod
+ public enum ScrollVisualisationMethod
{
[Description("Sequential")]
Sequential,
diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
index c3d58e940e..54a2ea47f9 100644
--- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs
+++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs
@@ -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.Game.Overlays;
using osu.Game.Overlays.Notifications;
@@ -20,14 +21,15 @@ namespace osu.Game.Graphics.Containers
}
private OsuGame game;
-
+ private ChannelManager channelManager;
private Action showNotImplementedError;
[BackgroundDependencyLoader(true)]
- private void load(OsuGame game, NotificationOverlay notifications)
+ private void load(OsuGame game, NotificationOverlay notifications, ChannelManager channelManager)
{
// will be null in tests
this.game = game;
+ this.channelManager = channelManager;
showNotImplementedError = () => notifications?.Post(new SimpleNotification
{
@@ -77,7 +79,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:
diff --git a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
index 502f468ec9..5c6a2a0569 100644
--- a/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
+++ b/osu.Game/Graphics/UserInterface/OsuEnumDropdown.cs
@@ -2,9 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
-using System.ComponentModel;
-using System.Reflection;
-using System.Collections.Generic;
namespace osu.Game.Graphics.UserInterface
{
@@ -15,18 +12,7 @@ namespace osu.Game.Graphics.UserInterface
if (!typeof(T).IsEnum)
throw new InvalidOperationException("OsuEnumDropdown only supports enums as the generic type argument");
- List> items = new List>();
- foreach (var val in (T[])Enum.GetValues(typeof(T)))
- {
- var field = typeof(T).GetField(Enum.GetName(typeof(T), val));
- items.Add(
- new KeyValuePair(
- field.GetCustomAttribute()?.Description ?? Enum.GetName(typeof(T), val),
- val
- )
- );
- }
- Items = items;
+ Items = (T[])Enum.GetValues(typeof(T));
}
}
}
diff --git a/osu.Game/Input/IdleTracker.cs b/osu.Game/Input/IdleTracker.cs
new file mode 100644
index 0000000000..bbc15fe7af
--- /dev/null
+++ b/osu.Game/Input/IdleTracker.cs
@@ -0,0 +1,41 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Framework.Graphics;
+using osu.Framework.Input;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+
+namespace osu.Game.Input
+{
+ public class IdleTracker : Component, IKeyBindingHandler
+ {
+ private double lastInteractionTime;
+ public double IdleTime => Clock.CurrentTime - lastInteractionTime;
+
+ private bool updateLastInteractionTime()
+ {
+ lastInteractionTime = Clock.CurrentTime;
+ return false;
+ }
+
+ public bool OnPressed(PlatformAction action) => updateLastInteractionTime();
+
+ public bool OnReleased(PlatformAction action) => updateLastInteractionTime();
+
+ protected override bool Handle(UIEvent e)
+ {
+ switch (e)
+ {
+ case KeyDownEvent _:
+ case KeyUpEvent _:
+ case MouseDownEvent _:
+ case MouseUpEvent _:
+ case MouseMoveEvent _:
+ return updateLastInteractionTime();
+ default:
+ return base.Handle(e);
+ }
+ }
+ }
+}
diff --git a/osu.Game/Online/API/APIMessagesRequest.cs b/osu.Game/Online/API/APIMessagesRequest.cs
new file mode 100644
index 0000000000..991096c0af
--- /dev/null
+++ b/osu.Game/Online/API/APIMessagesRequest.cs
@@ -0,0 +1,28 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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>
+ {
+ 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;
+ }
+ }
+}
diff --git a/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs
new file mode 100644
index 0000000000..00dab10c75
--- /dev/null
+++ b/osu.Game/Online/API/Requests/CreateNewPrivateMessageRequest.cs
@@ -0,0 +1,34 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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
+ {
+ 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";
+ }
+}
diff --git a/osu.Game/Online/API/Requests/CreateNewPrivateMessageResponse.cs b/osu.Game/Online/API/Requests/CreateNewPrivateMessageResponse.cs
new file mode 100644
index 0000000000..84f1593c31
--- /dev/null
+++ b/osu.Game/Online/API/Requests/CreateNewPrivateMessageResponse.cs
@@ -0,0 +1,16 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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;
+ }
+}
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index 3c808d1bee..ffea7b83e1 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -1,16 +1,14 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-using System.Collections.Generic;
using System.ComponentModel;
-using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Direct;
using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
- public class SearchBeatmapSetsRequest : APIRequest>
+ public class SearchBeatmapSetsRequest : APIRequest
{
private readonly string query;
private readonly RulesetInfo ruleset;
@@ -35,6 +33,7 @@ namespace osu.Game.Online.API.Requests
public enum BeatmapSearchCategory
{
Any = 7,
+
[Description("Ranked & Approved")]
RankedApproved = 0,
Approved = 1,
@@ -43,6 +42,7 @@ namespace osu.Game.Online.API.Requests
Qualified = 3,
Pending = 4,
Graveyard = 5,
+
[Description("My Maps")]
MyMaps = 6,
}
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs
new file mode 100644
index 0000000000..cf8b40d068
--- /dev/null
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsResponse.cs
@@ -0,0 +1,20 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class SearchBeatmapSetsResponse
+ {
+ public IEnumerable BeatmapSets;
+
+ ///
+ /// A collection of parameters which should be passed to the search endpoint to fetch the next page.
+ ///
+ [JsonProperty("cursor")]
+ public dynamic CursorJson;
+ }
+}
diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs
index bbe74fcac0..9d3b7b5cc9 100644
--- a/osu.Game/Online/Chat/Channel.cs
+++ b/osu.Game/Online/Chat/Channel.cs
@@ -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;
+
+ ///
+ /// Contains every joined user except the current logged in user. Currently only returned for PM channels.
+ ///
+ public readonly ObservableCollection Users = new ObservableCollection();
+
+ [JsonProperty(@"users")]
+ private long[] userIds
+ {
+ set
+ {
+ foreach (var id in value)
+ Users.Add(new User { Id = id });
+ }
+ }
+
+ ///
+ /// Contains all the messages send in the channel.
+ ///
+ public readonly SortedList Messages = new SortedList(Comparer.Default);
+
+ ///
+ /// Contains all the messages that are still pending for submission to the server.
+ ///
+ private readonly List pendingMessages = new List();
+
+
+ ///
+ /// An event that fires when new messages arrived.
+ ///
+ public event Action> NewMessagesArrived;
+
+ ///
+ /// An event that fires when a pending message gets resolved.
+ ///
+ public event Action PendingMessageResolved;
+
+ ///
+ /// An event that fires when a pending message gets removed.
+ ///
+ public event Action 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 Messages = new SortedList(Comparer.Default);
-
- private readonly List pendingMessages = new List();
-
+ ///
+ /// Signalles if the current user joined this channel or not. Defaults to false.
+ ///
public Bindable Joined = new Bindable();
- public bool ReadOnly => false;
-
public const int MAX_HISTORY = 300;
[JsonConstructor]
@@ -42,10 +88,10 @@ namespace osu.Game.Online.Chat
{
}
- public event Action> NewMessagesArrived;
- public event Action PendingMessageResolved;
- public event Action MessageRemoved;
-
+ ///
+ /// Adds the argument message as a local echo. When this local echo is resolved will get called.
+ ///
+ ///
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;
+ ///
+ /// Adds new messages to the channel and purges old messages. Triggers the event.
+ ///
+ ///
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);
- }
-
///
/// Replace or remove a message from the channel.
///
@@ -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);
+ }
}
}
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
new file mode 100644
index 0000000000..9014fce18d
--- /dev/null
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -0,0 +1,409 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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
+{
+ ///
+ /// Manages everything channel related
+ ///
+ public class ChannelManager : Component, IOnlineComponent
+ {
+ ///
+ /// The channels the player joins on startup
+ ///
+ private readonly string[] defaultChannels =
+ {
+ @"#lazer",
+ @"#osu",
+ @"#lobby"
+ };
+
+ ///
+ /// The currently opened channel
+ ///
+ public Bindable CurrentChannel { get; } = new Bindable();
+
+ ///
+ /// The Channels the player has joined
+ ///
+ public ObservableCollection JoinedChannels { get; } = new ObservableCollection(); //todo: should be publicly readonly
+
+ ///
+ /// The channels available for the player to join
+ ///
+ public ObservableCollection AvailableChannels { get; } = new ObservableCollection(); //todo: should be publicly readonly
+
+ private IAPIProvider api;
+ private ScheduledDelegate fetchMessagesScheduleder;
+
+ public ChannelManager()
+ {
+ CurrentChannel.ValueChanged += currentChannelChanged;
+ }
+
+ ///
+ /// Opens a channel or switches to the channel if already opened.
+ ///
+ /// If the name of the specifed channel was not found this exception will be thrown.
+ ///
+ 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);
+ }
+
+ ///
+ /// Opens a new private channel.
+ ///
+ /// The user the private channel is opened with.
+ 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);
+
+ ///
+ /// Ensure we run post actions in sequence, once at a time.
+ ///
+ private readonly Queue postQueue = new Queue();
+
+ ///
+ /// Posts a message to the currently opened channel.
+ ///
+ /// The message text that is going to be posted
+ /// Is true if the message is an action, e.g.: user is currently eating
+ 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();
+ }
+
+ ///
+ /// Posts a command locally. Commands like /help will result in a help message written in the current channel.
+ ///
+ /// the text containing the command identifier and command parameters.
+ 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 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The channel
+ 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);
+ }
+ }
+
+ ///
+ /// An exception thrown when a channel could not been found.
+ ///
+ public class ChannelNotFoundException : Exception
+ {
+ public ChannelNotFoundException(string channelName)
+ : base($"A channel with the name {channelName} could not be found.")
+ {
+ }
+ }
+}
diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs
index 65e0415cd3..3f0f352ac7 100644
--- a/osu.Game/Online/Chat/Message.cs
+++ b/osu.Game/Online/Chat/Message.cs
@@ -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
- }
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index d016cd3df0..0bdbab79be 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -26,6 +26,7 @@ using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
+using osu.Game.Input;
using osu.Game.Rulesets.Scoring;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
@@ -86,6 +87,8 @@ namespace osu.Game
public float ToolbarOffset => Toolbar.Position.Y + Toolbar.DrawHeight;
+ private IdleTracker idleTracker;
+
public readonly Bindable OverlayActivationMode = new Bindable();
private OsuScreen screenStack;
@@ -184,12 +187,6 @@ namespace osu.Game
private ScheduledDelegate scoreLoad;
- ///
- /// Open chat to a channel matching the provided name, if present.
- ///
- /// The name of the channel.
- public void OpenChannel(string channelName) => chat.OpenChannel(chat.AvailableChannels.Find(c => c.Name == channelName));
-
///
/// Show a beatmap set as an overlay.
///
@@ -320,6 +317,7 @@ namespace osu.Game
},
mainContent = new Container { RelativeSizeAxes = Axes.Both },
overlayContent = new Container { RelativeSizeAxes = Axes.Both, Depth = float.MinValue },
+ idleTracker = new IdleTracker { RelativeSizeAxes = Axes.Both }
});
loadComponentSingleFile(screenStack = new Loader(), d =>
@@ -347,6 +345,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
{
@@ -376,6 +379,7 @@ namespace osu.Game
Depth = -6,
}, overlayContent.Add);
+ dependencies.Cache(idleTracker);
dependencies.Cache(settings);
dependencies.Cache(onscreenDisplay);
dependencies.Cache(social);
diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs
index 1fccaeb123..770f528e17 100644
--- a/osu.Game/Overlays/Chat/ChatLine.cs
+++ b/osu.Game/Overlays/Chat/ChatLine.cs
@@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// 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),
};
}
}
diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs
index c57e71b5ad..fedfd788ce 100644
--- a/osu.Game/Overlays/Chat/DrawableChannel.cs
+++ b/osu.Game/Overlays/Chat/DrawableChannel.cs
@@ -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 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++)
{
diff --git a/osu.Game/Overlays/Chat/ChannelListItem.cs b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
similarity index 99%
rename from osu.Game/Overlays/Chat/ChannelListItem.cs
rename to osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
index 8df29c89f2..cb6caf1506 100644
--- a/osu.Game/Overlays/Chat/ChannelListItem.cs
+++ b/osu.Game/Overlays/Chat/Selection/ChannelListItem.cs
@@ -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
{
diff --git a/osu.Game/Overlays/Chat/ChannelSection.cs b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
similarity index 97%
rename from osu.Game/Overlays/Chat/ChannelSection.cs
rename to osu.Game/Overlays/Chat/Selection/ChannelSection.cs
index 85fdecaaba..ac790b424e 100644
--- a/osu.Game/Overlays/Chat/ChannelSection.cs
+++ b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs
@@ -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
{
diff --git a/osu.Game/Overlays/Chat/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
similarity index 93%
rename from osu.Game/Overlays/Chat/ChannelSelectionOverlay.cs
rename to osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
index a86465b77b..9766c0f2c5 100644
--- a/osu.Game/Overlays/Chat/ChannelSelectionOverlay.cs
+++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs
@@ -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 OnRequestJoin;
public Action OnRequestLeave;
- public IEnumerable 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 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)
{
diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs
new file mode 100644
index 0000000000..0b1721741a
--- /dev/null
+++ b/osu.Game/Overlays/Chat/Tabs/ChannelSelectorTabItem.cs
@@ -0,0 +1,32 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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;
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs
new file mode 100644
index 0000000000..08d4e40a64
--- /dev/null
+++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabControl.cs
@@ -0,0 +1,123 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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
+ {
+ public static readonly float SHEAR_WIDTH = 10;
+
+ public Action OnRequestLeave;
+
+ public readonly Bindable ChannelSelectorActive = new Bindable();
+
+ 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 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 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.");
+ }
+ }
+
+ ///
+ /// Adds a channel to the ChannelTabControl.
+ /// The first channel added will automaticly selected.
+ ///
+ /// The channel that is going to be added.
+ public void AddChannel(Channel channel)
+ {
+ if (!Items.Contains(channel))
+ AddItem(channel);
+
+ if (Current.Value == null)
+ Current.Value = channel;
+ }
+
+ ///
+ /// Removes a channel from the ChannelTabControl.
+ /// If the selected channel is the one that is beeing removed, the next available channel will be selected.
+ ///
+ /// The channel that is going to be removed.
+ public void RemoveChannel(Channel channel)
+ {
+ RemoveItem(channel);
+
+ if (Current.Value == channel)
+ Current.Value = Items.FirstOrDefault();
+ }
+
+ protected override void SelectTab(TabItem tab)
+ {
+ if (tab is ChannelSelectorTabItem)
+ {
+ tab.Active.Toggle();
+ return;
+ }
+
+ selectorTab.Active.Value = false;
+
+ base.SelectTab(tab);
+ }
+
+ private void tabCloseRequested(TabItem 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);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs
new file mode 100644
index 0000000000..e6de55f9b2
--- /dev/null
+++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs
@@ -0,0 +1,212 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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
+ {
+ 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 OnRequestClose;
+ private readonly Container content;
+
+ protected override Container 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();
+ }
+}
diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
new file mode 100644
index 0000000000..c7ca1cb073
--- /dev/null
+++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs
@@ -0,0 +1,97 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs b/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs
new file mode 100644
index 0000000000..0adaa40889
--- /dev/null
+++ b/osu.Game/Overlays/Chat/Tabs/TabCloseButton.cs
@@ -0,0 +1,55 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// 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);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs
index ff2ff9af14..e45373c36f 100644
--- a/osu.Game/Overlays/ChatOverlay.cs
+++ b/osu.Game/Overlays/ChatOverlay.cs
@@ -1,9 +1,8 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// 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 currentChannelContainer;
+ private readonly List loadedChannels = new List();
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 ChatHeight { get; set; }
- public List AvailableChannels { get; private set; } = new List();
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(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 careChannels = new List();
-
- private readonly List loadedChannels = new List();
-
- 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 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
diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs
index f63d314053..641f57d25f 100644
--- a/osu.Game/Overlays/DirectOverlay.cs
+++ b/osu.Game/Overlays/DirectOverlay.cs
@@ -288,7 +288,7 @@ namespace osu.Game.Overlays
{
Task.Run(() =>
{
- var sets = response.Select(r => r.ToBeatmapSet(rulesets)).ToList();
+ var sets = response.BeatmapSets.Select(r => r.ToBeatmapSet(rulesets)).ToList();
// may not need scheduling; loads async internally.
Schedule(() =>
diff --git a/osu.Game/Overlays/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs
index 2d492dcf1e..e4807baeb4 100644
--- a/osu.Game/Overlays/Music/FilterControl.cs
+++ b/osu.Game/Overlays/Music/FilterControl.cs
@@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -37,7 +36,7 @@ namespace osu.Game.Overlays.Music
new CollectionsDropdown
{
RelativeSizeAxes = Axes.X,
- Items = new[] { new KeyValuePair(@"All", PlaylistCollection.All) },
+ Items = new[] { PlaylistCollection.All },
}
},
},
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index b32fd265cb..f282b757cd 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -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()
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
index d637eb96f6..ad79024f62 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
@@ -6,6 +6,7 @@ using osu.Framework.Audio;
using osu.Framework.Graphics;
using System.Collections.Generic;
using System.Linq;
+using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections.Audio
{
@@ -35,12 +36,12 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
private void updateItems()
{
- var deviceItems = new List> { new KeyValuePair("Default", string.Empty) };
- deviceItems.AddRange(audio.AudioDeviceNames.Select(d => new KeyValuePair(d, d)));
+ var deviceItems = new List { string.Empty };
+ deviceItems.AddRange(audio.AudioDeviceNames);
var preferredDeviceName = audio.AudioDevice.Value;
- if (deviceItems.All(kv => kv.Value != preferredDeviceName))
- deviceItems.Add(new KeyValuePair(preferredDeviceName, preferredDeviceName));
+ if (deviceItems.All(kv => kv != preferredDeviceName))
+ deviceItems.Add(preferredDeviceName);
// The option dropdown for audio device selection lists all audio
// device names. Dropdowns, however, may not have multiple identical
@@ -59,7 +60,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
Children = new Drawable[]
{
- dropdown = new SettingsDropdown()
+ dropdown = new AudioDeviceSettingsDropdown()
};
updateItems();
@@ -69,5 +70,16 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
audio.OnNewDevice += onDeviceChanged;
audio.OnLostDevice += onDeviceChanged;
}
+
+ private class AudioDeviceSettingsDropdown : SettingsDropdown
+ {
+ protected override OsuDropdown CreateDropdown() => new AudioDeviceDropdownControl { Items = Items };
+
+ private class AudioDeviceDropdownControl : DropdownControl
+ {
+ protected override string GenerateItemText(string item)
+ => string.IsNullOrEmpty(item) ? "Default" : base.GenerateItemText(item);
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 254de6c6f7..685244e06b 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -6,9 +6,9 @@ using System.Drawing;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
-using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings.Sections.Graphics
{
@@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
if (resolutions.Count > 1)
{
- resolutionSettingsContainer.Child = resolutionDropdown = new SettingsDropdown
+ resolutionSettingsContainer.Child = resolutionDropdown = new ResolutionSettingsDropdown
{
LabelText = "Resolution",
ShowsDefaultIndicator = false,
@@ -115,18 +115,36 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}, true);
}
- private IReadOnlyList> getResolutions()
+ private IReadOnlyList getResolutions()
{
- var resolutions = new KeyValuePair("Default", new Size(9999, 9999)).Yield();
+ var resolutions = new List { new Size(9999, 9999) };
if (game.Window != null)
- resolutions = resolutions.Concat(game.Window.AvailableResolutions
- .Where(r => r.Width >= 800 && r.Height >= 600)
- .OrderByDescending(r => r.Width)
- .ThenByDescending(r => r.Height)
- .Select(res => new KeyValuePair($"{res.Width}x{res.Height}", new Size(res.Width, res.Height)))
- .Distinct());
- return resolutions.ToList();
+ {
+ resolutions.AddRange(game.Window.AvailableResolutions
+ .Where(r => r.Width >= 800 && r.Height >= 600)
+ .OrderByDescending(r => r.Width)
+ .ThenByDescending(r => r.Height)
+ .Select(res => new Size(res.Width, res.Height))
+ .Distinct());
+ }
+
+ return resolutions;
+ }
+
+ private class ResolutionSettingsDropdown : SettingsDropdown
+ {
+ protected override OsuDropdown CreateDropdown() => new ResolutionDropdownControl { Items = Items };
+
+ private class ResolutionDropdownControl : DropdownControl
+ {
+ protected override string GenerateItemText(Size item)
+ {
+ if (item == new Size(9999, 9999))
+ return "Default";
+ return $"{item.Width}x{item.Height}";
+ }
+ }
}
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 15787d29f7..af7864836b 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -1,9 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Graphics;
@@ -15,12 +15,15 @@ namespace osu.Game.Overlays.Settings.Sections
{
public class SkinSection : SettingsSection
{
- private SettingsDropdown skinDropdown;
+ private SkinSettingsDropdown skinDropdown;
public override string Header => "Skin";
public override FontAwesome Icon => FontAwesome.fa_paint_brush;
+ private readonly Bindable dropdownBindable = new Bindable();
+ private readonly Bindable configBindable = new Bindable();
+
private SkinManager skins;
[BackgroundDependencyLoader]
@@ -31,7 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections
FlowContent.Spacing = new Vector2(0, 5);
Children = new Drawable[]
{
- skinDropdown = new SettingsDropdown(),
+ skinDropdown = new SkinSettingsDropdown(),
new SettingsSlider
{
LabelText = "Menu cursor size",
@@ -54,19 +57,21 @@ namespace osu.Game.Overlays.Settings.Sections
skins.ItemAdded += itemAdded;
skins.ItemRemoved += itemRemoved;
- skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID));
+ config.BindWith(OsuSetting.Skin, configBindable);
- var skinBindable = config.GetBindable(OsuSetting.Skin);
+ skinDropdown.Bindable = dropdownBindable;
+ skinDropdown.Items = skins.GetAllUsableSkins().ToArray();
// Todo: This should not be necessary when OsuConfigManager is databased
- if (skinDropdown.Items.All(s => s.Value != skinBindable.Value))
- skinBindable.Value = 0;
+ if (skinDropdown.Items.All(s => s.ID != configBindable.Value))
+ configBindable.Value = 0;
- skinDropdown.Bindable = skinBindable;
+ configBindable.BindValueChanged(v => dropdownBindable.Value = skinDropdown.Items.Single(s => s.ID == v), true);
+ dropdownBindable.BindValueChanged(v => configBindable.Value = v.ID);
}
- private void itemRemoved(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Where(i => i.Value != s.ID);
- private void itemAdded(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Append(new KeyValuePair(s.ToString(), s.ID));
+ private void itemRemoved(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != s.ID).ToArray();
+ private void itemAdded(SkinInfo s) => skinDropdown.Items = skinDropdown.Items.Append(s).ToArray();
protected override void Dispose(bool isDisposing)
{
@@ -83,5 +88,15 @@ namespace osu.Game.Overlays.Settings.Sections
{
public override string TooltipText => Current.Value.ToString(@"0.##x");
}
+
+ private class SkinSettingsDropdown : SettingsDropdown
+ {
+ protected override OsuDropdown CreateDropdown() => new SkinDropdownControl { Items = Items };
+
+ private class SkinDropdownControl : DropdownControl
+ {
+ protected override string GenerateItemText(SkinInfo item) => item.ToString();
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs
index 33a8af7d91..1c3eb6c245 100644
--- a/osu.Game/Overlays/Settings/SettingsDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs
@@ -2,36 +2,41 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
public class SettingsDropdown : SettingsItem
{
- private Dropdown dropdown;
+ protected new OsuDropdown Control => (OsuDropdown)base.Control;
- private IEnumerable> items = new KeyValuePair[] { };
- public IEnumerable> Items
+ private IEnumerable items = Enumerable.Empty();
+
+ public IEnumerable Items
{
- get
- {
- return items;
- }
+ get => items;
set
{
items = value;
- if (dropdown != null)
- dropdown.Items = value;
+
+ if (Control != null)
+ Control.Items = value;
}
}
- protected override Drawable CreateControl() => dropdown = new OsuDropdown
+ protected sealed override Drawable CreateControl() => CreateDropdown();
+
+ protected virtual OsuDropdown CreateDropdown() => new DropdownControl { Items = Items };
+
+ protected class DropdownControl : OsuDropdown
{
- Margin = new MarginPadding { Top = 5 },
- RelativeSizeAxes = Axes.X,
- Items = Items,
- };
+ public DropdownControl()
+ {
+ Margin = new MarginPadding { Top = 5 };
+ RelativeSizeAxes = Axes.X;
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
index 64811137a6..2a98b80816 100644
--- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
+++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs
@@ -8,10 +8,15 @@ namespace osu.Game.Overlays.Settings
{
public class SettingsEnumDropdown : SettingsDropdown
{
- protected override Drawable CreateControl() => new OsuEnumDropdown
+ protected override OsuDropdown CreateDropdown() => new DropdownControl();
+
+ protected class DropdownControl : OsuEnumDropdown
{
- Margin = new MarginPadding { Top = 5 },
- RelativeSizeAxes = Axes.X,
- };
+ public DropdownControl()
+ {
+ Margin = new MarginPadding { Top = 5 };
+ RelativeSizeAxes = Axes.X;
+ }
+ }
}
}
diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs
index ce9218bbe7..fbfb5481c5 100644
--- a/osu.Game/Overlays/Settings/SettingsTextBox.cs
+++ b/osu.Game/Overlays/Settings/SettingsTextBox.cs
@@ -8,6 +8,10 @@ namespace osu.Game.Overlays.Settings
{
public class SettingsTextBox : SettingsItem
{
- protected override Drawable CreateControl() => new OsuTextBox();
+ protected override Drawable CreateControl() => new OsuTextBox
+ {
+ Margin = new MarginPadding { Top = 5 },
+ RelativeSizeAxes = Axes.X,
+ };
}
}
diff --git a/osu.Game/Rulesets/Edit/EditRulesetContainer.cs b/osu.Game/Rulesets/Edit/EditRulesetContainer.cs
new file mode 100644
index 0000000000..bc54c907ab
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/EditRulesetContainer.cs
@@ -0,0 +1,106 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+
+namespace osu.Game.Rulesets.Edit
+{
+ public abstract class EditRulesetContainer : CompositeDrawable
+ {
+ ///
+ /// The contained by this .
+ ///
+ public abstract Playfield Playfield { get; }
+
+ internal EditRulesetContainer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ ///
+ /// Adds a to the and displays a visual representation of it.
+ ///
+ /// The to add.
+ /// The visual representation of .
+ internal abstract DrawableHitObject Add(HitObject hitObject);
+
+ ///
+ /// Removes a from the and the display.
+ ///
+ /// The to remove.
+ /// The visual representation of the removed .
+ internal abstract DrawableHitObject Remove(HitObject hitObject);
+ }
+
+ public class EditRulesetContainer : EditRulesetContainer
+ where TObject : HitObject
+ {
+ public override Playfield Playfield => rulesetContainer.Playfield;
+
+ private Ruleset ruleset => rulesetContainer.Ruleset;
+ private Beatmap beatmap => rulesetContainer.Beatmap;
+
+ private readonly RulesetContainer rulesetContainer;
+
+ public EditRulesetContainer(RulesetContainer rulesetContainer)
+ {
+ this.rulesetContainer = rulesetContainer;
+
+ InternalChild = rulesetContainer;
+
+ Playfield.DisplayJudgements.Value = false;
+ }
+
+ internal override DrawableHitObject Add(HitObject hitObject)
+ {
+ var tObject = (TObject)hitObject;
+
+ // Add to beatmap, preserving sorting order
+ var insertionIndex = beatmap.HitObjects.FindLastIndex(h => h.StartTime <= hitObject.StartTime);
+ beatmap.HitObjects.Insert(insertionIndex + 1, tObject);
+
+ // Process object
+ var processor = ruleset.CreateBeatmapProcessor(beatmap);
+
+ processor.PreProcess();
+ tObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
+ processor.PostProcess();
+
+ // Add visual representation
+ var drawableObject = rulesetContainer.GetVisualRepresentation(tObject);
+
+ rulesetContainer.Playfield.Add(drawableObject);
+ rulesetContainer.Playfield.PostProcess();
+
+ return drawableObject;
+ }
+
+ internal override DrawableHitObject Remove(HitObject hitObject)
+ {
+ var tObject = (TObject)hitObject;
+
+ // Remove from beatmap
+ beatmap.HitObjects.Remove(tObject);
+
+ // Process the beatmap
+ var processor = ruleset.CreateBeatmapProcessor(beatmap);
+
+ processor.PreProcess();
+ processor.PostProcess();
+
+ // Remove visual representation
+ var drawableObject = Playfield.AllHitObjects.Single(d => d.HitObject == hitObject);
+
+ rulesetContainer.Playfield.Remove(drawableObject);
+ rulesetContainer.Playfield.PostProcess();
+
+ return drawableObject;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 8060ac742a..86ecbc0bb0 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -8,35 +8,41 @@ using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Input;
using osu.Framework.Logging;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
-using osu.Game.Screens.Edit.Screens.Compose.Layers;
-using osu.Game.Screens.Edit.Screens.Compose.RadioButtons;
+using osu.Game.Screens.Edit.Components.RadioButtons;
+using osu.Game.Screens.Edit.Compose.Components;
namespace osu.Game.Rulesets.Edit
{
public abstract class HitObjectComposer : CompositeDrawable
{
- private readonly Ruleset ruleset;
-
public IEnumerable HitObjects => rulesetContainer.Playfield.AllHitObjects;
- protected ICompositionTool CurrentTool { get; private set; }
+ protected readonly Ruleset Ruleset;
+
+ protected readonly IBindable Beatmap = new Bindable();
+
protected IRulesetConfigManager Config { get; private set; }
private readonly List layerContainers = new List();
- private readonly IBindable beatmap = new Bindable();
- private RulesetContainer rulesetContainer;
+ private EditRulesetContainer rulesetContainer;
- protected HitObjectComposer(Ruleset ruleset)
+ private BlueprintContainer blueprintContainer;
+
+ private InputManager inputManager;
+
+ internal HitObjectComposer(Ruleset ruleset)
{
- this.ruleset = ruleset;
+ Ruleset = ruleset;
RelativeSizeAxes = Axes.Both;
}
@@ -44,11 +50,11 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader]
private void load(IBindableBeatmap beatmap, IFrameBasedClock framedClock)
{
- this.beatmap.BindTo(beatmap);
+ Beatmap.BindTo(beatmap);
try
{
- rulesetContainer = CreateRulesetContainer(ruleset, beatmap.Value);
+ rulesetContainer = CreateRulesetContainer();
rulesetContainer.Clock = framedClock;
}
catch (Exception e)
@@ -57,14 +63,11 @@ namespace osu.Game.Rulesets.Edit
return;
}
- var layerBelowRuleset = new BorderLayer
- {
- RelativeSizeAxes = Axes.Both,
- Child = CreateLayerContainer()
- };
+ var layerBelowRuleset = CreateLayerContainer();
+ layerBelowRuleset.Child = new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both };
var layerAboveRuleset = CreateLayerContainer();
- layerAboveRuleset.Child = new HitObjectMaskLayer();
+ layerAboveRuleset.Child = blueprintContainer = new BlueprintContainer();
layerContainers.Add(layerBelowRuleset);
layerContainers.Add(layerAboveRuleset);
@@ -107,30 +110,30 @@ namespace osu.Game.Rulesets.Edit
};
toolboxCollection.Items =
- CompositionTools.Select(t => new RadioButton(t.Name, () => setCompositionTool(t)))
- .Prepend(new RadioButton("Select", () => setCompositionTool(null)))
+ CompositionTools.Select(t => new RadioButton(t.Name, () => blueprintContainer.CurrentTool = t))
+ .Prepend(new RadioButton("Select", () => blueprintContainer.CurrentTool = null))
.ToList();
toolboxCollection.Items[0].Select();
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+ }
+
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(this);
- Config = dependencies.Get().GetConfigFor(ruleset);
+ Config = dependencies.Get().GetConfigFor(Ruleset);
return dependencies;
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- rulesetContainer.Playfield.DisplayJudgements.Value = false;
- }
-
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
@@ -144,27 +147,52 @@ namespace osu.Game.Rulesets.Edit
});
}
- private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool;
-
- protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap);
-
- protected abstract IReadOnlyList CompositionTools { get; }
+ ///
+ /// Whether the user's cursor is currently in an area of the that is valid for placement.
+ ///
+ public virtual bool CursorInPlacementArea => rulesetContainer.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
///
- /// Creates a for a specific .
+ /// Adds a to the and visualises it.
+ ///
+ /// The to add.
+ public void Add(HitObject hitObject) => blueprintContainer.AddBlueprintFor(rulesetContainer.Add(hitObject));
+
+ public void Remove(HitObject hitObject) => blueprintContainer.RemoveBlueprintFor(rulesetContainer.Remove(hitObject));
+
+ internal abstract EditRulesetContainer CreateRulesetContainer();
+
+ protected abstract IReadOnlyList CompositionTools { get; }
+
+ ///
+ /// Creates a for a specific .
///
/// The to create the overlay for.
- public virtual HitObjectMask CreateMaskFor(DrawableHitObject hitObject) => null;
+ public virtual SelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null;
///
- /// Creates a which outlines s
+ /// Creates a which outlines s
/// and handles hitobject pattern adjustments.
///
- public virtual MaskSelection CreateMaskSelection() => new MaskSelection();
+ public virtual SelectionBox CreateSelectionBox() => new SelectionBox();
///
/// Creates a which provides a layer above or below the .
///
protected virtual Container CreateLayerContainer() => new Container { RelativeSizeAxes = Axes.Both };
}
+
+ public abstract class HitObjectComposer : HitObjectComposer
+ where TObject : HitObject
+ {
+ protected HitObjectComposer(Ruleset ruleset)
+ : base(ruleset)
+ {
+ }
+
+ internal override EditRulesetContainer CreateRulesetContainer()
+ => new EditRulesetContainer(CreateRulesetContainer(Ruleset, Beatmap.Value));
+
+ protected abstract RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap);
+ }
}
diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
new file mode 100644
index 0000000000..45dc7e4a05
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
@@ -0,0 +1,134 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using System;
+using osu.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input;
+using osu.Framework.Input.Events;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Compose;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Edit
+{
+ ///
+ /// A blueprint which governs the creation of a new to actualisation.
+ ///
+ public abstract class PlacementBlueprint : CompositeDrawable, IStateful, IRequireHighFrequencyMousePosition
+ {
+ ///
+ /// Invoked when has changed.
+ ///
+ public event Action StateChanged;
+
+ ///
+ /// Whether the is currently being placed, but has not necessarily finished being placed.
+ ///
+ public bool PlacementBegun { get; private set; }
+
+ ///
+ /// The that is being placed.
+ ///
+ protected readonly HitObject HitObject;
+
+ protected IClock EditorClock { get; private set; }
+
+ private readonly IBindable beatmap = new Bindable();
+
+ [Resolved]
+ private IPlacementHandler placementHandler { get; set; }
+
+ protected PlacementBlueprint(HitObject hitObject)
+ {
+ HitObject = hitObject;
+
+ RelativeSizeAxes = Axes.Both;
+
+ Alpha = 0;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(IBindableBeatmap beatmap, IAdjustableClock clock)
+ {
+ this.beatmap.BindTo(beatmap);
+
+ EditorClock = clock;
+
+ ApplyDefaultsToHitObject();
+ }
+
+ private PlacementState state;
+
+ public PlacementState State
+ {
+ get => state;
+ set
+ {
+ if (state == value)
+ return;
+ state = value;
+
+ if (state == PlacementState.Shown)
+ Show();
+ else
+ Hide();
+
+ StateChanged?.Invoke(value);
+ }
+ }
+
+ ///
+ /// Signals that the placement of has started.
+ ///
+ protected void BeginPlacement()
+ {
+ placementHandler.BeginPlacement(HitObject);
+ PlacementBegun = true;
+ }
+
+ ///
+ /// Signals that the placement of has finished.
+ /// This will destroy this , and add the to the .
+ ///
+ protected void EndPlacement()
+ {
+ if (!PlacementBegun)
+ BeginPlacement();
+ placementHandler.EndPlacement(HitObject);
+ }
+
+ ///
+ /// Invokes , refreshing and parameters for the .
+ ///
+ protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.Value.Beatmap.ControlPointInfo, beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty);
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false;
+
+ protected override bool Handle(UIEvent e)
+ {
+ base.Handle(e);
+
+ switch (e)
+ {
+ case ScrollEvent _:
+ return false;
+ case MouseEvent _:
+ return true;
+ default:
+ return false;
+ }
+ }
+ }
+
+ public enum PlacementState
+ {
+ Hidden,
+ Shown,
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/HitObjectMask.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
similarity index 66%
rename from osu.Game/Rulesets/Edit/HitObjectMask.cs
rename to osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index 636ea418f3..db35d47b2b 100644
--- a/osu.Game/Rulesets/Edit/HitObjectMask.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input.Events;
@@ -14,33 +15,33 @@ using OpenTK;
namespace osu.Game.Rulesets.Edit
{
///
- /// A mask placed above a adding editing functionality.
+ /// A blueprint placed above a adding editing functionality.
///
- public class HitObjectMask : CompositeDrawable, IStateful
+ public abstract class SelectionBlueprint : CompositeDrawable, IStateful
{
///
- /// Invoked when this has been selected.
+ /// Invoked when this has been selected.
///
- public event Action Selected;
+ public event Action Selected;
///
- /// Invoked when this has been deselected.
+ /// Invoked when this has been deselected.
///
- public event Action Deselected;
+ public event Action Deselected;
///
- /// Invoked when this has requested selection.
+ /// Invoked when this has requested selection.
/// Will fire even if already selected. Does not actually perform selection.
///
- public event Action SelectionRequested;
+ public event Action SelectionRequested;
///
- /// Invoked when this has requested drag.
+ /// Invoked when this has requested drag.
///
- public event Action DragRequested;
+ public event Action DragRequested;
///
- /// The which this applies to.
+ /// The which this applies to.
///
public readonly DrawableHitObject HitObject;
@@ -48,10 +49,12 @@ namespace osu.Game.Rulesets.Edit
public override bool HandlePositionalInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false;
- public HitObjectMask(DrawableHitObject hitObject)
+ protected SelectionBlueprint(DrawableHitObject hitObject)
{
HitObject = hitObject;
+ RelativeSizeAxes = Axes.Both;
+
AlwaysPresent = true;
Alpha = 0;
}
@@ -83,17 +86,19 @@ namespace osu.Game.Rulesets.Edit
}
///
- /// Selects this , causing it to become visible.
+ /// Selects this , causing it to become visible.
///
public void Select() => State = SelectionState.Selected;
///
- /// Deselects this , causing it to become invisible.
+ /// Deselects this , causing it to become invisible.
///
public void Deselect() => State = SelectionState.NotSelected;
public bool IsSelected => State == SelectionState.Selected;
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObject.ReceivePositionalInputAt(screenSpacePos);
+
private bool selectionRequested;
protected override bool OnMouseDown(MouseDownEvent e)
@@ -125,18 +130,20 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnDrag(DragEvent e)
{
- DragRequested?.Invoke(this, e.Delta, e.CurrentState);
+ DragRequested?.Invoke(e);
return true;
}
- ///
- /// The screen-space point that causes this to be selected.
- ///
- public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre;
+ public abstract void AdjustPosition(DragEvent dragEvent);
///
- /// The screen-space quad that outlines this for selections.
+ /// The screen-space point that causes this to be selected.
///
- public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
+ public virtual Vector2 SelectionPoint => HitObject.ScreenSpaceDrawQuad.Centre;
+
+ ///
+ /// The screen-space quad that outlines this for selections.
+ ///
+ public virtual Quad SelectionQuad => HitObject.ScreenSpaceDrawQuad;
}
}
diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs
index 78ad236e74..1cb3c4c451 100644
--- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs
+++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs
@@ -1,23 +1,17 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-using osu.Game.Rulesets.Objects;
-
namespace osu.Game.Rulesets.Edit.Tools
{
- public class HitObjectCompositionTool : ICompositionTool
- where T : HitObject
+ public abstract class HitObjectCompositionTool
{
- public string Name { get; }
+ public readonly string Name;
- public HitObjectCompositionTool()
- : this(typeof(T).Name)
- {
- }
-
- public HitObjectCompositionTool(string name)
+ protected HitObjectCompositionTool(string name)
{
Name = name;
}
+
+ public abstract PlacementBlueprint CreatePlacementBlueprint();
}
}
diff --git a/osu.Game/Rulesets/Edit/Tools/ICompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/ICompositionTool.cs
deleted file mode 100644
index ce8b139b43..0000000000
--- a/osu.Game/Rulesets/Edit/Tools/ICompositionTool.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-
-namespace osu.Game.Rulesets.Edit.Tools
-{
- public interface ICompositionTool
- {
- string Name { get; }
- }
-}
diff --git a/osu.Game/Rulesets/Edit/Types/IHasEditablePosition.cs b/osu.Game/Rulesets/Edit/Types/IHasEditablePosition.cs
deleted file mode 100644
index 7107b6c763..0000000000
--- a/osu.Game/Rulesets/Edit/Types/IHasEditablePosition.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-
-using osu.Game.Rulesets.Objects.Types;
-using OpenTK;
-
-namespace osu.Game.Rulesets.Edit.Types
-{
- public interface IHasEditablePosition : IHasPosition
- {
- void OffsetPosition(Vector2 offset);
- }
-}
diff --git a/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs b/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs
new file mode 100644
index 0000000000..1eb74ca76a
--- /dev/null
+++ b/osu.Game/Rulesets/Mods/IApplicableToBeatmap.cs
@@ -0,0 +1,22 @@
+// Copyright (c) 2007-2018 ppy Pty Ltd .
+// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+
+namespace osu.Game.Rulesets.Mods
+{
+ ///
+ /// Interface for a that applies changes to a
+ /// after conversion and post-processing has completed.
+ ///
+ public interface IApplicableToBeatmap : IApplicableMod
+ where TObject : HitObject
+ {
+ ///
+ /// Applies this to a .
+ ///
+ /// The to apply to.
+ void ApplyToBeatmap(Beatmap beatmap);
+ }
+}
diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs
index 223263195c..5e5353bfdd 100644
--- a/osu.Game/Rulesets/Mods/ModFlashlight.cs
+++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs
@@ -1,11 +1,27 @@
// Copyright (c) 2007-2018 ppy Pty Ltd .
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
+using System;
+using System.Collections.Generic;
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.OpenGL.Vertices;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using OpenTK;
+using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mods
{
- public abstract class ModFlashlight : Mod
+ public abstract class ModFlashlight : Mod, IApplicableToRulesetContainer, IApplicableToScoreProcessor
+ where T : HitObject
{
public override string Name => "Flashlight";
public override string ShortenedName => "FL";
@@ -13,5 +29,125 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => "Restricted view area.";
public override bool Ranked => true;
+
+ public const double FLASHLIGHT_FADE_DURATION = 800;
+ protected readonly BindableInt Combo = new BindableInt();
+
+ public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
+ {
+ Combo.BindTo(scoreProcessor.Combo);
+ }
+
+ public virtual void ApplyToRulesetContainer(RulesetContainer rulesetContainer)
+ {
+ var flashlight = CreateFlashlight();
+ flashlight.Combo = Combo;
+ flashlight.RelativeSizeAxes = Axes.Both;
+ flashlight.Colour = Color4.Black;
+ rulesetContainer.KeyBindingInputManager.Add(flashlight);
+
+ flashlight.Breaks = rulesetContainer.Beatmap.Breaks;
+ }
+
+ public abstract Flashlight CreateFlashlight();
+
+ public abstract class Flashlight : Drawable
+ {
+ internal BindableInt Combo;
+ private Shader shader;
+
+ protected override DrawNode CreateDrawNode() => new FlashlightDrawNode();
+
+ public override bool RemoveCompletedTransforms => false;
+
+ public List Breaks;
+
+ protected override void ApplyDrawNode(DrawNode node)
+ {
+ base.ApplyDrawNode(node);
+
+ var flashNode = (FlashlightDrawNode)node;
+
+ flashNode.Shader = shader;
+ flashNode.ScreenSpaceDrawQuad = ScreenSpaceDrawQuad;
+ flashNode.FlashlightPosition = Vector2Extensions.Transform(FlashlightPosition, DrawInfo.Matrix);
+ flashNode.FlashlightSize = Vector2Extensions.Transform(FlashlightSize, DrawInfo.Matrix);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(ShaderManager shaderManager)
+ {
+ shader = shaderManager.Load("PositionAndColour", FragmentShader);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Combo.ValueChanged += OnComboChange;
+
+ this.FadeInFromZero(FLASHLIGHT_FADE_DURATION);
+
+ foreach (var breakPeriod in Breaks)
+ {
+ if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue;
+
+ this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION);
+ this.Delay(breakPeriod.EndTime - FLASHLIGHT_FADE_DURATION).FadeInFromZero(FLASHLIGHT_FADE_DURATION);
+ }
+ }
+
+ protected abstract void OnComboChange(int newCombo);
+
+ protected abstract string FragmentShader { get; }
+
+ private Vector2 flashlightPosition;
+ protected Vector2 FlashlightPosition
+ {
+ get => flashlightPosition;
+ set
+ {
+ if (flashlightPosition == value) return;
+
+ flashlightPosition = value;
+ Invalidate(Invalidation.DrawNode);
+ }
+ }
+
+ private Vector2 flashlightSize;
+ protected Vector2 FlashlightSize
+ {
+ get => flashlightSize;
+ set
+ {
+ if (flashlightSize == value) return;
+
+ flashlightSize = value;
+ Invalidate(Invalidation.DrawNode);
+ }
+ }
+ }
+
+ private class FlashlightDrawNode : DrawNode
+ {
+ public Shader Shader;
+ public Quad ScreenSpaceDrawQuad;
+ public Vector2 FlashlightPosition;
+ public Vector2 FlashlightSize;
+
+ public override void Draw(Action vertexAction)
+ {
+ base.Draw(vertexAction);
+
+ Shader.Bind();
+
+ Shader.GetUniform("flashlightPos").UpdateValue(ref FlashlightPosition);
+ Shader.GetUniform("flashlightSize").UpdateValue(ref FlashlightSize);
+
+ Texture.WhitePixel.DrawQuad(ScreenSpaceDrawQuad, DrawColourInfo.Colour, vertexAction: vertexAction);
+
+ Shader.Unbind();
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Objects/BezierApproximator.cs b/osu.Game/Rulesets/Objects/BezierApproximator.cs
deleted file mode 100644
index a1803e32f7..0000000000
--- a/osu.Game/Rulesets/Objects/BezierApproximator.cs
+++ /dev/null
@@ -1,151 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-
-using System;
-using System.Collections.Generic;
-using OpenTK;
-
-namespace osu.Game.Rulesets.Objects
-{
- public readonly ref struct BezierApproximator
- {
- private readonly int count;
- private readonly ReadOnlySpan controlPoints;
- private readonly Vector2[] subdivisionBuffer1;
- private readonly Vector2[] subdivisionBuffer2;
-
- private const float tolerance = 0.25f;
- private const float tolerance_sq = tolerance * tolerance;
-
- public BezierApproximator(ReadOnlySpan controlPoints)
- {
- this.controlPoints = controlPoints;
- count = controlPoints.Length;
-
- subdivisionBuffer1 = new Vector2[count];
- subdivisionBuffer2 = new Vector2[count * 2 - 1];
- }
-
- ///
- /// Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds.
- /// NOTE: The 2nd order derivative of a 2d curve represents its curvature, so intuitively this function
- /// checks (as the name suggests) whether our approximation is _locally_ "flat". More curvy parts
- /// need to have a denser approximation to be more "flat".
- ///
- /// The control points to check for flatness.
- /// Whether the control points are flat enough.
- private static bool isFlatEnough(Vector2[] controlPoints)
- {
- for (int i = 1; i < controlPoints.Length - 1; i++)
- if ((controlPoints[i - 1] - 2 * controlPoints[i] + controlPoints[i + 1]).LengthSquared > tolerance_sq * 4)
- return false;
-
- return true;
- }
-
- ///
- /// Subdivides n control points representing a bezier curve into 2 sets of n control points, each
- /// describing a bezier curve equivalent to a half of the original curve. Effectively this splits
- /// the original curve into 2 curves which result in the original curve when pieced back together.
- ///
- /// The control points to split.
- /// Output: The control points corresponding to the left half of the curve.
- /// Output: The control points corresponding to the right half of the curve.
- private void subdivide(Vector2[] controlPoints, Vector2[] l, Vector2[] r)
- {
- Vector2[] midpoints = subdivisionBuffer1;
-
- for (int i = 0; i < count; ++i)
- midpoints[i] = controlPoints[i];
-
- for (int i = 0; i < count; i++)
- {
- l[i] = midpoints[0];
- r[count - i - 1] = midpoints[count - i - 1];
-
- for (int j = 0; j < count - i - 1; j++)
- midpoints[j] = (midpoints[j] + midpoints[j + 1]) / 2;
- }
- }
-
- ///
- /// This uses De Casteljau's algorithm to obtain an optimal
- /// piecewise-linear approximation of the bezier curve with the same amount of points as there are control points.
- ///
- /// The control points describing the bezier curve to be approximated.
- /// The points representing the resulting piecewise-linear approximation.
- private void approximate(Vector2[] controlPoints, List output)
- {
- Vector2[] l = subdivisionBuffer2;
- Vector2[] r = subdivisionBuffer1;
-
- subdivide(controlPoints, l, r);
-
- for (int i = 0; i < count - 1; ++i)
- l[count + i] = r[i + 1];
-
- output.Add(controlPoints[0]);
- for (int i = 1; i < count - 1; ++i)
- {
- int index = 2 * i;
- Vector2 p = 0.25f * (l[index - 1] + 2 * l[index] + l[index + 1]);
- output.Add(p);
- }
- }
-
- ///
- /// Creates a piecewise-linear approximation of a bezier curve, by adaptively repeatedly subdividing
- /// the control points until their approximation error vanishes below a given threshold.
- ///
- /// A list of vectors representing the piecewise-linear approximation.
- public List CreateBezier()
- {
- List output = new List();
-
- if (count == 0)
- return output;
-
- Stack toFlatten = new Stack();
- Stack freeBuffers = new Stack();
-
- // "toFlatten" contains all the curves which are not yet approximated well enough.
- // We use a stack to emulate recursion without the risk of running into a stack overflow.
- // (More specifically, we iteratively and adaptively refine our curve with a
- // Depth-first search
- // over the tree resulting from the subdivisions we make.)
- toFlatten.Push(controlPoints.ToArray());
-
- Vector2[] leftChild = subdivisionBuffer2;
-
- while (toFlatten.Count > 0)
- {
- Vector2[] parent = toFlatten.Pop();
- if (isFlatEnough(parent))
- {
- // If the control points we currently operate on are sufficiently "flat", we use
- // an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation
- // of the bezier curve represented by our control points, consisting of the same amount
- // of points as there are control points.
- approximate(parent, output);
- freeBuffers.Push(parent);
- continue;
- }
-
- // If we do not yet have a sufficiently "flat" (in other words, detailed) approximation we keep
- // subdividing the curve we are currently operating on.
- Vector2[] rightChild = freeBuffers.Count > 0 ? freeBuffers.Pop() : new Vector2[count];
- subdivide(parent, leftChild, rightChild);
-
- // We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration.
- for (int i = 0; i < count; ++i)
- parent[i] = leftChild[i];
-
- toFlatten.Push(rightChild);
- toFlatten.Push(parent);
- }
-
- output.Add(controlPoints[count - 1]);
- return output;
- }
- }
-}
diff --git a/osu.Game/Rulesets/Objects/CatmullApproximator.cs b/osu.Game/Rulesets/Objects/CatmullApproximator.cs
deleted file mode 100644
index 78f8e471f3..0000000000
--- a/osu.Game/Rulesets/Objects/CatmullApproximator.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (c) 2007-2018 ppy Pty Ltd .
-// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-
-using System;
-using System.Collections.Generic;
-using OpenTK;
-
-namespace osu.Game.Rulesets.Objects
-{
- public readonly ref struct CatmullApproximator
- {
- ///
- /// The amount of pieces to calculate for each controlpoint quadruplet.
- ///
- private const int detail = 50;
-
- private readonly ReadOnlySpan controlPoints;
-
- public CatmullApproximator(ReadOnlySpan controlPoints)
- {
- this.controlPoints = controlPoints;
- }
-
- ///