1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-16 10:22:53 +08:00

Compare commits

...

932 Commits

310 changed files with 11418 additions and 2894 deletions
+1 -1
View File
@@ -20,4 +20,4 @@ build:
verbosity: minimal
after_build:
- cmd: inspectcode /o="inspectcodereport.xml" /caches-home="inspectcode" osu.sln
- cmd: NVika parsereport "inspectcodereport.xml"
- cmd: NVika parsereport "inspectcodereport.xml" --treatwarningsaserrors
+4
View File
@@ -31,6 +31,10 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="SharpCompress" publicKeyToken="afb0a02973931d96" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.17.1.0" newVersion="0.17.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
+7 -7
View File
@@ -49,10 +49,6 @@
<HintPath>$(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.PatchApi.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="ICSharpCode.SharpZipLib, Version=0.86.0.518, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\ICSharpCode.SharpZipLib.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Cecil, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll</HintPath>
<Private>True</Private>
@@ -73,15 +69,19 @@
<HintPath>$(SolutionDir)\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="NuGet.Squirrel, Version=3.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\NuGet.Squirrel.dll</HintPath>
<HintPath>$(SolutionDir)\packages\squirrel.windows.1.7.5\lib\Net45\NuGet.Squirrel.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="SharpCompress, Version=0.17.1.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\SharpCompress.0.17.1\lib\net45\SharpCompress.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Splat, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Splat.2.0.0\lib\Net45\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Squirrel, Version=1.5.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\Squirrel.dll</HintPath>
<Reference Include="Squirrel, Version=1.7.5.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\squirrel.windows.1.7.5\lib\Net45\Squirrel.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
+3 -2
View File
@@ -7,7 +7,8 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste
<package id="DeltaCompressionDotNet" version="1.1.0" targetFramework="net452" />
<package id="Mono.Cecil" version="0.9.6.4" targetFramework="net452" />
<package id="Newtonsoft.Json" version="10.0.2" targetFramework="net452" />
<package id="NuGet.CommandLine" version="3.5.0" targetFramework="net452" developmentDependency="true" />
<package id="NuGet.CommandLine" version="4.1.0" targetFramework="net452" developmentDependency="true" />
<package id="SharpCompress" version="0.17.1" targetFramework="net452" />
<package id="Splat" version="2.0.0" targetFramework="net452" />
<package id="squirrel.windows" version="1.5.2" targetFramework="net452" />
<package id="squirrel.windows" version="1.7.5" targetFramework="net452" />
</packages>
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
+4 -2
View File
@@ -37,8 +37,9 @@
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="nunit.framework, Version=3.6.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\NUnit.3.6.1\lib\net45\nunit.framework.dll</HintPath>
<Reference Include="nunit.framework, Version=3.7.1.0, Culture=neutral, PublicKeyToken=2638cd05610744eb, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\NUnit.3.7.1\lib\net45\nunit.framework.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="SQLite.Net, Version=3.1.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
@@ -100,6 +101,7 @@
<None Include="..\osu.licenseheader">
<Link>osu.licenseheader</Link>
</None>
<None Include="app.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
+1 -1
View File
@@ -5,7 +5,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste
-->
<packages>
<package id="Newtonsoft.Json" version="10.0.2" targetFramework="net45" />
<package id="NUnit" version="3.6.1" targetFramework="net45" />
<package id="NUnit" version="3.7.1" targetFramework="net45" />
<package id="SQLite.Net.Core-PCL" version="3.1.1" targetFramework="net45" />
<package id="SQLite.Net-PCL" version="3.1.1" targetFramework="net45" />
<package id="SQLiteNetExtensions" version="1.3.0" targetFramework="net45" />
@@ -41,7 +41,7 @@ namespace osu.Desktop.VisualTests.Tests
StarDifficulty = 5.3f,
Metrics = new BeatmapMetrics
{
Ratings = Enumerable.Range(0,10),
Ratings = Enumerable.Range(0, 10),
Fails = Enumerable.Range(lastRange, 100).Select(i => i % 12 - 6),
Retries = Enumerable.Range(lastRange - 3, 100).Select(i => i % 12 - 6),
},
@@ -0,0 +1,39 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics;
namespace osu.Desktop.VisualTests.Tests
{
internal class TestCaseBreadcrumbs : TestCase
{
public override string Description => @"breadcrumb > control";
public override void Reset()
{
base.Reset();
BreadcrumbControl<BreadcrumbTab> c;
Add(c = new BreadcrumbControl<BreadcrumbTab>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
});
AddStep(@"first", () => c.Current.Value = BreadcrumbTab.Click);
AddStep(@"second", () => c.Current.Value = BreadcrumbTab.The);
AddStep(@"third", () => c.Current.Value = BreadcrumbTab.Circles);
}
private enum BreadcrumbTab
{
Click,
The,
Circles,
}
}
}
@@ -0,0 +1,125 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
namespace osu.Desktop.VisualTests.Tests
{
internal class TestCaseContextMenu : TestCase
{
public override string Description => @"Menu visible on right click";
private const int start_time = 0;
private const int duration = 1000;
private MyContextMenuContainer container;
public override void Reset()
{
base.Reset();
Add(container = new MyContextMenuContainer
{
Size = new Vector2(200),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Green,
}
}
});
Add(new AnotherContextMenuContainer
{
Size = new Vector2(200),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Red,
}
}
});
container.Transforms.Add(new TransformPosition
{
StartValue = Vector2.Zero,
EndValue = new Vector2(0, 100),
StartTime = start_time,
EndTime = start_time + duration,
LoopCount = -1,
LoopDelay = duration * 3
});
container.Transforms.Add(new TransformPosition
{
StartValue = new Vector2(0, 100),
EndValue = new Vector2(100, 100),
StartTime = start_time + duration,
EndTime = start_time + duration * 2,
LoopCount = -1,
LoopDelay = duration * 3
});
container.Transforms.Add(new TransformPosition
{
StartValue = new Vector2(100, 100),
EndValue = new Vector2(100, 0),
StartTime = start_time + duration * 2,
EndTime = start_time + duration * 3,
LoopCount = -1,
LoopDelay = duration * 3
});
container.Transforms.Add(new TransformPosition
{
StartValue = new Vector2(100, 0),
EndValue = Vector2.Zero,
StartTime = start_time + duration * 3,
EndTime = start_time + duration * 4,
LoopCount = -1,
LoopDelay = duration * 3
});
}
private class MyContextMenuContainer : Container, IHasContextMenu
{
public ContextMenuItem[] ContextMenuItems => new ContextMenuItem[]
{
new OsuContextMenuItem(@"Some option"),
new OsuContextMenuItem(@"Highlighted option", MenuItemType.Highlighted),
new OsuContextMenuItem(@"Another option"),
new OsuContextMenuItem(@"Choose me please"),
new OsuContextMenuItem(@"And me too"),
new OsuContextMenuItem(@"Trying to fill"),
new OsuContextMenuItem(@"Destructive option", MenuItemType.Destructive),
};
}
private class AnotherContextMenuContainer : Container, IHasContextMenu
{
public ContextMenuItem[] ContextMenuItems => new ContextMenuItem[]
{
new OsuContextMenuItem(@"Simple option"),
new OsuContextMenuItem(@"Simple very very long option"),
new OsuContextMenuItem(@"Change width", MenuItemType.Highlighted) { Action = () => ResizeWidthTo(Width * 2, 100, EasingTypes.OutQuint) },
new OsuContextMenuItem(@"Change height", MenuItemType.Highlighted) { Action = () => ResizeHeightTo(Height * 2, 100, EasingTypes.OutQuint) },
new OsuContextMenuItem(@"Change width back", MenuItemType.Destructive) { Action = () => ResizeWidthTo(Width / 2, 100, EasingTypes.OutQuint) },
new OsuContextMenuItem(@"Change height back", MenuItemType.Destructive) { Action = () => ResizeHeightTo(Height / 2, 100, EasingTypes.OutQuint) },
};
}
}
}
@@ -0,0 +1,199 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Overlays;
namespace osu.Desktop.VisualTests.Tests
{
public class TestCaseDirect : TestCase
{
public override string Description => @"osu!direct overlay";
private DirectOverlay direct;
private RulesetDatabase rulesets;
public override void Reset()
{
base.Reset();
Add(direct = new DirectOverlay());
newBeatmaps();
AddStep(@"toggle", direct.ToggleVisibility);
AddStep(@"result counts", () => direct.ResultAmounts = new DirectOverlay.ResultCounts(1, 4, 13));
}
[BackgroundDependencyLoader]
private void load(RulesetDatabase rulesets)
{
this.rulesets = rulesets;
}
private void newBeatmaps()
{
var ruleset = rulesets.GetRuleset(0);
direct.BeatmapSets = new[]
{
new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = @"OrVid",
Artist = @"An",
Author = @"RLC",
Source = @"",
},
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 5.35f,
Metadata = new BeatmapMetadata(),
OnlineInfo = new BeatmapOnlineInfo
{
Covers = new[] { @"https://assets.ppy.sh//beatmaps/578332/covers/cover.jpg?1494591390" },
Preview = @"https://b.ppy.sh/preview/578332.mp3",
PlayCount = 97,
FavouriteCount = 72,
},
},
},
},
new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = @"tiny lamp",
Artist = @"fhana",
Author = @"Sotarks",
Source = @"ぎんぎつね",
},
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 5.81f,
Metadata = new BeatmapMetadata(),
OnlineInfo = new BeatmapOnlineInfo
{
Covers = new[] { @"https://assets.ppy.sh//beatmaps/599627/covers/cover.jpg?1494539318" },
Preview = @"https//b.ppy.sh/preview/599627.mp3",
PlayCount = 3082,
FavouriteCount = 14,
},
},
},
},
new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = @"At Gwanghwamun",
Artist = @"KYUHYUN",
Author = @"Cerulean Veyron",
Source = @"",
},
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 0.9f,
Metadata = new BeatmapMetadata(),
OnlineInfo = new BeatmapOnlineInfo
{
Covers = new[] { @"https://assets.ppy.sh//beatmaps/513268/covers/cover.jpg?1494502863" },
Preview = @"https//b.ppy.sh/preview/513268.mp3",
PlayCount = 2762,
FavouriteCount = 15,
},
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 1.1f,
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 2.02f,
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 3.49f,
},
},
},
new BeatmapSetInfo
{
Metadata = new BeatmapMetadata
{
Title = @"RHAPSODY OF BLUE SKY",
Artist = @"fhana",
Author = @"[Kamiya]",
Source = @"小林さんちのメイドラゴン",
},
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 1.26f,
Metadata = new BeatmapMetadata(),
OnlineInfo = new BeatmapOnlineInfo
{
Covers = new[] { @"https://assets.ppy.sh//beatmaps/586841/covers/cover.jpg?1494052741" },
Preview = @"https//b.ppy.sh/preview/586841.mp3",
PlayCount = 62317,
FavouriteCount = 161,
},
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 2.01f,
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 2.87f,
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 3.76f,
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 3.93f,
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 4.37f,
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 5.13f,
},
new BeatmapInfo
{
Ruleset = ruleset,
StarDifficulty = 5.42f,
},
},
},
};
}
}
}
@@ -0,0 +1,74 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Screens.Multiplayer;
using osu.Game.Online.Multiplayer;
using osu.Game.Users;
using osu.Game.Database;
namespace osu.Desktop.VisualTests.Tests
{
internal class TestCaseDrawableRoom : TestCase
{
public override string Description => @"Select your favourite room";
public override void Reset()
{
base.Reset();
DrawableRoom first;
DrawableRoom second;
Add(new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
Width = 500f,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
first = new DrawableRoom(new Room()),
second = new DrawableRoom(new Room()),
}
});
first.Room.Name.Value = @"Great Room Right Here";
first.Room.Host.Value = new User { Username = @"Naeferith", Id = 9492835, Country = new Country { FlagName = @"FR" } };
first.Room.Status.Value = new RoomStatusOpen();
first.Room.Beatmap.Value = new BeatmapMetadata { Title = @"Seiryu", Artist = @"Critical Crystal" };
second.Room.Name.Value = @"Relax It's The Weekend";
second.Room.Host.Value = new User { Username = @"peppy", Id = 2, Country = new Country { FlagName = @"AU" } };
second.Room.Status.Value = new RoomStatusPlaying();
second.Room.Beatmap.Value = new BeatmapMetadata { Title = @"ZAQ", Artist = @"Serendipity" };
AddStep(@"change state", () =>
{
first.Room.Status.Value = new RoomStatusPlaying();
});
AddStep(@"change name", () =>
{
first.Room.Name.Value = @"I Changed Name";
});
AddStep(@"change host", () =>
{
first.Room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } };
});
AddStep(@"change beatmap", () =>
{
first.Room.Beatmap.Value = null;
});
AddStep(@"change state", () =>
{
first.Room.Status.Value = new RoomStatusOpen();
});
}
}
}
@@ -18,7 +18,7 @@ using osu.Game.Rulesets.Taiko.UI;
using System.Collections.Generic;
using osu.Desktop.VisualTests.Beatmaps;
using osu.Framework.Allocation;
using osu.Game.Beatmaps.Timing;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Desktop.VisualTests.Tests
{
@@ -53,8 +53,8 @@ namespace osu.Desktop.VisualTests.Tests
time += RNG.Next(50, 500);
}
TimingInfo timing = new TimingInfo();
timing.ControlPoints.Add(new ControlPoint
var controlPointInfo = new ControlPointInfo();
controlPointInfo.TimingPoints.Add(new TimingControlPoint
{
BeatLength = 200
});
@@ -73,7 +73,7 @@ namespace osu.Desktop.VisualTests.Tests
Author = @"peppy",
},
},
TimingInfo = timing
ControlPointInfo = controlPointInfo
});
Add(new Drawable[]
@@ -30,7 +30,7 @@ namespace osu.Desktop.VisualTests.Tests
},
};
AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1,10).Select(i => (float)i));
AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Select(i => (float)i));
AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).Select(i => (float)i));
AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().Select(i => (float)i));
AddStep("Bottom to top", () => graph.Direction = BarDirection.BottomToTop);
@@ -54,7 +54,7 @@ namespace osu.Desktop.VisualTests.Tests
Children = new Drawable[]
{
new SpriteText { Text = "FadeTime" },
sliderBar =new TestSliderBar<int>
sliderBar = new TestSliderBar<int>
{
Width = 150,
Height = 10,
@@ -40,7 +40,7 @@ namespace osu.Desktop.VisualTests.Tests
{
Name = "Timing section",
RelativeSizeAxes = Axes.Both,
RelativeCoordinateSpace = new Vector2(1, 10000),
RelativeChildSize = new Vector2(1, 10000),
Children = new[]
{
new DrawableNote(new Note { StartTime = 5000 }) { AccentColour = Color4.Red },
@@ -62,7 +62,7 @@ namespace osu.Desktop.VisualTests.Tests
{
Name = "Timing section",
RelativeSizeAxes = Axes.Both,
RelativeCoordinateSpace = new Vector2(1, 10000),
RelativeChildSize = new Vector2(1, 10000),
Children = new[]
{
new DrawableHoldNote(new HoldNote
@@ -6,11 +6,16 @@ using osu.Framework.Testing;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.UI;
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps.Timing;
using OpenTK;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects;
using osu.Framework.Configuration;
using OpenTK.Input;
using osu.Framework.Timing;
using osu.Framework.Extensions.IEnumerableExtensions;
using System.Linq;
using osu.Game.Rulesets.Mania.Timing;
using osu.Game.Rulesets.Timing;
namespace osu.Desktop.VisualTests.Tests
{
@@ -27,7 +32,7 @@ namespace osu.Desktop.VisualTests.Tests
Action<int, SpecialColumnPosition> createPlayfield = (cols, pos) =>
{
Clear();
Add(new ManiaPlayfield(cols, new List<ControlPoint>())
Add(new ManiaPlayfield(cols)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -36,27 +41,72 @@ namespace osu.Desktop.VisualTests.Tests
});
};
Action<int, SpecialColumnPosition> createPlayfieldWithNotes = (cols, pos) =>
const double start_time = 500;
const double duration = 500;
Func<double, bool, SpeedAdjustmentContainer> createTimingChange = (time, gravity) => new ManiaSpeedAdjustmentContainer(new MultiplierControlPoint(time)
{
TimingPoint = { BeatLength = 1000 }
}, gravity ? ScrollingAlgorithm.Gravity : ScrollingAlgorithm.Basic);
Action<bool> createPlayfieldWithNotes = gravity =>
{
Clear();
var rateAdjustClock = new StopwatchClock(true) { Rate = 1 };
ManiaPlayfield playField;
Add(playField = new ManiaPlayfield(cols, new List<ControlPoint> { new ControlPoint { BeatLength = 200 } })
Add(playField = new ManiaPlayfield(4)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
SpecialColumnPosition = pos,
Scale = new Vector2(1, -1)
Scale = new Vector2(1, -1),
Clock = new FramedClock(rateAdjustClock)
});
for (int i = 0; i < cols; i++)
if (!gravity)
playField.Columns.ForEach(c => c.Add(createTimingChange(0, false)));
for (double t = start_time; t <= start_time + duration; t += 100)
{
if (gravity)
playField.Columns.ElementAt(0).Add(createTimingChange(t, true));
playField.Add(new DrawableNote(new Note
{
StartTime = Time.Current + 1000,
Column = i
}));
StartTime = t,
Column = 0
}, new Bindable<Key>(Key.D)));
if (gravity)
playField.Columns.ElementAt(3).Add(createTimingChange(t, true));
playField.Add(new DrawableNote(new Note
{
StartTime = t,
Column = 3
}, new Bindable<Key>(Key.K)));
}
if (gravity)
playField.Columns.ElementAt(1).Add(createTimingChange(start_time, true));
playField.Add(new DrawableHoldNote(new HoldNote
{
StartTime = start_time,
Duration = duration,
Column = 1
}, new Bindable<Key>(Key.F)));
if (gravity)
playField.Columns.ElementAt(2).Add(createTimingChange(start_time, true));
playField.Add(new DrawableHoldNote(new HoldNote
{
StartTime = start_time,
Duration = duration,
Column = 2
}, new Bindable<Key>(Key.J)));
};
AddStep("1 column", () => createPlayfield(1, SpecialColumnPosition.Normal));
@@ -68,19 +118,16 @@ namespace osu.Desktop.VisualTests.Tests
AddStep("Left special style", () => createPlayfield(8, SpecialColumnPosition.Left));
AddStep("Right special style", () => createPlayfield(8, SpecialColumnPosition.Right));
AddStep("Normal special style", () => createPlayfield(4, SpecialColumnPosition.Normal));
AddStep("Notes with input", () => createPlayfieldWithNotes(false));
AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction));
AddStep("Notes", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Normal));
AddWaitStep(10);
AddStep("Left special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Left));
AddWaitStep(10);
AddStep("Right special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Right));
AddWaitStep(10);
AddStep("Notes with gravity", () => createPlayfieldWithNotes(true));
AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction));
}
private void triggerKeyDown(Column column)
{
column.TriggerKeyDown(new InputState(), new KeyDownEventArgs
column.TriggerOnKeyDown(new InputState(), new KeyDownEventArgs
{
Key = column.Key,
Repeat = false
@@ -89,7 +136,7 @@ namespace osu.Desktop.VisualTests.Tests
private void triggerKeyUp(Column column)
{
column.TriggerKeyUp(new InputState(), new KeyUpEventArgs
column.TriggerOnKeyUp(new InputState(), new KeyUpEventArgs
{
Key = column.Key
});
@@ -35,7 +35,7 @@ namespace osu.Desktop.VisualTests.Tests
});
AddStep(@"Pause", delegate {
if(failOverlay.State == Visibility.Visible)
if (failOverlay.State == Visibility.Visible)
{
failOverlay.Hide();
}
@@ -0,0 +1,55 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.ReplaySettings;
namespace osu.Desktop.VisualTests.Tests
{
internal class TestCaseReplaySettingsOverlay : TestCase
{
public override string Description => @"Settings visible in replay/auto";
private ExampleContainer container;
public override void Reset()
{
base.Reset();
Add(new ReplaySettingsOverlay()
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
});
Add(container = new ExampleContainer());
AddStep(@"Add button", () => container.Add(new OsuButton
{
RelativeSizeAxes = Axes.X,
Text = @"Button",
}));
AddStep(@"Add checkbox", () => container.Add(new ReplayCheckbox
{
LabelText = "Checkbox",
}));
AddStep(@"Add textbox", () => container.Add(new FocusedTextBox
{
RelativeSizeAxes = Axes.X,
Height = 30,
PlaceholderText = "Textbox",
HoldFocus = false,
}));
}
private class ExampleContainer : ReplayGroup
{
protected override string Title => @"example";
}
}
}
@@ -79,7 +79,8 @@ namespace osu.Desktop.VisualTests.Tests
{
score.Current.Value += 300 + (ulong)(300.0 * (comboCounter.Current > 0 ? comboCounter.Current - 1 : 0) / 25.0);
comboCounter.Increment();
numerator++; denominator++;
numerator++;
denominator++;
accuracyCounter.SetFraction(numerator, denominator);
});
@@ -0,0 +1,220 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Timing;
namespace osu.Desktop.VisualTests.Tests
{
public class TestCaseScrollingHitObjects : TestCase
{
public override string Description => "SpeedAdjustmentContainer/DrawableTimingSection";
private SpeedAdjustmentCollection adjustmentCollection;
private BindableDouble timeRangeBindable;
private OsuSpriteText timeRangeText;
private OsuSpriteText bottomLabel;
private SpriteText topTime, bottomTime;
public override void Reset()
{
base.Reset();
timeRangeBindable = new BindableDouble(2000)
{
MinValue = 200,
MaxValue = 4000,
};
SliderBar<double> timeRange;
Add(timeRange = new BasicSliderBar<double>
{
Size = new Vector2(200, 20),
SelectionColor = Color4.Pink,
KeyboardStep = 100
});
Add(timeRangeText = new OsuSpriteText
{
X = 210,
TextSize = 16,
});
timeRange.Current.BindTo(timeRangeBindable);
timeRangeBindable.ValueChanged += v => timeRangeText.Text = $"Visible Range: {v:#,#.#}";
timeRangeBindable.ValueChanged += v => bottomLabel.Text = $"t minus {v:#,#}";
Add(new Drawable[]
{
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(100, 500),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.25f
},
adjustmentCollection = new SpeedAdjustmentCollection(Axes.Y)
{
RelativeSizeAxes = Axes.Both,
VisibleTimeRange = timeRangeBindable,
Masking = true,
},
new OsuSpriteText
{
Text = "t minus 0",
Margin = new MarginPadding(2),
TextSize = 14,
Anchor = Anchor.TopRight,
},
bottomLabel = new OsuSpriteText
{
Text = "t minus x",
Margin = new MarginPadding(2),
TextSize = 14,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomLeft,
},
topTime = new OsuSpriteText
{
Margin = new MarginPadding(2),
TextSize = 14,
Anchor = Anchor.TopLeft,
Origin = Anchor.TopRight,
},
bottomTime = new OsuSpriteText
{
Margin = new MarginPadding(2),
TextSize = 14,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomRight,
},
}
}
});
timeRangeBindable.TriggerChange();
adjustmentCollection.Add(new TestSpeedAdjustmentContainer(new MultiplierControlPoint()));
AddStep("Add hit object", () => adjustmentCollection.Add(new TestDrawableHitObject(new HitObject { StartTime = Time.Current + 2000 })));
}
protected override void Update()
{
base.Update();
topTime.Text = Time.Current.ToString("#,#");
bottomTime.Text = (Time.Current + timeRangeBindable.Value).ToString("#,#");
}
private class TestSpeedAdjustmentContainer : SpeedAdjustmentContainer
{
public TestSpeedAdjustmentContainer(MultiplierControlPoint controlPoint)
: base(controlPoint)
{
}
protected override DrawableTimingSection CreateTimingSection() => new TestDrawableTimingSection(ControlPoint);
private class TestDrawableTimingSection : DrawableTimingSection
{
private readonly MultiplierControlPoint controlPoint;
public TestDrawableTimingSection(MultiplierControlPoint controlPoint)
{
this.controlPoint = controlPoint;
}
protected override void Update()
{
base.Update();
Y = (float)(controlPoint.StartTime - Time.Current);
}
}
}
private class TestDrawableHitObject : DrawableHitObject
{
private readonly Box background;
private const float height = 14;
public TestDrawableHitObject(HitObject hitObject)
: base(hitObject)
{
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
RelativePositionAxes = Axes.Y;
Y = (float)hitObject.StartTime;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.X,
Height = height,
},
new Box
{
RelativeSizeAxes = Axes.X,
Colour = Color4.Cyan,
Height = 1,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4.Black,
TextSize = height,
Font = @"Exo2.0-BoldItalic",
Text = $"{hitObject.StartTime:#,#}"
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
FadeInFromZero(250, EasingTypes.OutQuint);
}
private bool hasExpired;
protected override void Update()
{
base.Update();
if (Time.Current >= HitObject.StartTime)
{
background.Colour = Color4.Red;
if (!hasExpired)
{
using (BeginDelayedSequence(200))
{
FadeOut(200);
Expire();
}
hasExpired = true;
}
}
}
}
}
}
@@ -0,0 +1,19 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Testing;
using osu.Game.Screens.Play;
namespace osu.Desktop.VisualTests.Tests
{
internal class TestCaseSkipButton : TestCase
{
public override string Description => @"Skip skip skippediskip";
public override void Reset()
{
base.Reset();
Add(new SkipButton(Clock.CurrentTime + 5000));
}
}
}
@@ -0,0 +1,85 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Users;
namespace osu.Desktop.VisualTests.Tests
{
public class TestCaseSocial : TestCase
{
public override string Description => @"social browser overlay";
public override void Reset()
{
base.Reset();
SocialOverlay s = new SocialOverlay
{
Users = new[]
{
new User
{
Username = @"flyte",
Id = 3103765,
Country = new Country { FlagName = @"JP" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
},
new User
{
Username = @"Cookiezi",
Id = 124493,
Country = new Country { FlagName = @"KR" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
},
new User
{
Username = @"Angelsim",
Id = 1777162,
Country = new Country { FlagName = @"KR" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
new User
{
Username = @"Rafis",
Id = 2558286,
Country = new Country { FlagName = @"PL" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg",
},
new User
{
Username = @"hvick225",
Id = 50265,
Country = new Country { FlagName = @"TW" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg",
},
new User
{
Username = @"peppy",
Id = 2,
Country = new Country { FlagName = @"AU" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
},
new User
{
Username = @"filsdelama",
Id = 2831793,
Country = new Country { FlagName = @"FR" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c7.jpg"
},
new User
{
Username = @"_index",
Id = 652457,
Country = new Country { FlagName = @"RU" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c8.jpg"
},
},
};
Add(s);
AddStep(@"toggle", s.ToggleVisibility);
}
}
}
@@ -1,92 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Configuration;
using OpenTK;
using osu.Game.Graphics;
namespace osu.Desktop.VisualTests.Tests
{
internal class TestCaseTooltip : TestCase
{
public override string Description => "tests tooltips on various elements";
public override void Reset()
{
base.Reset();
OsuSliderBar<int> slider;
OsuSliderBar<double> sliderDouble;
const float width = 400;
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new TooltipTextContainer("text with a tooltip"),
new TooltipTextContainer("more text with another tooltip"),
new TooltipTextbox
{
Text = "a textbox with a tooltip",
Size = new Vector2(width,30),
},
slider = new OsuSliderBar<int>
{
Width = width,
},
sliderDouble = new OsuSliderBar<double>
{
Width = width,
},
},
},
};
slider.Current.BindTo(new BindableInt(5)
{
MaxValue = 10,
MinValue = 0
});
sliderDouble.Current.BindTo(new BindableDouble(0.5)
{
MaxValue = 1,
MinValue = 0
});
}
private class TooltipTextContainer : Container, IHasTooltip
{
private readonly OsuSpriteText text;
public string TooltipText => text.Text;
public TooltipTextContainer(string tooltipText)
{
AutoSizeAxes = Axes.Both;
Children = new[]
{
text = new OsuSpriteText
{
Text = tooltipText,
}
};
}
}
private class TooltipTextbox : OsuTextBox, IHasTooltip
{
public string TooltipText => Text;
}
}
}
@@ -3,20 +3,18 @@
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.Play;
namespace osu.Desktop.VisualTests.Tests
{
internal class TestCaseTwoLayerButton : TestCase
{
public override string Description => @"Back and skip and what not";
public override string Description => @"Mostly back button";
public override void Reset()
{
base.Reset();
Add(new BackButton());
Add(new SkipButton(Clock.CurrentTime + 5000));
}
}
}
@@ -0,0 +1,57 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Testing;
using osu.Framework.Graphics;
using osu.Game.Users;
using osu.Framework.Graphics.Containers;
using OpenTK;
namespace osu.Desktop.VisualTests.Tests
{
internal class TestCaseUserPanel : TestCase
{
public override string Description => @"Panels for displaying a user's status";
public override void Reset()
{
base.Reset();
UserPanel flyte;
UserPanel peppy;
Add(new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10f),
Children = new[]
{
flyte = new UserPanel(new User
{
Username = @"flyte",
Id = 3103765,
Country = new Country { FlagName = @"JP" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
}) { Width = 300 },
peppy = new UserPanel(new User
{
Username = @"peppy",
Id = 2,
Country = new Country { FlagName = @"AU" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg"
}) { Width = 300 },
},
});
flyte.Status.Value = new UserStatusOnline();
peppy.Status.Value = new UserStatusSoloGame();
AddStep(@"spectating", () => { flyte.Status.Value = new UserStatusSpectating(); });
AddStep(@"multiplaying", () => { flyte.Status.Value = new UserStatusMultiplayerGame(); });
AddStep(@"modding", () => { flyte.Status.Value = new UserStatusModding(); });
AddStep(@"offline", () => { flyte.Status.Value = new UserStatusOffline(); });
AddStep(@"null status", () => { flyte.Status.Value = null; });
}
}
}
@@ -87,11 +87,13 @@
<Reference Include="Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="OpenTK, Version=2.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll</HintPath>
<Reference Include="OpenTK, Version=3.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="SharpCompress, Version=0.15.2.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\SharpCompress.0.15.2\lib\net45\SharpCompress.dll</HintPath>
<Reference Include="SharpCompress, Version=0.17.1.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\SharpCompress.0.17.1\lib\net45\SharpCompress.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="SQLite.Net, Version=3.1.0.0, Culture=neutral, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
@@ -187,6 +189,7 @@
<Compile Include="Program.cs" />
<Compile Include="Tests\TestCaseChatDisplay.cs" />
<Compile Include="Tests\TestCaseBeatmapDetails.cs" />
<Compile Include="Tests\TestCaseContextMenu.cs" />
<Compile Include="Tests\TestCaseDrawings.cs" />
<Compile Include="Tests\TestCaseGamefield.cs" />
<Compile Include="Tests\TestCaseGraph.cs" />
@@ -196,6 +199,7 @@
<Compile Include="Tests\TestCaseMusicController.cs" />
<Compile Include="Tests\TestCaseNotificationManager.cs" />
<Compile Include="Tests\TestCaseOnScreenDisplay.cs" />
<Compile Include="Tests\TestCaseReplaySettingsOverlay.cs" />
<Compile Include="Tests\TestCasePlayer.cs" />
<Compile Include="Tests\TestCaseHitObjects.cs" />
<Compile Include="Tests\TestCaseKeyCounter.cs" />
@@ -203,12 +207,13 @@
<Compile Include="Tests\TestCaseReplay.cs" />
<Compile Include="Tests\TestCaseResults.cs" />
<Compile Include="Tests\TestCaseScoreCounter.cs" />
<Compile Include="Tests\TestCaseScrollingHitObjects.cs" />
<Compile Include="Tests\TestCaseSkipButton.cs" />
<Compile Include="Tests\TestCaseTabControl.cs" />
<Compile Include="Tests\TestCaseTaikoHitObjects.cs" />
<Compile Include="Tests\TestCaseTaikoPlayfield.cs" />
<Compile Include="Tests\TestCaseTextAwesome.cs" />
<Compile Include="Tests\TestCasePlaySongSelect.cs" />
<Compile Include="Tests\TestCaseTooltip.cs" />
<Compile Include="Tests\TestCaseTwoLayerButton.cs" />
<Compile Include="VisualTestGame.cs" />
<Compile Include="Platform\TestStorage.cs" />
@@ -220,6 +225,11 @@
<Compile Include="Tests\TestCaseLeaderboard.cs" />
<Compile Include="Beatmaps\TestWorkingBeatmap.cs" />
<Compile Include="Tests\TestCaseBeatmapDetailArea.cs" />
<Compile Include="Tests\TestCaseDrawableRoom.cs" />
<Compile Include="Tests\TestCaseUserPanel.cs" />
<Compile Include="Tests\TestCaseDirect.cs" />
<Compile Include="Tests\TestCaseSocial.cs" />
<Compile Include="Tests\TestCaseBreadcrumbs.cs" />
</ItemGroup>
<ItemGroup />
<ItemGroup />
+2 -2
View File
@@ -5,8 +5,8 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste
-->
<packages>
<package id="Newtonsoft.Json" version="10.0.2" targetFramework="net45" />
<package id="ppy.OpenTK" version="2.0.50727.1341" targetFramework="net45" />
<package id="SharpCompress" version="0.15.2" targetFramework="net45" />
<package id="ppy.OpenTK" version="3.0" targetFramework="net45" />
<package id="SharpCompress" version="0.17.1" targetFramework="net45" />
<package id="SQLite.Net.Core-PCL" version="3.1.1" targetFramework="net45" />
<package id="SQLite.Net-PCL" version="3.1.1" targetFramework="net45" />
<package id="SQLiteNetExtensions" version="1.3.0" targetFramework="net45" />
@@ -37,5 +37,7 @@ namespace osu.Desktop.Beatmaps.IO
{
// no-op
}
public override Stream GetUnderlyingStream() => null;
}
}
+10 -5
View File
@@ -121,18 +121,23 @@
</Reference>
<Reference Include="mscorlib" />
<Reference Include="NuGet.Squirrel, Version=3.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\NuGet.Squirrel.dll</HintPath>
<HintPath>$(SolutionDir)\packages\squirrel.windows.1.7.5\lib\Net45\NuGet.Squirrel.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="OpenTK, Version=2.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll</HintPath>
<Reference Include="OpenTK, Version=3.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="SharpCompress, Version=0.17.1.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\SharpCompress.0.17.1\lib\net45\SharpCompress.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Splat, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Splat.2.0.0\lib\Net45\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Squirrel, Version=1.5.2.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\squirrel.windows.1.5.2\lib\Net45\Squirrel.dll</HintPath>
<Reference Include="Squirrel, Version=1.7.5.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\squirrel.windows.1.7.5\lib\Net45\Squirrel.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
+3 -2
View File
@@ -7,7 +7,8 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste
<package id="DeltaCompressionDotNet" version="1.1.0" targetFramework="net45" />
<package id="Microsoft.Net.Http" version="2.2.29" targetFramework="net45" />
<package id="Mono.Cecil" version="0.9.6.4" targetFramework="net45" />
<package id="ppy.OpenTK" version="2.0.50727.1341" targetFramework="net45" />
<package id="ppy.OpenTK" version="3.0" targetFramework="net45" />
<package id="SharpCompress" version="0.17.1" targetFramework="net45" />
<package id="Splat" version="2.0.0" targetFramework="net45" />
<package id="squirrel.windows" version="1.5.2" targetFramework="net45" />
<package id="squirrel.windows" version="1.7.5" targetFramework="net45" />
</packages>
+8 -1
View File
@@ -28,7 +28,14 @@ namespace osu.Game.Rulesets.Catch
{
new CatchModEasy(),
new CatchModNoFail(),
new CatchModHalfTime(),
new MultiMod
{
Mods = new Mod[]
{
new CatchModHalfTime(),
new CatchModDaycore(),
},
},
};
case ModType.DifficultyIncrease:
+5
View File
@@ -32,6 +32,11 @@ namespace osu.Game.Rulesets.Catch.Mods
}
public class CatchModDaycore : ModDaycore
{
public override double ScoreMultiplier => 0.5;
}
public class CatchModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => 1.06;
@@ -33,8 +33,9 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Reference Include="OpenTK, Version=2.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll</HintPath>
<Reference Include="OpenTK, Version=3.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
+1 -1
View File
@@ -5,5 +5,5 @@ Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-->
<packages>
<package id="ppy.OpenTK" version="2.0.50727.1341" targetFramework="net45" />
<package id="ppy.OpenTK" version="3.0" targetFramework="net45" />
</packages>
@@ -11,11 +11,19 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Database;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using OpenTK;
using osu.Game.Audio;
namespace osu.Game.Rulesets.Mania.Beatmaps
{
public class ManiaBeatmapConverter : BeatmapConverter<ManiaHitObject>
{
/// <summary>
/// Maximum number of previous notes to consider for density calculation.
/// </summary>
private const int max_notes_for_density = 7;
protected override IEnumerable<Type> ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) };
private Pattern lastPattern = new Pattern();
@@ -55,6 +63,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
yield return obj;
}
private readonly List<double> prevNoteTimes = new List<double>(max_notes_for_density);
private double density = int.MaxValue;
private void computeDensity(double newNoteTime)
{
if (prevNoteTimes.Count == max_notes_for_density)
prevNoteTimes.RemoveAt(0);
prevNoteTimes.Add(newNoteTime);
density = (prevNoteTimes[prevNoteTimes.Count - 1] - prevNoteTimes[0]) / prevNoteTimes.Count;
}
private double lastTime;
private Vector2 lastPosition;
private PatternType lastStair;
private void recordNote(double time, Vector2 position)
{
lastTime = time;
lastPosition = position;
}
/// <summary>
/// Method that generates hit objects for osu!mania specific beatmaps.
/// </summary>
@@ -83,28 +111,31 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
// Following lines currently commented out to appease resharper
//Patterns.PatternGenerator conversion = null;
Patterns.PatternGenerator conversion = null;
if (distanceData != null)
{
// Slider
}
conversion = new DistanceObjectPatternGenerator(random, original, beatmap, lastPattern);
else if (endTimeData != null)
{
// Spinner
}
conversion = new EndTimeObjectPatternGenerator(random, original, beatmap);
else if (positionData != null)
{
// Circle
computeDensity(original.StartTime);
conversion = new HitObjectPatternGenerator(random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair);
recordNote(original.StartTime, positionData.Position);
}
//if (conversion == null)
return null;
if (conversion == null)
return null;
//Pattern newPattern = conversion.Generate();
//lastPattern = newPattern;
Pattern newPattern = conversion.Generate();
lastPattern = newPattern;
//return newPattern.HitObjects;
var stairPatternGenerator = (HitObjectPatternGenerator)conversion;
lastStair = stairPatternGenerator.StairType;
return newPattern.HitObjects;
}
/// <summary>
@@ -131,9 +162,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
pattern.Add(new HoldNote
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Duration = endTimeData.Duration,
Column = column,
Head = { Samples = sampleInfoListAt(HitObject.StartTime) },
Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) },
});
}
else if (positionData != null)
@@ -148,6 +180,24 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return pattern;
}
/// <summary>
/// Retrieves the sample info list at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param>
/// <returns></returns>
private SampleInfoList sampleInfoListAt(double time)
{
var curveData = HitObject as IHasCurve;
if (curveData == null)
return HitObject.Samples;
double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.RepeatCount;
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
return curveData.RepeatSamples[index];
}
}
}
}
@@ -0,0 +1,488 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
/// <summary>
/// A pattern generator for IHasDistance hit objects.
/// </summary>
internal class DistanceObjectPatternGenerator : PatternGenerator
{
/// <summary>
/// Base osu! slider scoring distance.
/// </summary>
private const float osu_base_scoring_distance = 100;
private readonly double endTime;
private readonly double segmentDuration;
private readonly int repeatCount;
private PatternType convertType;
public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern)
: base(random, hitObject, beatmap, previousPattern)
{
convertType = PatternType.None;
if (Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode)
convertType = PatternType.LowProbability;
var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats;
repeatCount = repeatsData?.RepeatCount ?? 1;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
// The true distance, accounting for any repeats
double distance = (distanceData?.Distance ?? 0) * repeatCount;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier / (timingPoint.BeatLength * difficultyPoint.SpeedMultiplier);
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
endTime = hitObject.StartTime + osuDuration;
segmentDuration = (endTime - HitObject.StartTime) / repeatCount;
}
public override Pattern Generate()
{
if (repeatCount > 1)
{
if (segmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1);
if (segmentDuration <= 120)
{
convertType |= PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, repeatCount + 1);
}
if (segmentDuration <= 160)
return generateStair(HitObject.StartTime);
if (segmentDuration <= 200 && ConversionDifficulty > 3)
return generateRandomMultipleNotes(HitObject.StartTime);
double duration = endTime - HitObject.StartTime;
if (duration >= 4000)
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0);
if (segmentDuration > 400 && repeatCount < AvailableColumns - 1 - RandomStart)
return generateTiledHoldNotes(HitObject.StartTime);
return generateHoldAndNormalNotes(HitObject.StartTime);
}
if (segmentDuration <= 110)
{
if (PreviousPattern.ColumnWithObjects < AvailableColumns)
convertType |= PatternType.ForceNotStack;
else
convertType &= ~PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, segmentDuration < 80 ? 1 : 2);
}
if (ConversionDifficulty > 6.5)
{
if ((convertType & PatternType.LowProbability) > 0)
return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03);
}
if (ConversionDifficulty > 4)
{
if ((convertType & PatternType.LowProbability) > 0)
return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0);
}
if (ConversionDifficulty > 2.5)
{
if ((convertType & PatternType.LowProbability) > 0)
return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0);
}
if ((convertType & PatternType.LowProbability) > 0)
return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0);
}
/// <summary>
/// Generates random hold notes that start at an span the same amount of rows.
/// </summary>
/// <param name="startTime">Start time of each hold note.</param>
/// <param name="noteCount">Number of hold notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomHoldNotes(double startTime, int noteCount)
{
// - - - -
// ■ - ■ ■
// □ - □ □
// ■ - ■ ■
var pattern = new Pattern();
int usableColumns = AvailableColumns - RandomStart - PreviousPattern.ColumnWithObjects;
int nextColumn = Random.Next(RandomStart, AvailableColumns);
for (int i = 0; i < Math.Min(usableColumns, noteCount); i++)
{
while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn)) //find available column
nextColumn = Random.Next(RandomStart, AvailableColumns);
addToPattern(pattern, nextColumn, startTime, endTime);
}
// This is can't be combined with the above loop due to RNG
for (int i = 0; i < noteCount - usableColumns; i++)
{
while (pattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, AvailableColumns);
addToPattern(pattern, nextColumn, startTime, endTime);
}
return pattern;
}
/// <summary>
/// Generates random notes, with one note per row and no stacking.
/// </summary>
/// <param name="startTime">The start time.</param>
/// <param name="noteCount">The number of notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomNotes(double startTime, int noteCount)
{
// - - - -
// x - - -
// - - x -
// - - - x
// x - - -
var pattern = new Pattern();
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if ((convertType & PatternType.ForceNotStack) > 0 && PreviousPattern.ColumnWithObjects < AvailableColumns)
{
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, AvailableColumns);
}
int lastColumn = nextColumn;
for (int i = 0; i < noteCount; i++)
{
addToPattern(pattern, nextColumn, startTime, startTime);
while (nextColumn == lastColumn)
nextColumn = Random.Next(RandomStart, AvailableColumns);
lastColumn = nextColumn;
startTime += segmentDuration;
}
return pattern;
}
/// <summary>
/// Generates a stair of notes, with one note per row.
/// </summary>
/// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateStair(double startTime)
{
// - - - -
// x - - -
// - x - -
// - - x -
// - - - x
// - - x -
// - x - -
// x - - -
var pattern = new Pattern();
int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
bool increasing = Random.NextDouble() > 0.5;
for (int i = 0; i <= repeatCount; i++)
{
addToPattern(pattern, column, startTime, startTime);
startTime += segmentDuration;
// Check if we're at the borders of the stage, and invert the pattern if so
if (increasing)
{
if (column >= AvailableColumns - 1)
{
increasing = false;
column--;
}
else
column++;
}
else
{
if (column <= RandomStart)
{
increasing = true;
column++;
}
else
column--;
}
}
return pattern;
}
/// <summary>
/// Generates random notes with 1-2 notes per row and no stacking.
/// </summary>
/// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomMultipleNotes(double startTime)
{
// - - - -
// x - - -
// - x x -
// - - - x
// x - x -
var pattern = new Pattern();
bool legacy = AvailableColumns >= 4 && AvailableColumns <= 8;
int interval = Random.Next(1, AvailableColumns - (legacy ? 1 : 0));
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
for (int i = 0; i <= repeatCount; i++)
{
addToPattern(pattern, nextColumn, startTime, startTime);
nextColumn += interval;
if (nextColumn >= AvailableColumns - RandomStart)
nextColumn = nextColumn - AvailableColumns - RandomStart + (legacy ? 1 : 0);
nextColumn += RandomStart;
// If we're in 2K, let's not add many consecutive doubles
if (AvailableColumns > 2)
addToPattern(pattern, nextColumn, startTime, startTime);
nextColumn = Random.Next(RandomStart, AvailableColumns);
startTime += segmentDuration;
}
return pattern;
}
/// <summary>
/// Generates random hold notes. The amount of hold notes generated is determined by probabilities.
/// </summary>
/// <param name="startTime">The hold note start time.</param>
/// <param name="p2">The probability required for 2 hold notes to be generated.</param>
/// <param name="p3">The probability required for 3 hold notes to be generated.</param>
/// <param name="p4">The probability required for 4 hold notes to be generated.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4)
{
// - - - -
// ■ - ■ ■
// □ - □ □
// ■ - ■ ■
switch (AvailableColumns)
{
case 2:
p2 = 0;
p3 = 0;
p4 = 0;
break;
case 3:
p2 = Math.Max(p2, 0.1);
p3 = 0;
p4 = 0;
break;
case 4:
p2 = Math.Max(p2, 0.3);
p3 = Math.Max(p3, 0.04);
p4 = 0;
break;
case 5:
p2 = Math.Max(p2, 0.34);
p3 = Math.Max(p3, 0.1);
p4 = Math.Max(p4, 0.03);
break;
}
Func<SampleInfo, bool> isDoubleSample = sample => sample.Name == SampleInfo.HIT_CLAP && sample.Name == SampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = (convertType & PatternType.LowProbability) == 0;
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes)
p2 = 1;
return generateRandomHoldNotes(startTime, GetRandomNoteCount(p2, p3, p4));
}
/// <summary>
/// Generates tiled hold notes. You can think of this as a stair of hold notes.
/// </summary>
/// <param name="startTime">The first hold note start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateTiledHoldNotes(double startTime)
{
// - - - -
// ■ ■ ■ ■
// □ □ □ □
// □ □ □ □
// □ □ □ ■
// □ □ ■ -
// □ ■ - -
// ■ - - -
var pattern = new Pattern();
int columnRepeat = Math.Min(repeatCount, AvailableColumns);
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if ((convertType & PatternType.ForceNotStack) > 0 && PreviousPattern.ColumnWithObjects < AvailableColumns)
{
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, AvailableColumns);
}
for (int i = 0; i < columnRepeat; i++)
{
while (pattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, AvailableColumns);
addToPattern(pattern, nextColumn, startTime, endTime);
startTime += segmentDuration;
}
return pattern;
}
/// <summary>
/// Generates a hold note alongside normal notes.
/// </summary>
/// <param name="startTime">The start time of notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateHoldAndNormalNotes(double startTime)
{
// - - - -
// ■ x x -
// ■ - x x
// ■ x - x
// ■ - x x
var pattern = new Pattern();
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if ((convertType & PatternType.ForceNotStack) > 0 && PreviousPattern.ColumnWithObjects < AvailableColumns)
{
while (PreviousPattern.ColumnHasObject(holdColumn))
holdColumn = Random.Next(RandomStart, AvailableColumns);
}
// Create the hold note
addToPattern(pattern, holdColumn, startTime, endTime);
int noteCount = 1;
if (ConversionDifficulty > 6.5)
noteCount = GetRandomNoteCount(0.63, 0);
else if (ConversionDifficulty > 4)
noteCount = GetRandomNoteCount(AvailableColumns < 6 ? 0.12 : 0.45, 0);
else if (ConversionDifficulty > 2.5)
noteCount = GetRandomNoteCount(AvailableColumns < 6 ? 0 : 0.24, 0);
noteCount = Math.Min(AvailableColumns - 1, noteCount);
bool ignoreHead = !sampleInfoListAt(startTime).Any(s => s.Name == SampleInfo.HIT_WHISTLE || s.Name == SampleInfo.HIT_FINISH || s.Name == SampleInfo.HIT_CLAP);
int nextColumn = Random.Next(RandomStart, AvailableColumns);
var rowPattern = new Pattern();
for (int i = 0; i <= repeatCount; i++)
{
if (!(ignoreHead && startTime == HitObject.StartTime))
{
for (int j = 0; j < noteCount; j++)
{
while (rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn)
nextColumn = Random.Next(RandomStart, AvailableColumns);
addToPattern(rowPattern, nextColumn, startTime, startTime);
}
}
pattern.Add(rowPattern);
rowPattern.Clear();
startTime += segmentDuration;
}
return pattern;
}
/// <summary>
/// Retrieves the sample info list at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param>
/// <returns></returns>
private SampleInfoList sampleInfoListAt(double time)
{
var curveData = HitObject as IHasCurve;
if (curveData == null)
return HitObject.Samples;
double segmentTime = (endTime - HitObject.StartTime) / repeatCount;
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
return curveData.RepeatSamples[index];
}
/// <summary>
/// Constructs and adds a note to a pattern.
/// </summary>
/// <param name="pattern">The pattern to add to.</param>
/// <param name="column">The column to add the note to.</param>
/// <param name="startTime">The start time of the note.</param>
/// <param name="endTime">The end time of the note (set to <paramref name="startTime"/> for a non-hold note).</param>
private void addToPattern(Pattern pattern, int column, double startTime, double endTime)
{
ManiaHitObject newObject;
if (startTime == endTime)
{
newObject = new Note
{
StartTime = startTime,
Samples = sampleInfoListAt(startTime),
Column = column
};
}
else
{
var holdNote = new HoldNote
{
StartTime = startTime,
Column = column,
Duration = endTime - startTime,
Head = { Samples = sampleInfoListAt(startTime) },
Tail = { Samples = sampleInfoListAt(endTime) }
};
newObject = holdNote;
}
pattern.Add(newObject);
}
}
}
@@ -0,0 +1,101 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class EndTimeObjectPatternGenerator : PatternGenerator
{
private readonly double endTime;
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap)
: base(random, hitObject, beatmap, new Pattern())
{
var endtimeData = HitObject as IHasEndTime;
endTime = endtimeData?.EndTime ?? 0;
}
public override Pattern Generate()
{
var pattern = new Pattern();
bool generateHold = endTime - HitObject.StartTime >= 100;
if (AvailableColumns == 8)
{
if (HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_FINISH) && endTime - HitObject.StartTime < 1000)
addToPattern(pattern, 0, generateHold);
else
addToPattern(pattern, getNextRandomColumn(RandomStart), generateHold);
}
else if (AvailableColumns > 0)
addToPattern(pattern, getNextRandomColumn(0), generateHold);
return pattern;
}
/// <summary>
/// Picks a random column after a column.
/// </summary>
/// <param name="start">The starting column.</param>
/// <returns>A random column after <paramref name="start"/>.</returns>
private int getNextRandomColumn(int start)
{
int nextColumn = Random.Next(start, AvailableColumns);
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(start, AvailableColumns);
return nextColumn;
}
/// <summary>
/// Constructs and adds a note to a pattern.
/// </summary>
/// <param name="pattern">The pattern to add to.</param>
/// <param name="column">The column to add the note to.</param>
/// <param name="holdNote">Whether to add a hold note.</param>
private void addToPattern(Pattern pattern, int column, bool holdNote)
{
ManiaHitObject newObject;
if (holdNote)
{
var hold = new HoldNote
{
StartTime = HitObject.StartTime,
Column = column,
Duration = endTime - HitObject.StartTime
};
hold.Head.Samples.Add(new SampleInfo
{
Name = SampleInfo.HIT_NORMAL
});
hold.Tail.Samples = HitObject.Samples;
newObject = hold;
}
else
{
newObject = new Note
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Column = column
};
}
pattern.Add(newObject);
}
}
}
@@ -0,0 +1,404 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using OpenTK;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class HitObjectPatternGenerator : PatternGenerator
{
public PatternType StairType { get; private set; }
private readonly PatternType convertType;
public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair)
: base(random, hitObject, beatmap, previousPattern)
{
StairType = lastStair;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
EffectControlPoint effectPoint = beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime);
var positionData = hitObject as IHasPosition;
float positionSeparation = ((positionData?.Position ?? Vector2.Zero) - previousPosition).Length;
double timeSeparation = hitObject.StartTime - previousTime;
if (timeSeparation <= 125)
{
// More than 120 BPM
convertType |= PatternType.ForceNotStack;
}
if (timeSeparation <= 80)
{
// More than 187 BPM
convertType |= PatternType.ForceNotStack | PatternType.KeepSingle;
}
else if (timeSeparation <= 95)
{
// More than 157 BPM
convertType |= PatternType.ForceNotStack | PatternType.KeepSingle | lastStair;
}
else if (timeSeparation <= 105)
{
// More than 140 BPM
convertType |= PatternType.ForceNotStack | PatternType.LowProbability;
}
else if (timeSeparation <= 125)
{
// More than 120 BPM
convertType |= PatternType.ForceNotStack;
}
else if (timeSeparation <= 135 && positionSeparation < 20)
{
// More than 111 BPM stream
convertType |= PatternType.Cycle | PatternType.KeepSingle;
}
else if (timeSeparation <= 150 & positionSeparation < 20)
{
// More than 100 BPM stream
convertType |= PatternType.ForceStack | PatternType.LowProbability;
}
else if (positionSeparation < 20 && density >= timingPoint.BeatLength / 2.5)
{
// Low density stream
convertType |= PatternType.Reverse | PatternType.LowProbability;
}
else if (density < timingPoint.BeatLength / 2.5 || effectPoint.KiaiMode)
{
// High density
}
else
convertType |= PatternType.LowProbability;
}
public override Pattern Generate()
{
int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0;
if ((convertType & PatternType.Reverse) > 0 && PreviousPattern.HitObjects.Any())
{
// Generate a new pattern by copying the last hit objects in reverse-column order
var pattern = new Pattern();
for (int i = RandomStart; i < AvailableColumns; i++)
if (PreviousPattern.ColumnHasObject(i))
addToPattern(pattern, RandomStart + AvailableColumns - i - 1);
return pattern;
}
if ((convertType & PatternType.Cycle) > 0 && PreviousPattern.HitObjects.Count() == 1
// If we convert to 7K + 1, let's not overload the special key
&& (AvailableColumns != 8 || lastColumn != 0)
// Make sure the last column was not the centre column
&& (AvailableColumns % 2 == 0 || lastColumn != AvailableColumns / 2))
{
// Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object)
var pattern = new Pattern();
int column = RandomStart + AvailableColumns - lastColumn - 1;
addToPattern(pattern, column);
return pattern;
}
if ((convertType & PatternType.ForceStack) > 0 && PreviousPattern.HitObjects.Any())
{
// Generate a new pattern by placing on the already filled columns
var pattern = new Pattern();
for (int i = RandomStart; i < AvailableColumns; i++)
if (PreviousPattern.ColumnHasObject(i))
addToPattern(pattern, i);
return pattern;
}
if ((convertType & PatternType.Stair) > 0 && PreviousPattern.HitObjects.Count() == 1)
{
// Generate a new pattern by placing on the next column, cycling back to the start if there is no "next"
var pattern = new Pattern();
int targetColumn = lastColumn + 1;
if (targetColumn == AvailableColumns)
{
targetColumn = RandomStart;
StairType = PatternType.ReverseStair;
}
addToPattern(pattern, targetColumn);
return pattern;
}
if ((convertType & PatternType.ReverseStair) > 0 && PreviousPattern.HitObjects.Count() == 1)
{
// Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous"
var pattern = new Pattern();
int targetColumn = lastColumn - 1;
if (targetColumn == RandomStart - 1)
{
targetColumn = AvailableColumns - 1;
StairType = PatternType.Stair;
}
addToPattern(pattern, targetColumn);
return pattern;
}
if ((convertType & PatternType.KeepSingle) > 0)
return generateRandomNotes(1);
if ((convertType & PatternType.Mirror) > 0)
{
if (ConversionDifficulty > 6.5)
return generateRandomPatternWithMirrored(0.12, 0.38, 0.12);
if (ConversionDifficulty > 4)
return generateRandomPatternWithMirrored(0.12, 0.17, 0);
return generateRandomPatternWithMirrored(0.12, 0, 0);
}
if (ConversionDifficulty > 6.5)
{
if ((convertType & PatternType.LowProbability) > 0)
return generateRandomPattern(0.78, 0.42, 0, 0);
return generateRandomPattern(1, 0.62, 0, 0);
}
if (ConversionDifficulty > 4)
{
if ((convertType & PatternType.LowProbability) > 0)
return generateRandomPattern(0.35, 0.08, 0, 0);
return generateRandomPattern(0.52, 0.15, 0, 0);
}
if (ConversionDifficulty > 2)
{
if ((convertType & PatternType.LowProbability) > 0)
return generateRandomPattern(0.18, 0, 0, 0);
return generateRandomPattern(0.45, 0, 0, 0);
}
return generateRandomPattern(0, 0, 0, 0);
}
/// <summary>
/// Generates random notes.
/// <para>
/// This will generate as many as it can up to <paramref name="noteCount"/>, accounting for
/// any stacks if <see cref="convertType"/> is forcing no stacks.
/// </para>
/// </summary>
/// <param name="noteCount">The amount of notes to generate.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomNotes(int noteCount)
{
var pattern = new Pattern();
bool allowStacking = (convertType & PatternType.ForceNotStack) == 0;
if (!allowStacking)
noteCount = Math.Min(noteCount, AvailableColumns - RandomStart - PreviousPattern.ColumnWithObjects);
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
for (int i = 0; i < noteCount; i++)
{
while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking)
{
if ((convertType & PatternType.Gathered) > 0)
{
nextColumn++;
if (nextColumn == AvailableColumns)
nextColumn = RandomStart;
}
else
nextColumn = Random.Next(RandomStart, AvailableColumns);
}
addToPattern(pattern, nextColumn);
}
return pattern;
}
/// <summary>
/// Whether this hit object can generate a note in the special column.
/// </summary>
private bool hasSpecialColumn => HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_CLAP) && HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_FINISH);
/// <summary>
/// Generates a random pattern.
/// </summary>
/// <param name="p2">Probability for 2 notes to be generated.</param>
/// <param name="p3">Probability for 3 notes to be generated.</param>
/// <param name="p4">Probability for 4 notes to be generated.</param>
/// <param name="p5">Probability for 5 notes to be generated.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomPattern(double p2, double p3, double p4, double p5)
{
var pattern = new Pattern();
pattern.Add(generateRandomNotes(getRandomNoteCount(p2, p3, p4, p5)));
if (RandomStart > 0 && hasSpecialColumn)
addToPattern(pattern, 0);
return pattern;
}
/// <summary>
/// Generates a random pattern which has both normal and mirrored notes.
/// </summary>
/// <param name="centreProbability">The probability for a note to be added to the centre column.</param>
/// <param name="p2">Probability for 2 notes to be generated.</param>
/// <param name="p3">Probability for 3 notes to be generated.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3)
{
var pattern = new Pattern();
bool addToCentre;
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre);
int columnLimit = (AvailableColumns % 2 == 0 ? AvailableColumns : AvailableColumns - 1) / 2;
int nextColumn = Random.Next(RandomStart, columnLimit);
for (int i = 0; i < noteCount; i++)
{
while (pattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, columnLimit);
// Add normal note
addToPattern(pattern, nextColumn);
// Add mirrored note
addToPattern(pattern, RandomStart + AvailableColumns - nextColumn - 1);
}
if (addToCentre)
addToPattern(pattern, AvailableColumns / 2);
if (RandomStart > 0 && hasSpecialColumn)
addToPattern(pattern, 0);
return pattern;
}
/// <summary>
/// Generates a count of notes to be generated from a list of probabilities.
/// </summary>
/// <param name="p2">Probability for 2 notes to be generated.</param>
/// <param name="p3">Probability for 3 notes to be generated.</param>
/// <param name="p4">Probability for 4 notes to be generated.</param>
/// <param name="p5">Probability for 5 notes to be generated.</param>
/// <returns>The amount of notes to be generated.</returns>
private int getRandomNoteCount(double p2, double p3, double p4, double p5)
{
switch (AvailableColumns)
{
case 2:
p2 = 0;
p3 = 0;
p4 = 0;
p5 = 0;
break;
case 3:
p2 = Math.Max(p2, 0.1);
p3 = 0;
p4 = 0;
p5 = 0;
break;
case 4:
p2 = Math.Max(p2, 0.23);
p3 = Math.Max(p3, 0.04);
p4 = 0;
p5 = 0;
break;
case 5:
p3 = Math.Max(p3, 0.15);
p4 = Math.Max(p4, 0.03);
p5 = 0;
break;
}
if (HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_CLAP))
p2 = 1;
return GetRandomNoteCount(p2, p3, p4, p5);
}
/// <summary>
/// Generates a count of notes to be generated from a list of probabilities.
/// </summary>
/// <param name="centreProbability">The probability for a note to be added to the centre column.</param>
/// <param name="p2">Probability for 2 notes to be generated.</param>
/// <param name="p3">Probability for 3 notes to be generated.</param>
/// <param name="addToCentre">Whether to add a note to the centre column.</param>
/// <returns>The amount of notes to be generated. The note to be added to the centre column will NOT be part of this count.</returns>
private int getRandomNoteCountMirrored(double centreProbability, double p2, double p3, out bool addToCentre)
{
addToCentre = false;
if ((convertType & PatternType.ForceNotStack) > 0)
return getRandomNoteCount(p2 / 2, p2, (p2 + p3) / 2, p3);
switch (AvailableColumns)
{
case 2:
centreProbability = 0;
p2 = 0;
p3 = 0;
break;
case 3:
centreProbability = Math.Max(centreProbability, 0.03);
p2 = Math.Max(p2, 0.1);
p3 = 0;
break;
case 4:
centreProbability = 0;
p2 = Math.Max(p2 * 2, 0.2);
p3 = 0;
break;
case 5:
centreProbability = Math.Max(centreProbability, 0.03);
p3 = 0;
break;
case 6:
centreProbability = 0;
p2 = Math.Max(p2 * 2, 0.5);
p3 = Math.Max(p3 * 2, 0.15);
break;
}
double centreVal = Random.NextDouble();
int noteCount = GetRandomNoteCount(p2, p3);
addToCentre = AvailableColumns % 2 != 0 && noteCount != 3 && centreVal > 1 - centreProbability;
return noteCount;
}
/// <summary>
/// Constructs and adds a note to a pattern.
/// </summary>
/// <param name="pattern">The pattern to add to.</param>
/// <param name="column">The column to add the note to.</param>
private void addToPattern(Pattern pattern, int column)
{
pattern.Add(new Note
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Column = column
});
}
}
}
@@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
HitObject firstObject = Beatmap.HitObjects.FirstOrDefault();
double drainTime = (lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0);
drainTime -= Beatmap.EventInfo.TotalBreakTime;
drainTime -= Beatmap.TotalBreakTime;
if (drainTime == 0)
drainTime = 10000;
@@ -15,51 +15,51 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <summary>
/// Keep the same as last row.
/// </summary>
ForceStack = 1,
ForceStack = 1 << 0,
/// <summary>
/// Keep different from last row.
/// </summary>
ForceNotStack = 2,
ForceNotStack = 1 << 1,
/// <summary>
/// Keep as single note at its original position.
/// </summary>
KeepSingle = 4,
KeepSingle = 1 << 2,
/// <summary>
/// Use a lower random value.
/// </summary>
LowProbability = 8,
LowProbability = 1 << 3,
/// <summary>
/// Reserved.
/// </summary>
Alternate = 16,
Alternate = 1 << 4,
/// <summary>
/// Ignore the repeat count.
/// </summary>
ForceSigSlider = 32,
ForceSigSlider = 1 << 5,
/// <summary>
/// Convert slider to circle.
/// </summary>
ForceNotSlider = 64,
ForceNotSlider = 1 << 6,
/// <summary>
/// Notes gathered together.
/// </summary>
Gathered = 128,
Mirror = 256,
Gathered = 1 << 7,
Mirror = 1 << 8,
/// <summary>
/// Change 0 -> 6.
/// </summary>
Reverse = 512,
Reverse = 1 << 9,
/// <summary>
/// 1 -> 5 -> 1 -> 5 like reverse.
/// </summary>
Cycle = 1024,
Cycle = 1 << 10,
/// <summary>
/// Next note will be at column + 1.
/// </summary>
Stair = 2048,
Stair = 1 << 11,
/// <summary>
/// Next note will be at column - 1.
/// </summary>
ReverseStair = 4096
ReverseStair = 1 << 12
}
}
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mania.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
@@ -21,16 +20,16 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
public IEnumerable<ManiaHitObject> HitObjects => hitObjects;
/// <summary>
/// Whether this pattern already contains a hit object in a code.
/// Check whether a column of this patterns contains a hit object.
/// </summary>
/// <param name="column">The column index.</param>
/// <returns>Whether this pattern already contains a hit object in <paramref name="column"/></returns>
public bool IsFilled(int column) => hitObjects.Exists(h => h.Column == column);
/// <returns>Whether the column with index <paramref name="column"/> contains a hit object.</returns>
public bool ColumnHasObject(int column) => hitObjects.Exists(h => h.Column == column);
/// <summary>
/// Amount of columns taken up by hit objects in this pattern.
/// </summary>
public int ColumnsFilled => HitObjects.GroupBy(h => h.Column).Count();
public int ColumnWithObjects => HitObjects.GroupBy(h => h.Column).Count();
/// <summary>
/// Adds a hit object to this pattern.
@@ -42,10 +41,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
/// Copies hit object from another pattern to this one.
/// </summary>
/// <param name="other">The other pattern.</param>
public void Add(Pattern other)
{
other.HitObjects.ForEach(Add);
}
public void Add(Pattern other) => hitObjects.AddRange(other.HitObjects);
/// <summary>
/// Clears this pattern, removing all hit objects.
@@ -140,6 +140,26 @@ namespace osu.Game.Rulesets.Mania.Judgements
Miss = BeatmapDifficulty.DifficultyRange(difficulty, miss_max, miss_mid, miss_min);
}
/// <summary>
/// Retrieves the hit result for a time offset.
/// </summary>
/// <param name="hitOffset">The time offset.</param>
/// <returns>The hit result, or null if the time offset results in a miss.</returns>
public ManiaHitResult? ResultFor(double hitOffset)
{
if (hitOffset <= Perfect / 2)
return ManiaHitResult.Perfect;
if (hitOffset <= Great / 2)
return ManiaHitResult.Great;
if (hitOffset <= Good / 2)
return ManiaHitResult.Good;
if (hitOffset <= Ok / 2)
return ManiaHitResult.Ok;
if (hitOffset <= Bad / 2)
return ManiaHitResult.Bad;
return null;
}
/// <summary>
/// Constructs new hit windows which have been multiplied by a value.
/// </summary>
@@ -0,0 +1,37 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTailJudgement : ManiaJudgement
{
/// <summary>
/// Whether the hold note has been released too early and shouldn't give full score for the release.
/// </summary>
public bool HasBroken;
public override int NumericResultForScore(ManiaHitResult result)
{
switch (result)
{
default:
return base.NumericResultForScore(result);
case ManiaHitResult.Great:
case ManiaHitResult.Perfect:
return base.NumericResultForScore(HasBroken ? ManiaHitResult.Good : result);
}
}
public override int NumericResultForAccuracy(ManiaHitResult result)
{
switch (result)
{
default:
return base.NumericResultForAccuracy(result);
case ManiaHitResult.Great:
case ManiaHitResult.Perfect:
return base.NumericResultForAccuracy(HasBroken ? ManiaHitResult.Good : result);
}
}
}
}
@@ -0,0 +1,13 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
public override bool AffectsCombo => false;
public override int NumericResultForScore(ManiaHitResult result) => 20;
public override int NumericResultForAccuracy(ManiaHitResult result) => 0; // Don't count ticks into accuracy
}
}
@@ -0,0 +1,21 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.ComponentModel;
namespace osu.Game.Rulesets.Mania.Judgements
{
public enum ManiaHitResult
{
[Description("PERFECT")]
Perfect,
[Description("GREAT")]
Great,
[Description("GOOD")]
Good,
[Description("OK")]
Ok,
[Description("BAD")]
Bad
}
}
@@ -2,13 +2,81 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Judgements
{
public class ManiaJudgement : Judgement
{
/// <summary>
/// The maximum possible hit result.
/// </summary>
public const ManiaHitResult MAX_HIT_RESULT = ManiaHitResult.Perfect;
/// <summary>
/// The result value for the combo portion of the score.
/// </summary>
public int ResultValueForScore => Result == HitResult.Miss ? 0 : NumericResultForScore(ManiaResult);
/// <summary>
/// The result value for the accuracy portion of the score.
/// </summary>
public int ResultValueForAccuracy => Result == HitResult.Miss ? 0 : NumericResultForAccuracy(ManiaResult);
/// <summary>
/// The maximum result value for the combo portion of the score.
/// </summary>
public int MaxResultValueForScore => NumericResultForScore(MAX_HIT_RESULT);
/// <summary>
/// The maximum result value for the accuracy portion of the score.
/// </summary>
public int MaxResultValueForAccuracy => NumericResultForAccuracy(MAX_HIT_RESULT);
public override string ResultString => string.Empty;
public override string MaxResultString => string.Empty;
/// <summary>
/// The hit result.
/// </summary>
public ManiaHitResult ManiaResult;
public virtual int NumericResultForScore(ManiaHitResult result)
{
switch (result)
{
default:
return 0;
case ManiaHitResult.Bad:
return 50;
case ManiaHitResult.Ok:
return 100;
case ManiaHitResult.Good:
return 200;
case ManiaHitResult.Great:
case ManiaHitResult.Perfect:
return 300;
}
}
public virtual int NumericResultForAccuracy(ManiaHitResult result)
{
switch (result)
{
default:
return 0;
case ManiaHitResult.Bad:
return 50;
case ManiaHitResult.Ok:
return 100;
case ManiaHitResult.Good:
return 200;
case ManiaHitResult.Great:
return 300;
case ManiaHitResult.Perfect:
return 305;
}
}
}
}
+9 -1
View File
@@ -27,7 +27,14 @@ namespace osu.Game.Rulesets.Mania
{
new ManiaModEasy(),
new ManiaModNoFail(),
new ManiaModHalfTime(),
new MultiMod
{
Mods = new Mod[]
{
new ManiaModHalfTime(),
new ManiaModDaycore(),
},
},
};
case ModType.DifficultyIncrease:
@@ -89,6 +96,7 @@ namespace osu.Game.Rulesets.Mania
new ModCinema(),
},
},
new ManiaModGravity()
};
default:
@@ -87,6 +87,5 @@ namespace osu.Game.Rulesets.Mania.MathUtils
bitIndex++;
return ((bitBuffer >>= 1) & 1) == 1;
}
}
}
@@ -0,0 +1,23 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.Mods
{
/// <summary>
/// A type of mod which generates speed adjustments that scroll the hit objects and bar lines.
/// </summary>
internal interface IGenerateSpeedAdjustments
{
/// <summary>
/// Applies this mod to a hit renderer.
/// </summary>
/// <param name="hitRenderer">The hit renderer to apply to.</param>
/// <param name="hitObjectTimingChanges">The per-column list of speed adjustments for hit objects.</param>
/// <param name="barlineTimingChanges">The list of speed adjustments for bar lines.</param>
void ApplyToHitRenderer(ManiaHitRenderer hitRenderer, ref List<SpeedAdjustmentContainer>[] hitObjectTimingChanges, ref List<SpeedAdjustmentContainer> barlineTimingChanges);
}
}
+5
View File
@@ -34,6 +34,11 @@ namespace osu.Game.Rulesets.Mania.Mods
}
public class ManiaModDaycore : ModDaycore
{
public override double ScoreMultiplier => 0.3;
}
public class ManiaModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => 1.0;
@@ -0,0 +1,46 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Timing;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModGravity : Mod, IGenerateSpeedAdjustments
{
public override string Name => "Gravity";
public override double ScoreMultiplier => 0;
public override FontAwesome Icon => FontAwesome.fa_sort_desc;
public void ApplyToHitRenderer(ManiaHitRenderer hitRenderer, ref List<SpeedAdjustmentContainer>[] hitObjectTimingChanges, ref List<SpeedAdjustmentContainer> barlineTimingChanges)
{
// We have to generate one speed adjustment per hit object for gravity
foreach (ManiaHitObject obj in hitRenderer.Objects)
{
MultiplierControlPoint controlPoint = hitRenderer.CreateControlPointAt(obj.StartTime);
// Beat length has too large of an effect for gravity, so we'll force it to a constant value for now
controlPoint.TimingPoint.BeatLength = 1000;
hitObjectTimingChanges[obj.Column].Add(new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Gravity));
}
// Like with hit objects, we need to generate one speed adjustment per bar line
foreach (DrawableBarLine barLine in hitRenderer.BarLines)
{
var controlPoint = hitRenderer.CreateControlPointAt(barLine.HitObject.StartTime);
// Beat length has too large of an effect for gravity, so we'll force it to a constant value for now
controlPoint.TimingPoint.BeatLength = 1000;
barlineTimingChanges.Add(new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Gravity));
}
}
}
}
@@ -0,0 +1,21 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Mania.Objects
{
public class BarLine : ManiaHitObject
{
/// <summary>
/// The control point which this bar line is part of.
/// </summary>
public TimingControlPoint ControlPoint;
/// <summary>
/// The index of the beat which this bar line represents within the control point.
/// This is a "major" bar line if <see cref="BeatIndex"/> % <see cref="TimingControlPoint.TimeSignature"/> == 0.
/// </summary>
public int BeatIndex;
}
}
@@ -0,0 +1,74 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="BarLine"/>. Although this derives DrawableManiaHitObject,
/// this does not handle input/sound like a normal hit object.
/// </summary>
public class DrawableBarLine : DrawableManiaHitObject<BarLine>
{
/// <summary>
/// Height of major bar line triangles.
/// </summary>
private const float triangle_height = 12;
/// <summary>
/// Offset of the major bar line triangles from the sides of the bar line.
/// </summary>
private const float triangle_offset = 9;
public DrawableBarLine(BarLine barLine)
: base(barLine)
{
RelativeSizeAxes = Axes.X;
Height = 1;
Add(new Box
{
Name = "Bar line",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
});
bool isMajor = barLine.BeatIndex % (int)barLine.ControlPoint.TimeSignature == 0;
if (isMajor)
{
Add(new EquilateralTriangle
{
Name = "Left triangle",
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopCentre,
Size = new Vector2(triangle_height),
X = -triangle_offset,
Rotation = 90
});
Add(new EquilateralTriangle
{
Name = "Right triangle",
Anchor = Anchor.BottomRight,
Origin = Anchor.TopCentre,
Size = new Vector2(triangle_height),
X = triangle_offset,
Rotation = -90
});
}
if (!isMajor && barLine.BeatIndex % 2 == 1)
Alpha = 0.2f;
}
protected override void UpdateState(ArmedState state)
{
}
}
}
@@ -5,17 +5,39 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using OpenTK.Graphics;
using osu.Framework.Configuration;
using OpenTK.Input;
using osu.Framework.Input;
using OpenTK;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Framework.Extensions.IEnumerableExtensions;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="HoldNote"/> hit object.
/// </summary>
public class DrawableHoldNote : DrawableManiaHitObject<HoldNote>
{
private readonly NotePiece headPiece;
private readonly BodyPiece bodyPiece;
private readonly NotePiece tailPiece;
private readonly DrawableNote head;
private readonly DrawableNote tail;
public DrawableHoldNote(HoldNote hitObject)
: base(hitObject)
private readonly BodyPiece bodyPiece;
private readonly Container<DrawableHoldNoteTick> tickContainer;
/// <summary>
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
/// </summary>
private double? holdStartTime;
/// <summary>
/// Whether the hold note has been released too early and shouldn't give full score for the release.
/// </summary>
private bool hasBroken;
public DrawableHoldNote(HoldNote hitObject, Bindable<Key> key = null)
: base(hitObject, key)
{
RelativeSizeAxes = Axes.Both;
Height = (float)HitObject.Duration;
@@ -30,17 +52,37 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
headPiece = new NotePiece
tickContainer = new Container<DrawableHoldNoteTick>
{
RelativeSizeAxes = Axes.Both,
RelativeChildOffset = new Vector2(0, (float)HitObject.StartTime),
RelativeChildSize = new Vector2(1, (float)HitObject.Duration)
},
head = new DrawableHeadNote(this, key)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
tailPiece = new NotePiece
tail = new DrawableTailNote(this, key)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.TopCentre
}
});
foreach (var tick in HitObject.Ticks)
{
var drawableTick = new DrawableHoldNoteTick(tick)
{
HoldStartTime = () => holdStartTime
};
tickContainer.Add(drawableTick);
AddNested(drawableTick);
}
AddNested(head);
AddNested(tail);
}
public override Color4 AccentColour
@@ -52,9 +94,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
return;
base.AccentColour = value;
headPiece.AccentColour = value;
tickContainer.Children.ForEach(t => t.AccentColour = value);
bodyPiece.AccentColour = value;
tailPiece.AccentColour = value;
head.AccentColour = value;
tail.AccentColour = value;
}
}
@@ -62,14 +106,132 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
}
protected override void Update()
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (Time.Current > HitObject.StartTime)
headPiece.Colour = Color4.Green;
if (Time.Current > HitObject.EndTime)
// Make sure the keypress happened within the body of the hold note
if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime)
return false;
if (args.Key != Key)
return false;
if (args.Repeat)
return false;
// The user has pressed during the body of the hold note, after the head note and its hit windows have passed
// and within the limited range of the above if-statement. This state will be managed by the head note if the
// user has pressed during the hit windows of the head note.
holdStartTime = Time.Current;
return true;
}
protected override bool OnKeyUp(InputState state, KeyUpEventArgs args)
{
// Make sure that the user started holding the key during the hold note
if (!holdStartTime.HasValue)
return false;
if (args.Key != Key)
return false;
holdStartTime = null;
// If the key has been released too early, the user should not receive full score for the release
if (!tail.Judged)
hasBroken = true;
return true;
}
/// <summary>
/// The head note of a hold.
/// </summary>
private class DrawableHeadNote : DrawableNote
{
private readonly DrawableHoldNote holdNote;
public DrawableHeadNote(DrawableHoldNote holdNote, Bindable<Key> key = null)
: base(holdNote.HitObject.Head, key)
{
bodyPiece.Colour = Color4.Green;
tailPiece.Colour = Color4.Green;
this.holdNote = holdNote;
RelativePositionAxes = Axes.None;
Y = 0;
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (!base.OnKeyDown(state, args))
return false;
// We only want to trigger a holding state from the head if the head has received a judgement
if (!Judged)
return false;
// If the key has been released too early, the user should not receive full score for the release
if (Judgement.Result == HitResult.Miss)
holdNote.hasBroken = true;
// The head note also handles early hits before the body, but we want accurate early hits to count as the body being held
// The body doesn't handle these early early hits, so we have to explicitly set the holding state here
holdNote.holdStartTime = Time.Current;
return true;
}
}
/// <summary>
/// The tail note of a hold.
/// </summary>
private class DrawableTailNote : DrawableNote
{
private readonly DrawableHoldNote holdNote;
public DrawableTailNote(DrawableHoldNote holdNote, Bindable<Key> key = null)
: base(holdNote.HitObject.Tail, key)
{
this.holdNote = holdNote;
RelativePositionAxes = Axes.None;
Y = 0;
}
protected override ManiaJudgement CreateJudgement() => new HoldNoteTailJudgement();
protected override void CheckJudgement(bool userTriggered)
{
base.CheckJudgement(userTriggered);
var tailJudgement = Judgement as HoldNoteTailJudgement;
if (tailJudgement == null)
return;
tailJudgement.HasBroken = holdNote.hasBroken;
}
protected override bool OnKeyUp(InputState state, KeyUpEventArgs args)
{
// Make sure that the user started holding the key during the hold note
if (!holdNote.holdStartTime.HasValue)
return false;
if (Judgement.Result != HitResult.None)
return false;
if (args.Key != Key)
return false;
UpdateJudgement(true);
// Handled by the hold note, which will set holding = false
return false;
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
// Tail doesn't handle key down
return false;
}
}
}
@@ -0,0 +1,121 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="HoldNoteTick"/> hit object.
/// </summary>
public class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
{
/// <summary>
/// References the time at which the user started holding the hold note.
/// </summary>
public Func<double?> HoldStartTime;
/// <summary>
/// References whether the user is currently holding the hold note.
/// </summary>
public Func<bool> IsHolding;
private readonly Container glowContainer;
public DrawableHoldNoteTick(HoldNoteTick hitObject)
: base(hitObject)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
RelativeSizeAxes = Axes.X;
Size = new Vector2(1);
Children = new[]
{
glowContainer = new CircularContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
};
// Set the default glow
AccentColour = Color4.White;
}
public override Color4 AccentColour
{
get { return base.AccentColour; }
set
{
base.AccentColour = value;
glowContainer.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 2f,
Roundness = 15f,
Colour = value.Opacity(0.3f)
};
}
}
protected override ManiaJudgement CreateJudgement() => new HoldNoteTickJudgement();
protected override void CheckJudgement(bool userTriggered)
{
if (!userTriggered)
return;
if (Time.Current < HitObject.StartTime)
return;
if (HoldStartTime?.Invoke() > HitObject.StartTime)
return;
Judgement.ManiaResult = ManiaHitResult.Perfect;
Judgement.Result = HitResult.Hit;
}
protected override void UpdateState(ArmedState state)
{
switch (State)
{
case ArmedState.Hit:
AccentColour = Color4.Green;
break;
}
}
protected override void Update()
{
if (Judgement.Result != HitResult.None)
return;
if (IsHolding?.Invoke() != true)
return;
UpdateJudgement(true);
}
}
}
@@ -2,8 +2,11 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using OpenTK.Input;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
@@ -11,17 +14,32 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public abstract class DrawableManiaHitObject<TObject> : DrawableHitObject<ManiaHitObject, ManiaJudgement>
where TObject : ManiaHitObject
{
/// <summary>
/// The key that will trigger input for this hit object.
/// </summary>
protected Bindable<Key> Key { get; private set; } = new Bindable<Key>();
public new TObject HitObject;
protected DrawableManiaHitObject(TObject hitObject)
protected DrawableManiaHitObject(TObject hitObject, Bindable<Key> key = null)
: base(hitObject)
{
HitObject = hitObject;
if (key != null)
Key.BindTo(key);
RelativePositionAxes = Axes.Y;
Y = (float)HitObject.StartTime;
}
protected override void LoadComplete()
{
base.LoadComplete();
LifetimeStart = HitObject.StartTime - ManiaPlayfield.TIME_SPAN_MAX;
}
public override Color4 AccentColour
{
get { return base.AccentColour; }
@@ -1,19 +1,27 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using OpenTK.Graphics;
using OpenTK.Input;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="Note"/> hit object.
/// </summary>
public class DrawableNote : DrawableManiaHitObject<Note>
{
private readonly NotePiece headPiece;
public DrawableNote(Note hitObject)
: base(hitObject)
public DrawableNote(Note hitObject, Bindable<Key> key = null)
: base(hitObject, key)
{
RelativeSizeAxes = Axes.Both;
Height = 100;
@@ -38,14 +46,53 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
}
}
protected override void Update()
protected override void CheckJudgement(bool userTriggered)
{
if (Time.Current > HitObject.StartTime)
Colour = Color4.Green;
if (!userTriggered)
{
if (Judgement.TimeOffset > HitObject.HitWindows.Bad / 2)
Judgement.Result = HitResult.Miss;
return;
}
double offset = Math.Abs(Judgement.TimeOffset);
if (offset > HitObject.HitWindows.Miss / 2)
return;
ManiaHitResult? tmpResult = HitObject.HitWindows.ResultFor(offset);
if (tmpResult.HasValue)
{
Judgement.Result = HitResult.Hit;
Judgement.ManiaResult = tmpResult.Value;
}
else
Judgement.Result = HitResult.Miss;
}
protected override void UpdateState(ArmedState state)
{
switch (State)
{
case ArmedState.Hit:
Colour = Color4.Green;
break;
}
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (Judgement.Result != HitResult.None)
return false;
if (args.Key != Key)
return false;
if (args.Repeat)
return false;
return UpdateJudgement(true);
}
}
}
+91 -19
View File
@@ -1,9 +1,9 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps.Timing;
using System.Collections.Generic;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Database;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Mania.Objects
@@ -11,27 +11,99 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary>
/// Represents a hit object which requires pressing, holding, and releasing a key.
/// </summary>
public class HoldNote : Note, IHasEndTime
public class HoldNote : ManiaHitObject, IHasEndTime
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// </summary>
private const double release_window_lenience = 1.5;
public double Duration { get; set; }
public double EndTime => StartTime + Duration;
/// <summary>
/// The key-release hit windows for this hold note.
/// </summary>
protected HitWindows ReleaseHitWindows = new HitWindows();
public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty)
private double duration;
public double Duration
{
base.ApplyDefaults(timing, difficulty);
get { return duration; }
set
{
duration = value;
Tail.StartTime = EndTime;
}
}
ReleaseHitWindows = HitWindows * release_window_lenience;
public override double StartTime
{
get { return base.StartTime; }
set
{
base.StartTime = value;
Head.StartTime = value;
Tail.StartTime = EndTime;
}
}
/// <summary>
/// The head note of the hold.
/// </summary>
public readonly Note Head = new Note();
/// <summary>
/// The tail note of the hold.
/// </summary>
public readonly Note Tail = new TailNote();
/// <summary>
/// The time between ticks of this hold.
/// </summary>
private double tickSpacing = 50;
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
Head.ApplyDefaults(controlPointInfo, difficulty);
Tail.ApplyDefaults(controlPointInfo, difficulty);
}
/// <summary>
/// The scoring scoring ticks of the hold note.
/// </summary>
public IEnumerable<HoldNoteTick> Ticks => ticks ?? (ticks = createTicks());
private List<HoldNoteTick> ticks;
private List<HoldNoteTick> createTicks()
{
var ret = new List<HoldNoteTick>();
if (tickSpacing == 0)
return ret;
for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing)
{
ret.Add(new HoldNoteTick
{
StartTime = t
});
}
return ret;
}
/// <summary>
/// The tail of the hold note.
/// </summary>
private class TailNote : Note
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// </summary>
private const double release_window_lenience = 1.5;
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(controlPointInfo, difficulty);
HitWindows *= release_window_lenience;
}
}
}
}
@@ -0,0 +1,12 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
/// A scoring tick of a hold note.
/// </summary>
public class HoldNoteTick : ManiaHitObject
{
}
}
@@ -9,11 +9,5 @@ namespace osu.Game.Rulesets.Mania.Objects
public abstract class ManiaHitObject : HitObject, IHasColumn
{
public int Column { get; set; }
/// <summary>
/// The number of other <see cref="ManiaHitObject"/> that start at
/// the same time as this hit object.
/// </summary>
public int Siblings { get; set; }
}
}
+4 -4
View File
@@ -1,7 +1,7 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps.Timing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Database;
using osu.Game.Rulesets.Mania.Judgements;
@@ -15,11 +15,11 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary>
/// The key-press hit window for this note.
/// </summary>
protected HitWindows HitWindows = new HitWindows();
public HitWindows HitWindows { get; protected set; } = new HitWindows();
public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty)
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(timing, difficulty);
base.ApplyDefaults(controlPointInfo, difficulty);
HitWindows = new HitWindows(difficulty.OverallDifficulty);
}
@@ -1,8 +1,13 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
@@ -10,6 +15,143 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor<ManiaHitObject, ManiaJudgement>
{
/// <summary>
/// The maximum score achievable.
/// Does _not_ include bonus score - for bonus score see <see cref="bonusScore"/>.
/// </summary>
private const int max_score = 1000000;
/// <summary>
/// The amount of the score attributed to combo.
/// </summary>
private const double combo_portion_max = max_score * 0.2;
/// <summary>
/// The amount of the score attributed to accuracy.
/// </summary>
private const double accuracy_portion_max = max_score * 0.8;
/// <summary>
/// The factor used to determine relevance of combos.
/// </summary>
private const double combo_base = 4;
/// <summary>
/// The combo value at which hit objects result in the max score possible.
/// </summary>
private const int combo_relevance_cap = 400;
/// <summary>
/// The hit HP multiplier at OD = 0.
/// </summary>
private const double hp_multiplier_min = 0.75;
/// <summary>
/// The hit HP multiplier at OD = 0.
/// </summary>
private const double hp_multiplier_mid = 0.85;
/// <summary>
/// The hit HP multiplier at OD = 0.
/// </summary>
private const double hp_multiplier_max = 1;
/// <summary>
/// The default BAD hit HP increase.
/// </summary>
private const double hp_increase_bad = 0.005;
/// <summary>
/// The default OK hit HP increase.
/// </summary>
private const double hp_increase_ok = 0.010;
/// <summary>
/// The default GOOD hit HP increase.
/// </summary>
private const double hp_increase_good = 0.035;
/// <summary>
/// The default tick hit HP increase.
/// </summary>
private const double hp_increase_tick = 0.040;
/// <summary>
/// The default GREAT hit HP increase.
/// </summary>
private const double hp_increase_great = 0.055;
/// <summary>
/// The default PERFECT hit HP increase.
/// </summary>
private const double hp_increase_perfect = 0.065;
/// <summary>
/// The MISS HP multiplier at OD = 0.
/// </summary>
private const double hp_multiplier_miss_min = 0.5;
/// <summary>
/// The MISS HP multiplier at OD = 5.
/// </summary>
private const double hp_multiplier_miss_mid = 0.75;
/// <summary>
/// The MISS HP multiplier at OD = 10.
/// </summary>
private const double hp_multiplier_miss_max = 1;
/// <summary>
/// The default MISS HP increase.
/// </summary>
private const double hp_increase_miss = -0.125;
/// <summary>
/// The MISS HP multiplier. This is multiplied to the miss hp increase.
/// </summary>
private double hpMissMultiplier = 1;
/// <summary>
/// The HIT HP multiplier. This is multiplied to hit hp increases.
/// </summary>
private double hpMultiplier = 1;
/// <summary>
/// The cumulative combo portion of the score.
/// </summary>
private double comboScore => combo_portion_max * comboPortion / maxComboPortion;
/// <summary>
/// The cumulative accuracy portion of the score.
/// </summary>
private double accuracyScore => accuracy_portion_max * Math.Pow(Accuracy, 4) * totalHits / maxTotalHits;
/// <summary>
/// The cumulative bonus score.
/// This is added on top of <see cref="max_score"/>, thus the total score can exceed <see cref="max_score"/>.
/// </summary>
private double bonusScore;
/// <summary>
/// The <see cref="comboPortion"/> achieved by a perfect playthrough.
/// </summary>
private double maxComboPortion;
/// <summary>
/// The portion of the score dedicated to combo.
/// </summary>
private double comboPortion;
/// <summary>
/// The <see cref="totalHits"/> achieved by a perfect playthrough.
/// </summary>
private int maxTotalHits;
/// <summary>
/// The total hits.
/// </summary>
private int totalHits;
public ManiaScoreProcessor()
{
}
@@ -19,8 +161,135 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
}
protected override void ComputeTargets(Beatmap<ManiaHitObject> beatmap)
{
BeatmapDifficulty difficulty = beatmap.BeatmapInfo.Difficulty;
hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max);
hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max);
while (true)
{
foreach (var obj in beatmap.HitObjects)
{
var holdNote = obj as HoldNote;
if (obj is Note)
{
AddJudgement(new ManiaJudgement
{
Result = HitResult.Hit,
ManiaResult = ManiaHitResult.Perfect
});
}
else if (holdNote != null)
{
// Head
AddJudgement(new ManiaJudgement
{
Result = HitResult.Hit,
ManiaResult = ManiaJudgement.MAX_HIT_RESULT
});
// Ticks
int tickCount = holdNote.Ticks.Count();
for (int i = 0; i < tickCount; i++)
{
AddJudgement(new HoldNoteTickJudgement
{
Result = HitResult.Hit,
ManiaResult = ManiaJudgement.MAX_HIT_RESULT,
});
}
AddJudgement(new HoldNoteTailJudgement
{
Result = HitResult.Hit,
ManiaResult = ManiaJudgement.MAX_HIT_RESULT
});
}
}
if (!HasFailed)
break;
hpMultiplier *= 1.01;
hpMissMultiplier *= 0.98;
Reset();
}
maxTotalHits = totalHits;
maxComboPortion = comboPortion;
}
protected override void OnNewJudgement(ManiaJudgement judgement)
{
bool isTick = judgement is HoldNoteTickJudgement;
if (!isTick)
totalHits++;
switch (judgement.Result)
{
case HitResult.Miss:
Health.Value += hpMissMultiplier * hp_increase_miss;
break;
case HitResult.Hit:
if (isTick)
{
Health.Value += hpMultiplier * hp_increase_tick;
bonusScore += judgement.ResultValueForScore;
}
else
{
switch (judgement.ManiaResult)
{
case ManiaHitResult.Bad:
Health.Value += hpMultiplier * hp_increase_bad;
break;
case ManiaHitResult.Ok:
Health.Value += hpMultiplier * hp_increase_ok;
break;
case ManiaHitResult.Good:
Health.Value += hpMultiplier * hp_increase_good;
break;
case ManiaHitResult.Great:
Health.Value += hpMultiplier * hp_increase_great;
break;
case ManiaHitResult.Perfect:
Health.Value += hpMultiplier * hp_increase_perfect;
break;
}
// A factor that is applied to make higher combos more relevant
double comboRelevance = Math.Min(Math.Max(0.5, Math.Log(Combo.Value, combo_base)), Math.Log(combo_relevance_cap, combo_base));
comboPortion += judgement.ResultValueForScore * comboRelevance;
}
break;
}
int scoreForAccuracy = 0;
int maxScoreForAccuracy = 0;
foreach (var j in Judgements)
{
scoreForAccuracy += j.ResultValueForAccuracy;
maxScoreForAccuracy += j.MaxResultValueForAccuracy;
}
Accuracy.Value = (double)scoreForAccuracy / maxScoreForAccuracy;
TotalScore.Value = comboScore + accuracyScore + bonusScore;
}
protected override void Reset()
{
base.Reset();
Health.Value = 1;
bonusScore = 0;
comboPortion = 0;
totalHits = 0;
}
}
}
@@ -0,0 +1,27 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.Timing
{
/// <summary>
/// A <see cref="DrawableTimingSection"/> which scrolls relative to the control point start time.
/// </summary>
internal class BasicScrollingDrawableTimingSection : DrawableTimingSection
{
private readonly MultiplierControlPoint controlPoint;
public BasicScrollingDrawableTimingSection(MultiplierControlPoint controlPoint)
{
this.controlPoint = controlPoint;
}
protected override void Update()
{
base.Update();
Y = (float)(controlPoint.StartTime - Time.Current);
}
}
}
@@ -1,153 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using OpenTK;
using osu.Game.Beatmaps.Timing;
namespace osu.Game.Rulesets.Mania.Timing
{
/// <summary>
/// A container in which added drawables are put into a relative coordinate space spanned by a length of time.
/// <para>
/// This container contains <see cref="ControlPoint"/>s which scroll inside this container.
/// Drawables added to this container are moved inside the relevant <see cref="ControlPoint"/>,
/// and as such, will scroll along with the <see cref="ControlPoint"/>s.
/// </para>
/// </summary>
public class ControlPointContainer : Container<Drawable>
{
/// <summary>
/// The amount of time which this container spans.
/// </summary>
public double TimeSpan { get; set; }
private readonly List<DrawableControlPoint> drawableControlPoints;
public ControlPointContainer(IEnumerable<ControlPoint> timingChanges)
{
drawableControlPoints = timingChanges.Select(t => new DrawableControlPoint(t)).ToList();
Children = drawableControlPoints;
}
/// <summary>
/// Adds a drawable to this container. Note that the drawable added must have its Y-position be
/// an absolute unit of time that is _not_ relative to <see cref="TimeSpan"/>.
/// </summary>
/// <param name="drawable">The drawable to add.</param>
public override void Add(Drawable drawable)
{
// Always add timing sections to ourselves
if (drawable is DrawableControlPoint)
{
base.Add(drawable);
return;
}
var controlPoint = drawableControlPoints.LastOrDefault(t => t.CanContain(drawable)) ?? drawableControlPoints.FirstOrDefault();
if (controlPoint == null)
throw new Exception("Could not find suitable timing section to add object to.");
controlPoint.Add(drawable);
}
/// <summary>
/// A container that contains drawables within the time span of a timing section.
/// <para>
/// The content of this container will scroll relative to the current time.
/// </para>
/// </summary>
private class DrawableControlPoint : Container
{
private readonly ControlPoint timingChange;
protected override Container<Drawable> Content => content;
private readonly Container content;
/// <summary>
/// Creates a drawable control point. The height of this container will be proportional
/// to the beat length of the control point it is initialized with such that, e.g. a beat length
/// of 500ms results in this container being twice as high as its parent, which further means that
/// the content container will scroll at twice the normal rate.
/// </summary>
/// <param name="timingChange">The control point to create the drawable control point for.</param>
public DrawableControlPoint(ControlPoint timingChange)
{
this.timingChange = timingChange;
RelativeSizeAxes = Axes.Both;
AddInternal(content = new AutoTimeRelativeContainer
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
Y = (float)timingChange.Time
});
}
protected override void Update()
{
var parent = (ControlPointContainer)Parent;
// Adjust our height to account for the speed changes
Height = (float)(1000 / timingChange.BeatLength / timingChange.SpeedMultiplier);
RelativeCoordinateSpace = new Vector2(1, (float)parent.TimeSpan);
// Scroll the content
content.Y = (float)(timingChange.Time - Time.Current);
}
public override void Add(Drawable drawable)
{
// The previously relatively-positioned drawable will now become relative to content, but since the drawable has no knowledge of content,
// we need to offset it back by content's position position so that it becomes correctly relatively-positioned to content
// This can be removed if hit objects were stored such that either their StartTime or their "beat offset" was relative to the timing change
// they belonged to, but this requires a radical change to the beatmap format which we're not ready to do just yet
drawable.Y -= (float)timingChange.Time;
base.Add(drawable);
}
/// <summary>
/// Whether this control point can contain a drawable. This control point can contain a drawable if the drawable is positioned "after" this control point.
/// </summary>
/// <param name="drawable">The drawable to check.</param>
public bool CanContain(Drawable drawable) => content.Y <= drawable.Y;
/// <summary>
/// A container which always keeps its height and relative coordinate space "auto-sized" to its children.
/// <para>
/// This is used in the case where children are relatively positioned/sized to time values (e.g. notes/bar lines) to keep
/// such children wrapped inside a container, otherwise they would disappear due to container flattening.
/// </para>
/// </summary>
private class AutoTimeRelativeContainer : Container
{
public override void InvalidateFromChild(Invalidation invalidation)
{
// We only want to re-compute our size when a child's size or position has changed
if ((invalidation & Invalidation.Geometry) == 0)
{
base.InvalidateFromChild(invalidation);
return;
}
if (!Children.Any())
return;
float height = Children.Select(child => child.Y + child.Height).Max();
Height = height;
RelativeCoordinateSpace = new Vector2(1, height);
base.InvalidateFromChild(invalidation);
}
}
}
}
}
@@ -0,0 +1,60 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.Timing
{
/// <summary>
/// A <see cref="DrawableTimingSection"/> that emulates a form of gravity where hit objects speed up over time.
/// </summary>
internal class GravityScrollingDrawableTimingSection : DrawableTimingSection
{
private readonly MultiplierControlPoint controlPoint;
public GravityScrollingDrawableTimingSection(MultiplierControlPoint controlPoint)
{
this.controlPoint = controlPoint;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
// The gravity-adjusted start position
float startPos = (float)computeGravityTime(controlPoint.StartTime);
// The gravity-adjusted end position
float endPos = (float)computeGravityTime(controlPoint.StartTime + RelativeChildSize.Y);
Y = startPos;
Height = endPos - startPos;
}
/// <summary>
/// Applies gravity to a time value based on the current time.
/// </summary>
/// <param name="time">The time value gravity should be applied to.</param>
/// <returns>The time after gravity is applied to <paramref name="time"/>.</returns>
private double computeGravityTime(double time)
{
double relativeTime = relativeTimeAt(time);
// The sign of the relative time, this is used to apply backwards acceleration leading into startTime
double sign = relativeTime < 0 ? -1 : 1;
return VisibleTimeRange - acceleration * relativeTime * relativeTime * sign;
}
/// <summary>
/// The acceleration due to "gravity" of the content of this container.
/// </summary>
private double acceleration => 1 / VisibleTimeRange;
/// <summary>
/// Computes the current time relative to <paramref name="time"/>, accounting for <see cref="DrawableTimingSection.VisibleTimeRange"/>.
/// </summary>
/// <param name="time">The non-offset time.</param>
/// <returns>The current time relative to <paramref name="time"/> - <see cref="DrawableTimingSection.VisibleTimeRange"/>. </returns>
private double relativeTimeAt(double time) => Time.Current - time + VisibleTimeRange;
}
}
@@ -0,0 +1,30 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.Timing
{
public class ManiaSpeedAdjustmentContainer : SpeedAdjustmentContainer
{
private readonly ScrollingAlgorithm scrollingAlgorithm;
public ManiaSpeedAdjustmentContainer(MultiplierControlPoint timingSection, ScrollingAlgorithm scrollingAlgorithm)
: base(timingSection)
{
this.scrollingAlgorithm = scrollingAlgorithm;
}
protected override DrawableTimingSection CreateTimingSection()
{
switch (scrollingAlgorithm)
{
default:
case ScrollingAlgorithm.Basic:
return new BasicScrollingDrawableTimingSection(ControlPoint);
case ScrollingAlgorithm.Gravity:
return new GravityScrollingDrawableTimingSection(ControlPoint);
}
}
}
}
@@ -0,0 +1,17 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Mania.Timing
{
public enum ScrollingAlgorithm
{
/// <summary>
/// Basic scrolling algorithm based on the timing section time. This is the default algorithm.
/// </summary>
Basic,
/// <summary>
/// Emulating a form of gravity where hit objects speed up over time.
/// </summary>
Gravity
}
}
+52 -17
View File
@@ -1,7 +1,6 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Input;
@@ -12,12 +11,10 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Colour;
using osu.Framework.Input;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Timing;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Beatmaps.Timing;
using System;
using osu.Framework.Configuration;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -33,15 +30,25 @@ namespace osu.Game.Rulesets.Mania.UI
private const float column_width = 45;
private const float special_column_width = 70;
public Key Key;
private readonly BindableDouble visibleTimeRange = new BindableDouble();
public BindableDouble VisibleTimeRange
{
get { return visibleTimeRange; }
set { visibleTimeRange.BindTo(value); }
}
/// <summary>
/// The key that will trigger input actions for this column and hit objects contained inside it.
/// </summary>
public Bindable<Key> Key = new Bindable<Key>();
private readonly Box background;
private readonly Container hitTargetBar;
private readonly Container keyIcon;
public readonly ControlPointContainer ControlPointContainer;
private readonly SpeedAdjustmentCollection speedAdjustments;
public Column(IEnumerable<ControlPoint> timingChanges)
public Column()
{
RelativeSizeAxes = Axes.Y;
Width = column_width;
@@ -58,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
Name = "Hit target + hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = ManiaPlayfield.HIT_TARGET_POSITION},
Padding = new MarginPadding { Top = ManiaPlayfield.HIT_TARGET_POSITION },
Children = new Drawable[]
{
new Container
@@ -90,11 +97,18 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
},
ControlPointContainer = new ControlPointContainer(timingChanges)
speedAdjustments = new SpeedAdjustmentCollection(Axes.Y)
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
VisibleTimeRange = VisibleTimeRange
},
// For column lighting, we need to capture input events before the notes
new InputTarget
{
KeyDown = onKeyDown,
KeyUp = onKeyUp
}
}
},
new Container
@@ -162,14 +176,14 @@ namespace osu.Game.Rulesets.Mania.UI
background.Colour = accentColour;
hitTargetBar.EdgeEffect = new EdgeEffect
hitTargetBar.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 5,
Colour = accentColour.Opacity(0.5f),
};
keyIcon.EdgeEffect = new EdgeEffect
keyIcon.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 5,
@@ -178,12 +192,14 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
public void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> hitObject)
public void Add(SpeedAdjustmentContainer speedAdjustment) => speedAdjustments.Add(speedAdjustment);
public void Add(DrawableHitObject hitObject)
{
ControlPointContainer.Add(hitObject);
hitObject.AccentColour = AccentColour;
speedAdjustments.Add(hitObject);
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
private bool onKeyDown(InputState state, KeyDownEventArgs args)
{
if (args.Repeat)
return false;
@@ -197,7 +213,7 @@ namespace osu.Game.Rulesets.Mania.UI
return false;
}
protected override bool OnKeyUp(InputState state, KeyUpEventArgs args)
private bool onKeyUp(InputState state, KeyUpEventArgs args)
{
if (args.Key == Key)
{
@@ -207,5 +223,24 @@ namespace osu.Game.Rulesets.Mania.UI
return false;
}
/// <summary>
/// This is a simple container which delegates various input events that have to be captured before the notes.
/// </summary>
private class InputTarget : Container
{
public Func<InputState, KeyDownEventArgs, bool> KeyDown;
public Func<InputState, KeyUpEventArgs, bool> KeyUp;
public InputTarget()
{
RelativeSizeAxes = Axes.Both;
AlwaysPresent = true;
Alpha = 0;
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => KeyDown?.Invoke(state, args) ?? false;
protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => KeyUp?.Invoke(state, args) ?? false;
}
}
}
+121 -41
View File
@@ -2,87 +2,167 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using OpenTK;
using OpenTK.Input;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Lists;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mania.Timing;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaHitRenderer : HitRenderer<ManiaHitObject, ManiaJudgement>
public class ManiaHitRenderer : SpeedAdjustedHitRenderer<ManiaHitObject, ManiaJudgement>
{
public int? Columns;
/// <summary>
/// Preferred column count. This will only have an effect during the initialization of the play field.
/// </summary>
public int PreferredColumns;
public IEnumerable<DrawableBarLine> BarLines;
/// <summary>
/// Per-column timing changes.
/// </summary>
private readonly List<SpeedAdjustmentContainer>[] hitObjectSpeedAdjustments;
/// <summary>
/// Bar line timing changes.
/// </summary>
private readonly List<SpeedAdjustmentContainer> barLineSpeedAdjustments = new List<SpeedAdjustmentContainer>();
public ManiaHitRenderer(WorkingBeatmap beatmap, bool isForCurrentRuleset)
: base(beatmap, isForCurrentRuleset)
{
}
protected override Playfield<ManiaHitObject, ManiaJudgement> CreatePlayfield()
{
ControlPoint firstTimingChange = Beatmap.TimingInfo.ControlPoints.FirstOrDefault(t => t.TimingChange);
if (firstTimingChange == null)
throw new Exception("The Beatmap contains no timing points!");
// Generate the timing points, making non-timing changes use the previous timing change
var timingChanges = Beatmap.TimingInfo.ControlPoints.Select(c =>
{
ControlPoint t = c.Clone();
if (c.TimingChange)
firstTimingChange = c;
else
t.BeatLength = firstTimingChange.BeatLength;
return t;
});
// Generate the speed adjustment container lists
hitObjectSpeedAdjustments = new List<SpeedAdjustmentContainer>[PreferredColumns];
for (int i = 0; i < PreferredColumns; i++)
hitObjectSpeedAdjustments[i] = new List<SpeedAdjustmentContainer>();
// Generate the bar lines
double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
// Perform some post processing of the timing changes
timingChanges = timingChanges
// Collapse sections after the last hit object
.Where(s => s.Time <= lastObjectTime)
// Collapse sections with the same start time
.GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time)
// Collapse sections with the same beat length
.GroupBy(s => s.BeatLength * s.SpeedMultiplier).Select(g => g.First())
.ToList();
SortedList<TimingControlPoint> timingPoints = Beatmap.ControlPointInfo.TimingPoints;
var barLines = new List<DrawableBarLine>();
return new ManiaPlayfield(Columns ?? (int)Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize), timingChanges)
for (int i = 0; i < timingPoints.Count; i++)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
// Invert by default for now (should be moved to config/skin later)
Scale = new Vector2(1, -1)
};
TimingControlPoint point = timingPoints[i];
// Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - point.BeatLength : lastObjectTime + point.BeatLength * (int)point.TimeSignature;
int index = 0;
for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++)
{
barLines.Add(new DrawableBarLine(new BarLine
{
StartTime = t,
ControlPoint = point,
BeatIndex = index
}));
}
}
BarLines = barLines;
// Generate speed adjustments from mods first
bool useDefaultSpeedAdjustments = true;
if (Mods != null)
{
foreach (var speedAdjustmentMod in Mods.OfType<IGenerateSpeedAdjustments>())
{
useDefaultSpeedAdjustments = false;
speedAdjustmentMod.ApplyToHitRenderer(this, ref hitObjectSpeedAdjustments, ref barLineSpeedAdjustments);
}
}
// Generate the default speed adjustments
if (useDefaultSpeedAdjustments)
generateDefaultSpeedAdjustments();
}
[BackgroundDependencyLoader]
private void load()
{
var maniaPlayfield = (ManiaPlayfield)Playfield;
BarLines.ForEach(maniaPlayfield.Add);
}
protected override void ApplyBeatmap()
{
base.ApplyBeatmap();
PreferredColumns = (int)Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize);
}
protected override void ApplySpeedAdjustments()
{
var maniaPlayfield = (ManiaPlayfield)Playfield;
for (int i = 0; i < PreferredColumns; i++)
foreach (var change in hitObjectSpeedAdjustments[i])
maniaPlayfield.Columns.ElementAt(i).Add(change);
foreach (var change in barLineSpeedAdjustments)
maniaPlayfield.Add(change);
}
private void generateDefaultSpeedAdjustments()
{
DefaultControlPoints.ForEach(c =>
{
foreach (List<SpeedAdjustmentContainer> t in hitObjectSpeedAdjustments)
t.Add(new ManiaSpeedAdjustmentContainer(c, ScrollingAlgorithm.Basic));
barLineSpeedAdjustments.Add(new ManiaSpeedAdjustmentContainer(c, ScrollingAlgorithm.Basic));
});
}
protected sealed override Playfield<ManiaHitObject, ManiaJudgement> CreatePlayfield() => new ManiaPlayfield(PreferredColumns)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
// Invert by default for now (should be moved to config/skin later)
Scale = new Vector2(1, -1)
};
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter() => new ManiaBeatmapConverter();
protected override DrawableHitObject<ManiaHitObject, ManiaJudgement> GetVisualRepresentation(ManiaHitObject h)
{
var maniaPlayfield = (ManiaPlayfield)Playfield;
Bindable<Key> key = maniaPlayfield.Columns.ElementAt(h.Column).Key;
var holdNote = h as HoldNote;
if (holdNote != null)
return new DrawableHoldNote(holdNote);
return new DrawableHoldNote(holdNote, key);
var note = h as Note;
if (note != null)
return new DrawableNote(note);
return new DrawableNote(note, key);
return null;
}
+66 -55
View File
@@ -15,13 +15,13 @@ using osu.Framework.Allocation;
using OpenTK.Input;
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Mania.Timing;
using osu.Framework.Input;
using osu.Game.Beatmaps.Timing;
using osu.Framework.Graphics.Transforms;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Timing;
using osu.Framework.Configuration;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -29,10 +29,10 @@ namespace osu.Game.Rulesets.Mania.UI
{
public const float HIT_TARGET_POSITION = 50;
private const float time_span_default = 5000;
private const float time_span_min = 10;
private const float time_span_max = 50000;
private const float time_span_step = 200;
private const double time_span_default = 1500;
public const double TIME_SPAN_MIN = 50;
public const double TIME_SPAN_MAX = 10000;
private const double time_span_step = 50;
/// <summary>
/// Default column keys, expanding outwards from the middle as more column are added.
@@ -55,16 +55,23 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
public readonly FlowContainer<Column> Columns;
private readonly FlowContainer<Column> columns;
public IEnumerable<Column> Columns => columns.Children;
private readonly ControlPointContainer barlineContainer;
private readonly BindableDouble visibleTimeRange = new BindableDouble(time_span_default)
{
MinValue = TIME_SPAN_MIN,
MaxValue = TIME_SPAN_MAX
};
private readonly SpeedAdjustmentCollection barLineContainer;
private List<Color4> normalColumnColours = new List<Color4>();
private Color4 specialColumnColour;
private readonly int columnCount;
public ManiaPlayfield(int columnCount, IEnumerable<ControlPoint> timingChanges)
public ManiaPlayfield(int columnCount)
{
this.columnCount = columnCount;
@@ -77,35 +84,52 @@ namespace osu.Game.Rulesets.Mania.UI
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
},
Columns = new FillFlowContainer<Column>
{
Name = "Columns",
Name = "Masked elements",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Left = 1, Right = 1 },
Spacing = new Vector2(1, 0)
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
},
columns = new FillFlowContainer<Column>
{
Name = "Columns",
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Left = 1, Right = 1 },
Spacing = new Vector2(1, 0)
}
}
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = HIT_TARGET_POSITION },
Children = new[]
{
barlineContainer = new ControlPointContainer(timingChanges)
barLineContainer = new SpeedAdjustmentCollection(Axes.Y)
{
Name = "Bar lines",
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
VisibleTimeRange = visibleTimeRange
// Width is set in the Update method
}
}
}
@@ -114,9 +138,7 @@ namespace osu.Game.Rulesets.Mania.UI
};
for (int i = 0; i < columnCount; i++)
Columns.Add(new Column(timingChanges));
TimeSpan = time_span_default;
columns.Add(new Column { VisibleTimeRange = visibleTimeRange });
}
[BackgroundDependencyLoader]
@@ -133,17 +155,17 @@ namespace osu.Game.Rulesets.Mania.UI
// Set the special column + colour + key
for (int i = 0; i < columnCount; i++)
{
Column column = Columns.Children.ElementAt(i);
Column column = Columns.ElementAt(i);
column.IsSpecial = isSpecialColumn(i);
if (!column.IsSpecial)
continue;
column.Key = Key.Space;
column.Key.Value = Key.Space;
column.AccentColour = specialColumnColour;
}
var nonSpecialColumns = Columns.Children.Where(c => !c.IsSpecial).ToList();
var nonSpecialColumns = Columns.Where(c => !c.IsSpecial).ToList();
// We'll set the colours of the non-special columns in a separate loop, because the non-special
// column colours are mirrored across their centre and special styles mess with this
@@ -162,11 +184,11 @@ namespace osu.Game.Rulesets.Mania.UI
int keyOffset = default_keys.Length / 2 - nonSpecialColumns.Count / 2 + i;
if (keyOffset >= 0 && keyOffset < default_keys.Length)
column.Key = default_keys[keyOffset];
column.Key.Value = default_keys[keyOffset];
else
// There is no default key defined for this column. Let's set this to Unknown for now
// however note that this will be gone after bindings are in place
column.Key = Key.Unknown;
column.Key.Value = Key.Unknown;
}
}
@@ -189,7 +211,9 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
public override void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> h) => Columns.Children.ElementAt(h.HitObject.Column).Add(h);
public override void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> h) => Columns.ElementAt(h.HitObject.Column).Add(h);
public void Add(DrawableBarLine barline) => barLineContainer.Add(barline);
public void Add(SpeedAdjustmentContainer speedAdjustment) => barLineContainer.Add(speedAdjustment);
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
@@ -198,10 +222,10 @@ namespace osu.Game.Rulesets.Mania.UI
switch (args.Key)
{
case Key.Minus:
transformTimeSpanTo(TimeSpan + time_span_step, 200, EasingTypes.OutQuint);
transformVisibleTimeRangeTo(visibleTimeRange + time_span_step, 200, EasingTypes.OutQuint);
break;
case Key.Plus:
transformTimeSpanTo(TimeSpan - time_span_step, 200, EasingTypes.OutQuint);
transformVisibleTimeRangeTo(visibleTimeRange - time_span_step, 200, EasingTypes.OutQuint);
break;
}
}
@@ -209,29 +233,16 @@ namespace osu.Game.Rulesets.Mania.UI
return false;
}
private double timeSpan;
/// <summary>
/// The amount of time which the length of the playfield spans.
/// </summary>
public double TimeSpan
private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, EasingTypes easing = EasingTypes.None)
{
get { return timeSpan; }
set
{
if (timeSpan == value)
return;
timeSpan = value;
timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max);
barlineContainer.TimeSpan = value;
Columns.Children.ForEach(c => c.ControlPointContainer.TimeSpan = value);
}
TransformTo(() => visibleTimeRange.Value, newTimeRange, duration, easing, new TransformTimeSpan());
}
private void transformTimeSpanTo(double newTimeSpan, double duration = 0, EasingTypes easing = EasingTypes.None)
protected override void Update()
{
TransformTo(() => TimeSpan, newTimeSpan, duration, easing, new TransformTimeSpan());
// Due to masking differences, it is not possible to get the width of the columns container automatically
// While masking on effectively only the Y-axis, so we need to set the width of the bar line container manually
barLineContainer.Width = columns.Width;
}
private class TransformTimeSpan : Transform<double>
@@ -253,7 +264,7 @@ namespace osu.Game.Rulesets.Mania.UI
base.Apply(d);
var p = (ManiaPlayfield)d;
p.TimeSpan = CurrentValue;
p.visibleTimeRange.Value = (float)CurrentValue;
}
}
}
@@ -33,8 +33,9 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Reference Include="OpenTK, Version=2.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll</HintPath>
<Reference Include="OpenTK, Version=3.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
@@ -47,33 +48,48 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Beatmaps\Patterns\Legacy\EndTimeObjectPatternGenerator.cs" />
<Compile Include="Beatmaps\Patterns\Legacy\DistanceObjectPatternGenerator.cs" />
<Compile Include="Beatmaps\Patterns\Legacy\PatternGenerator.cs" />
<Compile Include="Beatmaps\Patterns\PatternGenerator.cs" />
<Compile Include="Beatmaps\Patterns\Legacy\HitObjectPatternGenerator.cs" />
<Compile Include="Beatmaps\Patterns\Legacy\PatternType.cs" />
<Compile Include="Beatmaps\ManiaBeatmapConverter.cs" />
<Compile Include="Beatmaps\Patterns\Pattern.cs" />
<Compile Include="MathUtils\FastRandom.cs" />
<Compile Include="Judgements\HitWindows.cs" />
<Compile Include="Judgements\HoldNoteTailJudgement.cs" />
<Compile Include="Judgements\HoldNoteTickJudgement.cs" />
<Compile Include="Judgements\ManiaHitResult.cs" />
<Compile Include="Judgements\ManiaJudgement.cs" />
<Compile Include="ManiaDifficultyCalculator.cs" />
<Compile Include="Mods\IGenerateSpeedAdjustments.cs" />
<Compile Include="Objects\Drawables\DrawableBarLine.cs" />
<Compile Include="Objects\Drawables\DrawableHoldNote.cs" />
<Compile Include="Objects\Drawables\DrawableHoldNoteTick.cs" />
<Compile Include="Objects\Drawables\DrawableManiaHitObject.cs" />
<Compile Include="Objects\Drawables\DrawableNote.cs" />
<Compile Include="Objects\Drawables\Pieces\BodyPiece.cs" />
<Compile Include="Objects\Drawables\Pieces\NotePiece.cs" />
<Compile Include="Objects\Types\IHasColumn.cs" />
<Compile Include="Scoring\ManiaScoreProcessor.cs" />
<Compile Include="Objects\BarLine.cs" />
<Compile Include="Objects\HoldNote.cs" />
<Compile Include="Objects\HoldNoteTick.cs" />
<Compile Include="Objects\ManiaHitObject.cs" />
<Compile Include="Objects\Note.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Timing\BasicScrollingDrawableTimingSection.cs" />
<Compile Include="Timing\GravityScrollingDrawableTimingSection.cs" />
<Compile Include="Timing\ScrollingAlgorithm.cs" />
<Compile Include="UI\Column.cs" />
<Compile Include="UI\ManiaHitRenderer.cs" />
<Compile Include="UI\ManiaPlayfield.cs" />
<Compile Include="ManiaRuleset.cs" />
<Compile Include="Mods\ManiaMod.cs" />
<Compile Include="Mods\ManiaModGravity.cs" />
<Compile Include="UI\SpecialColumnPosition.cs" />
<Compile Include="Timing\ControlPointContainer.cs" />
<Compile Include="Timing\ManiaSpeedAdjustmentContainer.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu-framework\osu.Framework\osu.Framework.csproj">
+1 -2
View File
@@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-->
<packages>
<package id="ppy.OpenTK" version="2.0.50727.1341" targetFramework="net45" />
<package id="ppy.OpenTK" version="3.0" targetFramework="net45" />
</packages>
+5
View File
@@ -39,6 +39,11 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
}
public class OsuModDaycore : ModDaycore
{
public override double ScoreMultiplier => 0.5;
}
public class OsuModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => 1.12;
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
Masking = true;
AutoSizeAxes = Axes.Both;
CornerRadius = width / 2;
EdgeEffect = new EdgeEffect
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Color4.White.Opacity(0.2f),
@@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly TextAwesome symbol;
private readonly Color4 baseColour = OsuColour.FromHex(@"002c3c");
private readonly Color4 fillColour = OsuColour.FromHex(@"005b7c");
private Color4 normalColour;
private Color4 completeColour;
@@ -154,13 +157,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
normalColour = colours.SpinnerBase;
normalColour = baseColour;
background.AccentColour = normalColour;
completeColour = colours.YellowLight.Opacity(0.6f);
completeColour = colours.YellowLight.Opacity(0.75f);
disc.AccentColour = colours.SpinnerFill;
disc.AccentColour = fillColour;
circle.Colour = colours.BlueDark;
glow.Colour = colours.BlueDark;
}
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
Masking = true,
Origin = Anchor.Centre,
EdgeEffect = new EdgeEffect
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 60,
@@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -26,11 +25,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
Disc.Colour = value;
EdgeEffect = new EdgeEffect
EdgeEffect = new EdgeEffectParameters
{
Hollow = true,
Type = EdgeEffectType.Glow,
Radius = 14,
Colour = value.Opacity(0.3f),
Radius = 40,
Colour = value,
};
}
}
@@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
if (Complete && updateCompleteTick())
{
background.Flush(flushType: typeof(TransformAlpha));
background.FadeTo(tracking_alpha + 0.4f, 60, EasingTypes.OutExpo);
background.FadeTo(tracking_alpha + 0.2f, 60, EasingTypes.OutExpo);
background.Delay(60);
background.FadeTo(tracking_alpha, 250, EasingTypes.OutQuint);
}
@@ -2,12 +2,10 @@
// 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.Sprites;
using osu.Game.Graphics;
using OpenTK;
using OpenTK.Graphics;
@@ -15,24 +13,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class SpinnerTicks : Container
{
private Color4 glowColour;
public SpinnerTicks()
{
Origin = Anchor.Centre;
Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
glowColour = colours.BlueDarker.Opacity(0.4f);
layout();
}
private void layout()
{
const int count = 18;
for (int i = 0; i < count; i++)
@@ -41,11 +27,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
Colour = Color4.Black,
Alpha = 0.4f,
EdgeEffect = new EdgeEffect
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 20,
Colour = glowColour,
Radius = 10,
Colour = Color4.Gray.Opacity(0.2f),
},
RelativePositionAxes = Axes.Both,
Masking = true,
@@ -6,8 +6,8 @@ using OpenTK;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using OpenTK.Graphics;
using osu.Game.Beatmaps.Timing;
using osu.Game.Database;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Osu.Objects
{
@@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Osu.Objects
return OsuScoreResult.Miss;
}
public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty)
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(timing, difficulty);
base.ApplyDefaults(controlPointInfo, difficulty);
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}
@@ -1,201 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using System;
using System.Diagnostics;
using System.Linq;
namespace osu.Game.Rulesets.Osu.Objects
{
internal class OsuHitObjectDifficulty
{
/// <summary>
/// Factor by how much speed / aim strain decays per second.
/// </summary>
/// <remarks>
/// These values are results of tweaking a lot and taking into account general feedback.
/// Opinionated observation: Speed is easier to maintain than accurate jumps.
/// </remarks>
internal static readonly double[] DECAY_BASE = { 0.3, 0.15 };
/// <summary>
/// Pseudo threshold values to distinguish between "singles" and "streams"
/// </summary>
/// <remarks>
/// Of course the border can not be defined clearly, therefore the algorithm has a smooth transition between those values.
/// They also are based on tweaking and general feedback.
/// </remarks>
private const double stream_spacing_threshold = 110,
single_spacing_threshold = 125;
/// <summary>
/// Scaling values for weightings to keep aim and speed difficulty in balance.
/// </summary>
/// <remarks>
/// Found from testing a very large map pool (containing all ranked maps) and keeping the average values the same.
/// </remarks>
private static readonly double[] spacing_weight_scaling = { 1400, 26.25 };
/// <summary>
/// Almost the normed diameter of a circle (104 osu pixel). That is -after- position transforming.
/// </summary>
private const double almost_diameter = 90;
internal OsuHitObject BaseHitObject;
internal double[] Strains = { 1, 1 };
internal int MaxCombo = 1;
private readonly float scalingFactor;
private float lazySliderLength;
private readonly Vector2 startPosition;
private readonly Vector2 endPosition;
internal OsuHitObjectDifficulty(OsuHitObject baseHitObject)
{
BaseHitObject = baseHitObject;
float circleRadius = baseHitObject.Scale * 64;
Slider slider = BaseHitObject as Slider;
if (slider != null)
MaxCombo += slider.Ticks.Count();
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
scalingFactor = 52.0f / circleRadius;
if (circleRadius < 30)
{
float smallCircleBonus = Math.Min(30.0f - circleRadius, 5.0f) / 50.0f;
scalingFactor *= 1.0f + smallCircleBonus;
}
lazySliderLength = 0;
startPosition = baseHitObject.StackedPosition;
// Calculate approximation of lazy movement on the slider
if (slider != null)
{
float sliderFollowCircleRadius = circleRadius * 3; // Not sure if this is correct, but here we do not need 100% exact values. This comes pretty darn close in my tests.
// For simplifying this step we use actual osu! coordinates and simply scale the length, that we obtain by the ScalingFactor later
Vector2 cursorPos = startPosition;
Action<Vector2> addSliderVertex = delegate (Vector2 pos)
{
Vector2 difference = pos - cursorPos;
float distance = difference.Length;
// Did we move away too far?
if (distance > sliderFollowCircleRadius)
{
// Yep, we need to move the cursor
difference.Normalize(); // Obtain the direction of difference. We do no longer need the actual difference
distance -= sliderFollowCircleRadius;
cursorPos += difference * distance; // We move the cursor just as far as needed to stay in the follow circle
lazySliderLength += distance;
}
};
// Actual computation of the first lazy curve
foreach (var tick in slider.Ticks)
addSliderVertex(tick.StackedPosition);
addSliderVertex(baseHitObject.StackedEndPosition);
lazySliderLength *= scalingFactor;
endPosition = cursorPos;
}
// We have a normal HitCircle or a spinner
else
endPosition = startPosition;
}
internal void CalculateStrains(OsuHitObjectDifficulty previousHitObject, double timeRate)
{
calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Speed, timeRate);
calculateSpecificStrain(previousHitObject, OsuDifficultyCalculator.DifficultyType.Aim, timeRate);
}
// Caution: The subjective values are strong with this one
private static double spacingWeight(double distance, OsuDifficultyCalculator.DifficultyType type)
{
switch (type)
{
case OsuDifficultyCalculator.DifficultyType.Speed:
if (distance > single_spacing_threshold)
return 2.5;
else if (distance > stream_spacing_threshold)
return 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold);
else if (distance > almost_diameter)
return 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter);
else if (distance > almost_diameter / 2)
return 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2);
else
return 0.95;
case OsuDifficultyCalculator.DifficultyType.Aim:
return Math.Pow(distance, 0.99);
}
Debug.Assert(false, "Invalid osu difficulty hit object type.");
return 0;
}
private void calculateSpecificStrain(OsuHitObjectDifficulty previousHitObject, OsuDifficultyCalculator.DifficultyType type, double timeRate)
{
double addition = 0;
double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
double decay = Math.Pow(DECAY_BASE[(int)type], timeElapsed / 1000);
if (BaseHitObject is Spinner)
{
// Do nothing for spinners
}
else if (BaseHitObject is Slider)
{
switch (type)
{
case OsuDifficultyCalculator.DifficultyType.Speed:
// For speed strain we treat the whole slider as a single spacing entity, since "Speed" is about how hard it is to click buttons fast.
// The spacing weight exists to differentiate between being able to easily alternate or having to single.
addition =
spacingWeight(previousHitObject.lazySliderLength +
DistanceTo(previousHitObject), type) *
spacing_weight_scaling[(int)type];
break;
case OsuDifficultyCalculator.DifficultyType.Aim:
// For Aim strain we treat each slider segment and the jump after the end of the slider as separate jumps, since movement-wise there is no difference
// to multiple jumps.
addition =
(
spacingWeight(previousHitObject.lazySliderLength, type) +
spacingWeight(DistanceTo(previousHitObject), type)
) *
spacing_weight_scaling[(int)type];
break;
}
}
else if (BaseHitObject is HitCircle)
{
addition = spacingWeight(DistanceTo(previousHitObject), type) * spacing_weight_scaling[(int)type];
}
// Scale addition by the time, that elapsed. Filter out HitObjects that are too close to be played anyway to avoid crazy values by division through close to zero.
// You will never find maps that require this amongst ranked maps.
addition /= Math.Max(timeElapsed, 50);
Strains[(int)type] = previousHitObject.Strains[(int)type] * decay + addition;
}
internal double DistanceTo(OsuHitObjectDifficulty other)
{
// Scale the distance by circle size.
return (startPosition - other.endPosition).Length * scalingFactor;
}
}
}
+8 -5
View File
@@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects.Types;
using System;
using System.Collections.Generic;
@@ -10,6 +9,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Database;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Osu.Objects
{
@@ -62,13 +62,16 @@ namespace osu.Game.Rulesets.Osu.Objects
public double Velocity;
public double TickDistance;
public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty)
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(timing, difficulty);
base.ApplyDefaults(controlPointInfo, difficulty);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier / timing.SpeedMultiplierAt(StartTime);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
Velocity = scoringDistance / timing.BeatLengthAt(StartTime);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier / difficultyPoint.SpeedMultiplier;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
}
+6 -3
View File
@@ -2,8 +2,8 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Timing;
using osu.Game.Database;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Osu.Objects
{
@@ -19,11 +19,14 @@ namespace osu.Game.Rulesets.Osu.Objects
public override bool NewCombo => true;
public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty)
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(timing, difficulty);
base.ApplyDefaults(controlPointInfo, difficulty);
SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5));
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
SpinsRequired = (int)(SpinsRequired * 0.6);
}
}
}
@@ -0,0 +1,73 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Beatmaps;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.OsuDifficulty.Skills;
namespace osu.Game.Rulesets.Osu.OsuDifficulty
{
public class OsuDifficultyCalculator : DifficultyCalculator<OsuHitObject>
{
private const int section_length = 400;
private const double difficulty_multiplier = 0.0675;
public OsuDifficultyCalculator(Beatmap beatmap) : base(beatmap)
{
}
protected override void PreprocessHitObjects()
{
foreach (OsuHitObject h in Objects)
(h as Slider)?.Curve?.Calculate();
}
protected override double CalculateInternal(Dictionary<string, string> categoryDifficulty)
{
OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Objects);
Skill[] skills =
{
new Aim(),
new Speed()
};
double sectionEnd = section_length / TimeRate;
foreach (OsuDifficultyHitObject h in beatmap)
{
while (h.BaseObject.StartTime > sectionEnd)
{
foreach (Skill s in skills)
{
s.SaveCurrentPeak();
s.StartNewSectionFrom(sectionEnd);
}
sectionEnd += section_length;
}
foreach (Skill s in skills)
s.Process(h);
}
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2;
if (categoryDifficulty != null)
{
categoryDifficulty.Add("Aim", aimRating.ToString("0.00"));
categoryDifficulty.Add("Speed", speedRating.ToString("0.00"));
}
return starRating;
}
protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter() => new OsuBeatmapConverter();
}
}
@@ -0,0 +1,93 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
{
/// <summary>
/// An enumerable container wrapping <see cref="OsuHitObject"/> input as <see cref="OsuDifficultyHitObject"/>
/// which contains extra data required for difficulty calculation.
/// </summary>
public class OsuDifficultyBeatmap : IEnumerable<OsuDifficultyHitObject>
{
private readonly IEnumerator<OsuDifficultyHitObject> difficultyObjects;
private readonly Queue<OsuDifficultyHitObject> onScreen = new Queue<OsuDifficultyHitObject>();
/// <summary>
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
/// <see cref="OsuDifficultyHitObject"/> which contains extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyBeatmap(List<OsuHitObject> objects)
{
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
// This should probably happen before the objects reach the difficulty calculator.
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
difficultyObjects = createDifficultyObjectEnumerator(objects);
}
/// <summary>
/// Returns an enumerator that enumerates all <see cref="OsuDifficultyHitObject"/>s in the <see cref="OsuDifficultyBeatmap"/>.
/// The inner loop adds objects that appear on screen into a queue until we need to hit the next object.
/// The outer loop returns objects from this queue one at a time, only after they had to be hit, and should no longer be on screen.
/// This means that we can loop through every object that is on screen at the time when a new one appears,
/// allowing us to determine a reading strain for the object that just appeared.
/// </summary>
public IEnumerator<OsuDifficultyHitObject> GetEnumerator()
{
while (true)
{
// Add upcoming objects to the queue until we have at least one object that had been hit and can be dequeued.
// This means there is always at least one object in the queue unless we reached the end of the map.
do
{
if (!difficultyObjects.MoveNext())
break; // New objects can't be added anymore, but we still need to dequeue and return the ones already on screen.
OsuDifficultyHitObject latest = difficultyObjects.Current;
// Calculate flow values here
foreach (OsuDifficultyHitObject h in onScreen)
{
h.TimeUntilHit -= latest.DeltaTime;
// Calculate reading strain here
}
onScreen.Enqueue(latest);
}
while (onScreen.Peek().TimeUntilHit > 0); // Keep adding new objects on screen while there is still time before we have to hit the next one.
if (onScreen.Count == 0) break; // We have reached the end of the map and enumerated all the objects.
yield return onScreen.Dequeue(); // Remove and return objects one by one that had to be hit before the latest one appeared.
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects)
{
// We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object.
OsuHitObject[] triangle = new OsuHitObject[3];
// OsuDifficultyHitObject construction requires three components, an extra copy of the first OsuHitObject is used at the beginning.
if (objects.Count > 1)
{
triangle[1] = objects[0]; // This copy will get shifted to the last spot in the triangle.
triangle[0] = objects[0]; // This component corresponds to the real first OsuHitOject.
}
// The final component of the first triangle will be the second OsuHitOject of the map, which forms the first jump.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < objects.Count; ++i)
{
triangle[2] = triangle[1];
triangle[1] = triangle[0];
triangle[0] = objects[i];
yield return new OsuDifficultyHitObject(triangle);
}
}
}
}
@@ -0,0 +1,70 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
{
/// <summary>
/// A wrapper around <see cref="OsuHitObject"/> extending it with additional data required for difficulty calculation.
/// </summary>
public class OsuDifficultyHitObject
{
/// <summary>
/// The <see cref="OsuHitObject"/> this <see cref="OsuDifficultyHitObject"/> refers to.
/// </summary>
public OsuHitObject BaseObject { get; }
/// <summary>
/// Normalized distance from the <see cref="OsuHitObject.StackedPosition"/> of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double Distance { get; private set; }
/// <summary>
/// Milliseconds elapsed since the StartTime of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double DeltaTime { get; private set; }
/// <summary>
/// Number of milliseconds until the <see cref="OsuDifficultyHitObject"/> has to be hit.
/// </summary>
public double TimeUntilHit { get; set; }
private const int normalized_radius = 52;
private readonly OsuHitObject[] t;
/// <summary>
/// Initializes the object calculating extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyHitObject(OsuHitObject[] triangle)
{
t = triangle;
BaseObject = t[0];
setDistances();
setTimingValues();
// Calculate angle here
}
private void setDistances()
{
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
double scalingFactor = normalized_radius / BaseObject.Radius;
if (BaseObject.Radius < 30)
{
double smallCircleBonus = Math.Min(30 - BaseObject.Radius, 5) / 50;
scalingFactor *= 1 + smallCircleBonus;
}
Distance = (t[0].StackedPosition - t[1].StackedPosition).Length * scalingFactor;
}
private void setTimingValues()
{
// Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
DeltaTime = Math.Max(40, t[0].StartTime - t[1].StartTime);
TimeUntilHit = 450; // BaseObject.PreEmpt;
}
}
}
@@ -0,0 +1,19 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills
{
/// <summary>
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
/// </summary>
public class Aim : Skill
{
protected override double SkillMultiplier => 26.25;
protected override double StrainDecayBase => 0.15;
protected override double StrainValueOf(OsuDifficultyHitObject current) => Math.Pow(current.Distance, 0.99) / current.DeltaTime;
}
}
@@ -0,0 +1,100 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.OsuDifficulty.Utils;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills
{
/// <summary>
/// Used to processes strain values of <see cref="OsuDifficultyHitObject"/>s, keep track of strain levels caused by the processed objects
/// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
/// </summary>
public abstract class Skill
{
/// <summary>
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
/// </summary>
protected abstract double SkillMultiplier { get; }
/// <summary>
/// Determines how quickly strain decays for the given skill.
/// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
/// </summary>
protected abstract double StrainDecayBase { get; }
/// <summary>
/// <see cref="OsuDifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
/// </summary>
protected readonly History<OsuDifficultyHitObject> Previous = new History<OsuDifficultyHitObject>(2); // Contained objects not used yet
private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap.
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private readonly List<double> strainPeaks = new List<double>();
/// <summary>
/// Process an <see cref="OsuDifficultyHitObject"/> and update current strain values accordingly.
/// </summary>
public void Process(OsuDifficultyHitObject current)
{
currentStrain *= strainDecay(current.DeltaTime);
if (!(current.BaseObject is Spinner))
currentStrain += StrainValueOf(current) * SkillMultiplier;
currentSectionPeak = Math.Max(currentStrain, currentSectionPeak);
Previous.Push(current);
}
/// <summary>
/// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
/// </summary>
public void SaveCurrentPeak()
{
if (Previous.Count > 0)
strainPeaks.Add(currentSectionPeak);
}
/// <summary>
/// Sets the initial strain level for a new section.
/// </summary>
/// <param name="offset">The beginning of the new section in milliseconds</param>
public void StartNewSectionFrom(double offset)
{
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
if (Previous.Count > 0)
currentSectionPeak = currentStrain * strainDecay(offset - Previous[0].BaseObject.StartTime);
}
/// <summary>
/// Returns the calculated difficulty value representing all processed <see cref="OsuDifficultyHitObject"/>s.
/// </summary>
public double DifficultyValue()
{
strainPeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
foreach (double strain in strainPeaks)
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
/// <summary>
/// Calculates the strain value of an <see cref="OsuDifficultyHitObject"/>. This value is affected by previously processed objects.
/// </summary>
protected abstract double StrainValueOf(OsuDifficultyHitObject current);
private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
}
}
@@ -0,0 +1,39 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills
{
/// <summary>
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
/// </summary>
public class Speed : Skill
{
protected override double SkillMultiplier => 1400;
protected override double StrainDecayBase => 0.3;
private const double single_spacing_threshold = 125;
private const double stream_spacing_threshold = 110;
private const double almost_diameter = 90;
protected override double StrainValueOf(OsuDifficultyHitObject current)
{
double distance = current.Distance;
double speedValue;
if (distance > single_spacing_threshold)
speedValue = 2.5;
else if (distance > stream_spacing_threshold)
speedValue = 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold);
else if (distance > almost_diameter)
speedValue = 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter);
else if (distance > almost_diameter / 2)
speedValue = 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2);
else
speedValue = 0.95;
return speedValue / current.DeltaTime;
}
}
}
@@ -0,0 +1,86 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils
{
/// <summary>
/// An indexed stack with Push() only, which disposes items at the bottom after the capacity is full.
/// Indexing starts at the top of the stack.
/// </summary>
public class History<T> : IEnumerable<T>
{
public int Count { get; private set; }
private readonly T[] array;
private readonly int capacity;
private int marker; // Marks the position of the most recently added item.
/// <summary>
/// Initializes a new instance of the History class that is empty and has the specified capacity.
/// </summary>
/// <param name="capacity">The number of items the History can hold.</param>
public History(int capacity)
{
if (capacity < 0)
throw new ArgumentOutOfRangeException();
this.capacity = capacity;
array = new T[capacity];
marker = capacity; // Set marker to the end of the array, outside of the indexed range by one.
}
/// <summary>
/// The most recently added item is returned at index 0.
/// </summary>
public T this[int i]
{
get
{
if (i < 0 || i > Count - 1)
throw new IndexOutOfRangeException();
i += marker;
if (i > capacity - 1)
i -= capacity;
return array[i];
}
}
/// <summary>
/// Adds the item as the most recent one in the history.
/// The oldest item is disposed if the history is full.
/// </summary>
public void Push(T item) // Overwrite the oldest item instead of shifting every item by one with every addition.
{
if (marker == 0)
marker = capacity - 1;
else
--marker;
array[marker] = item;
if (Count < capacity)
++Count;
}
/// <summary>
/// Returns an enumerator which enumerates items in the history starting from the most recently added one.
/// </summary>
public IEnumerator<T> GetEnumerator()
{
for (int i = marker; i < capacity; ++i)
yield return array[i];
if (Count == capacity)
for (int i = 0; i < marker; ++i)
yield return array[i];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
@@ -1,192 +0,0 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Beatmaps;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using System;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Osu
{
public class OsuDifficultyCalculator : DifficultyCalculator<OsuHitObject>
{
private const double star_scaling_factor = 0.0675;
private const double extreme_scaling_factor = 0.5;
/// <summary>
/// HitObjects are stored as a member variable.
/// </summary>
internal List<OsuHitObjectDifficulty> DifficultyHitObjects = new List<OsuHitObjectDifficulty>();
public OsuDifficultyCalculator(Beatmap beatmap) : base(beatmap)
{
}
protected override void PreprocessHitObjects()
{
foreach (var h in Objects)
(h as Slider)?.Curve?.Calculate();
}
protected override double CalculateInternal(Dictionary<string, string> categoryDifficulty)
{
// Fill our custom DifficultyHitObject class, that carries additional information
DifficultyHitObjects.Clear();
foreach (var hitObject in Objects)
DifficultyHitObjects.Add(new OsuHitObjectDifficulty(hitObject));
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
DifficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
if (!CalculateStrainValues()) return 0;
double speedDifficulty = CalculateDifficulty(DifficultyType.Speed);
double aimDifficulty = CalculateDifficulty(DifficultyType.Aim);
// OverallDifficulty is not considered in this algorithm and neither is HpDrainRate. That means, that in this form the algorithm determines how hard it physically is
// to play the map, assuming, that too much of an error will not lead to a death.
// It might be desirable to include OverallDifficulty into map difficulty, but in my personal opinion it belongs more to the weighting of the actual peformance
// and is superfluous in the beatmap difficulty rating.
// If it were to be considered, then I would look at the hit window of normal HitCircles only, since Sliders and Spinners are (almost) "free" 300s and take map length
// into account as well.
// The difficulty can be scaled by any desired metric.
// In osu!tp it gets squared to account for the rapid increase in difficulty as the limit of a human is approached. (Of course it also gets scaled afterwards.)
// It would not be suitable for a star rating, therefore:
// The following is a proposal to forge a star rating from 0 to 5. It consists of taking the square root of the difficulty, since by simply scaling the easier
// 5-star maps would end up with one star.
double speedStars = Math.Sqrt(speedDifficulty) * star_scaling_factor;
double aimStars = Math.Sqrt(aimDifficulty) * star_scaling_factor;
if (categoryDifficulty != null)
{
categoryDifficulty.Add("Aim", aimStars.ToString("0.00"));
categoryDifficulty.Add("Speed", speedStars.ToString("0.00"));
double hitWindow300 = 30/*HitObjectManager.HitWindow300*/ / TimeRate;
double preEmpt = 450/*HitObjectManager.PreEmpt*/ / TimeRate;
categoryDifficulty.Add("OD", (-(hitWindow300 - 80.0) / 6.0).ToString("0.00"));
categoryDifficulty.Add("AR", (preEmpt > 1200.0 ? -(preEmpt - 1800.0) / 120.0 : -(preEmpt - 1200.0) / 150.0 + 5.0).ToString("0.00"));
int maxCombo = 0;
foreach (OsuHitObjectDifficulty hitObject in DifficultyHitObjects)
maxCombo += hitObject.MaxCombo;
categoryDifficulty.Add("Max combo", maxCombo.ToString());
}
// Again, from own observations and from the general opinion of the community a map with high speed and low aim (or vice versa) difficulty is harder,
// than a map with mediocre difficulty in both. Therefore we can not just add both difficulties together, but will introduce a scaling that favors extremes.
double starRating = speedStars + aimStars + Math.Abs(speedStars - aimStars) * extreme_scaling_factor;
// Another approach to this would be taking Speed and Aim separately to a chosen power, which again would be equivalent. This would be more convenient if
// the hit window size is to be considered as well.
// Note: The star rating is tuned extremely tight! Airman (/b/104229) and Freedom Dive (/b/126645), two of the hardest ranked maps, both score ~4.66 stars.
// Expect the easier kind of maps that officially get 5 stars to obtain around 2 by this metric. The tutorial still scores about half a star.
// Tune by yourself as you please. ;)
return starRating;
}
protected bool CalculateStrainValues()
{
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
using (List<OsuHitObjectDifficulty>.Enumerator hitObjectsEnumerator = DifficultyHitObjects.GetEnumerator())
{
if (!hitObjectsEnumerator.MoveNext()) return false;
OsuHitObjectDifficulty current = hitObjectsEnumerator.Current;
// First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject.
while (hitObjectsEnumerator.MoveNext())
{
var next = hitObjectsEnumerator.Current;
next?.CalculateStrains(current, TimeRate);
current = next;
}
return true;
}
}
/// <summary>
/// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size STRAIN_STEP.
/// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain.
/// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage.
/// </summary>
protected const double STRAIN_STEP = 400;
/// <summary>
/// The weighting of each strain value decays to this number * it's previous value
/// </summary>
protected const double DECAY_WEIGHT = 0.9;
protected double CalculateDifficulty(DifficultyType type)
{
double actualStrainStep = STRAIN_STEP * TimeRate;
// Find the highest strain value within each strain step
List<double> highestStrains = new List<double>();
double intervalEndTime = actualStrainStep;
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
OsuHitObjectDifficulty previousHitObject = null;
foreach (OsuHitObjectDifficulty hitObject in DifficultyHitObjects)
{
// While we are beyond the current interval push the currently available maximum to our strain list
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
{
highestStrains.Add(maximumStrain);
// The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
// until the beginning of the next interval.
if (previousHitObject == null)
{
maximumStrain = 0;
}
else
{
double decay = Math.Pow(OsuHitObjectDifficulty.DECAY_BASE[(int)type], (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
maximumStrain = previousHitObject.Strains[(int)type] * decay;
}
// Go to the next time interval
intervalEndTime += actualStrainStep;
}
// Obtain maximum strain
maximumStrain = Math.Max(hitObject.Strains[(int)type], maximumStrain);
previousHitObject = hitObject;
}
// Build the weighted sum over the highest strains for each interval
double difficulty = 0;
double weight = 1;
highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
foreach (double strain in highestStrains)
{
difficulty += weight * strain;
weight *= DECAY_WEIGHT;
}
return difficulty;
}
protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter() => new OsuBeatmapConverter();
// Those values are used as array indices. Be careful when changing them!
public enum DifficultyType
{
Speed = 0,
Aim,
};
}
}
+9 -1
View File
@@ -7,6 +7,7 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.OsuDifficulty;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
@@ -46,7 +47,14 @@ namespace osu.Game.Rulesets.Osu
{
new OsuModEasy(),
new OsuModNoFail(),
new OsuModHalfTime(),
new MultiMod
{
Mods = new Mod[]
{
new OsuModHalfTime(),
new OsuModDaycore(),
},
},
};
case ModType.DifficultyIncrease:
@@ -34,8 +34,9 @@
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<Reference Include="OpenTK, Version=2.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.2.0.50727.1341\lib\net45\OpenTK.dll</HintPath>
<Reference Include="OpenTK, Version=3.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\ppy.OpenTK.3.0\lib\net45\OpenTK.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
@@ -68,9 +69,14 @@
<Compile Include="Objects\Drawables\Pieces\TrianglesPiece.cs" />
<Compile Include="Objects\Drawables\Pieces\SliderBall.cs" />
<Compile Include="Objects\Drawables\Pieces\SliderBody.cs" />
<Compile Include="Objects\OsuHitObjectDifficulty.cs" />
<Compile Include="Objects\SliderTick.cs" />
<Compile Include="OsuDifficultyCalculator.cs" />
<Compile Include="OsuDifficulty\OsuDifficultyCalculator.cs" />
<Compile Include="OsuDifficulty\Preprocessing\OsuDifficultyBeatmap.cs" />
<Compile Include="OsuDifficulty\Preprocessing\OsuDifficultyHitObject.cs" />
<Compile Include="OsuDifficulty\Skills\Aim.cs" />
<Compile Include="OsuDifficulty\Skills\Skill.cs" />
<Compile Include="OsuDifficulty\Skills\Speed.cs" />
<Compile Include="OsuDifficulty\Utils\History.cs" />
<Compile Include="OsuKeyConversionInputManager.cs" />
<Compile Include="Scoring\OsuScoreProcessor.cs" />
<Compile Include="UI\OsuHitRenderer.cs" />
+1 -1
View File
@@ -5,5 +5,5 @@ Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-->
<packages>
<package id="ppy.OpenTK" version="2.0.50727.1341" targetFramework="net45" />
<package id="ppy.OpenTK" version="3.0" targetFramework="net45" />
</packages>
@@ -12,6 +12,7 @@ using osu.Game.Database;
using osu.Game.IO.Serialization;
using osu.Game.Audio;
using osu.Game.Rulesets.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Taiko.Beatmaps
{
@@ -77,8 +78,11 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
int repeats = repeatsData?.RepeatCount ?? 1;
double speedAdjustment = beatmap.TimingInfo.SpeedMultiplierAt(obj.StartTime);
double speedAdjustedBeatLength = beatmap.TimingInfo.BeatLengthAt(obj.StartTime) * speedAdjustment;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime);
double speedAdjustment = difficultyPoint.SpeedMultiplier;
double speedAdjustedBeatLength = timingPoint.BeatLength * speedAdjustment;
// The true distance, accounting for any repeats. This ends up being the drum roll distance later
double distance = distanceData.Distance * repeats * legacy_velocity_multiplier;
+5
View File
@@ -37,6 +37,11 @@ namespace osu.Game.Rulesets.Taiko.Mods
}
public class TaikoModDaycore : ModDaycore
{
public override double ScoreMultiplier => 0.5;
}
public class TaikoModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => 1.12;
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Size = new Vector2(TaikoHitObject.DEFAULT_CIRCLE_DIAMETER),
BlendingMode = BlendingMode.Additive,
Masking = true,
Children = new []
Children = new[]
{
new Box
{
@@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new []
Children = new[]
{
symbol = new SwellSymbolPiece()
}

Some files were not shown because too many files have changed in this diff Show More