1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 18:23:04 +08:00

Merge branch 'master' into sliderbouncers

This commit is contained in:
Dean Herbert 2017-10-22 14:12:03 +09:00 committed by GitHub
commit ad685bd919
211 changed files with 6360 additions and 1997 deletions

29
.vscode/launch.json vendored
View File

@ -1,13 +1,13 @@
{
"version": "0.2.0",
"configurations": [{
"name": "osu! (VisualTests)",
"name": "osu! VisualTests (Debug)",
"windows": {
"type": "clr"
},
"type": "mono",
"request": "launch",
"program": "${workspaceRoot}/osu.Game/bin/Debug/osu!.exe",
"program": "${workspaceRoot}/osu.Desktop/bin/Debug/osu!.exe",
"args": [
"--tests"
],
@ -18,13 +18,30 @@
"console": "internalConsole"
},
{
"name": "osu! (debug)",
"name": "osu! VisualTests (Release)",
"windows": {
"type": "clr"
},
"type": "mono",
"request": "launch",
"program": "${workspaceRoot}/osu.Game/bin/Debug/osu!.exe",
"program": "${workspaceRoot}/osu.Desktop/bin/Release/osu!.exe",
"args": [
"--tests"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
"runtimeExecutable": null,
"env": {},
"console": "internalConsole"
},
{
"name": "osu! (Debug)",
"windows": {
"type": "clr"
},
"type": "mono",
"request": "launch",
"program": "${workspaceRoot}/osu.Desktop/bin/Debug/osu!.exe",
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug)",
"runtimeExecutable": null,
@ -32,13 +49,13 @@
"console": "internalConsole"
},
{
"name": "osu! (release)",
"name": "osu! (Release)",
"windows": {
"type": "clr"
},
"type": "mono",
"request": "launch",
"program": "${workspaceRoot}/osu.Game/bin/Release/osu!.exe",
"program": "${workspaceRoot}/osu.Desktop/bin/Release/osu!.exe",
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release)",
"runtimeExecutable": null,

1
.vscode/tasks.json vendored
View File

@ -23,6 +23,7 @@
},
{
"taskName": "Build (Release)",
"group": "build",
"args": [
"/property:Configuration=Release"
],

View File

@ -1,6 +1,7 @@
# 2017-09-14
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2017
configuration: Debug
cache:
- C:\ProgramData\chocolatey\bin -> appveyor.yml
@ -11,7 +12,7 @@ install:
- cmd: git submodule update --init --recursive
- cmd: choco install resharper-clt -y
- cmd: choco install nvika -y
- cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.2/CodeFileSanity.exe
- cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.3/CodeFileSanity.exe
before_build:
- cmd: CodeFileSanity.exe
- cmd: nuget restore

@ -1 +1 @@
Subproject commit cdb031c3a8ef693cd71458c5e19c68127ab72938
Subproject commit 383a8da7bc45af498288b4b72c72a048a0996e74

View File

@ -0,0 +1,25 @@
<configuration>
<dllmap os="linux" dll="opengl32.dll" target="libGL.so.1"/>
<dllmap os="linux" dll="glu32.dll" target="libGLU.so.1"/>
<dllmap os="linux" dll="openal32.dll" target="libopenal.so.1"/>
<dllmap os="linux" dll="alut.dll" target="libalut.so.0"/>
<dllmap os="linux" dll="opencl.dll" target="libOpenCL.so"/>
<dllmap os="linux" dll="libX11" target="libX11.so.6"/>
<dllmap os="linux" dll="libXi" target="libXi.so.6"/>
<dllmap os="linux" dll="SDL2.dll" target="libSDL2-2.0.so.0"/>
<dllmap os="osx" dll="opengl32.dll" target="/System/Library/Frameworks/OpenGL.framework/OpenGL"/>
<dllmap os="osx" dll="openal32.dll" target="/System/Library/Frameworks/OpenAL.framework/OpenAL" />
<dllmap os="osx" dll="alut.dll" target="/System/Library/Frameworks/OpenAL.framework/OpenAL" />
<dllmap os="osx" dll="libGLES.dll" target="/System/Library/Frameworks/OpenGLES.framework/OpenGLES" />
<dllmap os="osx" dll="libGLESv1_CM.dll" target="/System/Library/Frameworks/OpenGLES.framework/OpenGLES" />
<dllmap os="osx" dll="libGLESv2.dll" target="/System/Library/Frameworks/OpenGLES.framework/OpenGLES" />
<dllmap os="osx" dll="opencl.dll" target="/System/Library/Frameworks/OpenCL.framework/OpenCL"/>
<dllmap os="osx" dll="SDL2.dll" target="libSDL2.dylib"/>
<!-- XQuartz compatibility (X11 on Mac) -->
<dllmap os="osx" dll="libGL.so.1" target="/usr/X11/lib/libGL.dylib"/>
<dllmap os="osx" dll="libX11" target="/usr/X11/lib/libX11.dylib"/>
<dllmap os="osx" dll="libXcursor.so.1" target="/usr/X11/lib/libXcursor.dylib"/>
<dllmap os="osx" dll="libXi" target="/usr/X11/lib/libXi.dylib"/>
<dllmap os="osx" dll="libXinerama" target="/usr/X11/lib/libXinerama.dylib"/>
<dllmap os="osx" dll="libXrandr.so.2" target="/usr/X11/lib/libXrandr.dylib"/>
</configuration>

View File

@ -7,14 +7,15 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.Win32;
using osu.Desktop.Overlays;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Game.Overlays;
using osu.Game;
using osu.Game.Screens.Menu;
using OpenTK.Input;
namespace osu.Game
namespace osu.Desktop
{
internal class OsuGameDesktop : OsuGame
{
@ -104,16 +105,13 @@ namespace osu.Game
desktopWindow.Icon = new Icon(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
desktopWindow.Title = Name;
desktopWindow.DragEnter += dragEnter;
desktopWindow.DragDrop += dragDrop;
desktopWindow.FileDrop += fileDrop;
}
}
private void dragDrop(DragEventArgs e)
private void fileDrop(object sender, FileDropEventArgs e)
{
// this method will only be executed if e.Effect in dragEnter gets set to something other that None.
var dropData = (object[])e.Data.GetData(DataFormats.FileDrop);
var filePaths = dropData.Select(f => f.ToString()).ToArray();
var filePaths = new [] { e.FileName };
if (filePaths.All(f => Path.GetExtension(f) == @".osz"))
Task.Run(() => BeatmapManager.Import(filePaths));
@ -126,16 +124,5 @@ namespace osu.Game
}
private static readonly string[] allowed_extensions = { @".osz", @".osr" };
private void dragEnter(DragEventArgs e)
{
// dragDrop will only be executed if e.Effect gets set to something other that None in this method.
bool isFile = e.Data.GetDataPresent(DataFormats.FileDrop);
if (isFile)
{
var paths = ((object[])e.Data.GetData(DataFormats.FileDrop)).Select(f => f.ToString()).ToArray();
e.Effect = allowed_extensions.Any(ext => paths.All(p => p.EndsWith(ext))) ? DragDropEffects.Copy : DragDropEffects.None;
}
}
}
}

View File

@ -3,9 +3,10 @@
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game;
using osu.Game.Screens.Backgrounds;
namespace osu.Game
namespace osu.Desktop
{
internal class OsuTestBrowser : OsuGameBase
{

View File

@ -13,15 +13,17 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Game;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using OpenTK;
using OpenTK.Graphics;
using Squirrel;
namespace osu.Game.Overlays
namespace osu.Desktop.Overlays
{
public class VersionManager : OverlayContainer
{

View File

@ -8,7 +8,7 @@ using osu.Framework;
using osu.Framework.Platform;
using osu.Game.IPC;
namespace osu.Game
namespace osu.Desktop
{
public static class Program
{

View File

@ -0,0 +1,28 @@
// 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.Reflection;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("osu!lazer")]
[assembly: AssemblyDescription("click the circles. to the beat.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("ppy Pty Ltd")]
[assembly: AssemblyProduct("osu!lazer")]
[assembly: AssemblyCopyright("ppy Pty Ltd 2007-2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("b0cb1d48-e4c2-4612-a347-beea7b1a71e7")]
[assembly: AssemblyVersion("0.0.0")]
[assembly: AssemblyFileVersion("0.0.0")]

20
osu.Desktop/app.config Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /></startup>
<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>
<dependentAssembly>
<assemblyIdentity name="SharpCompress" publicKeyToken="afb0a02973931d96" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.18.1.0" newVersion="0.18.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

BIN
osu.Desktop/osu!.res Normal file

Binary file not shown.

View File

@ -0,0 +1,261 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="12.0">
<PropertyGroup>
<ProjectGuid>{419659FD-72EA-4678-9EB8-B22A746CED70}</ProjectGuid>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>osu.Desktop</RootNamespace>
<AssemblyName>osu!</AssemblyName>
<ManifestCertificateThumbprint>3CF060CD28877D0E3112948951A64B2A7CEEC909</ManifestCertificateThumbprint>
<ManifestKeyFile>codesigning.pfx</ManifestKeyFile>
<GenerateManifests>false</GenerateManifests>
<SignManifests>false</SignManifests>
<IsWebBootstrapper>false</IsWebBootstrapper>
<FileUpgradeFlags>
</FileUpgradeFlags>
<OldToolsVersion>3.5</OldToolsVersion>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<StartupObject>osu.Desktop.Program</StartupObject>
<RunPostBuildEvent>OnOutputUpdated</RunPostBuildEvent>
<SignAssembly>false</SignAssembly>
<TargetZone>LocalIntranet</TargetZone>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>2</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<ProductVersion>12.0.0</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<TargetFrameworkProfile>
</TargetFrameworkProfile>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>0</WarningLevel>
<NoStdLib>true</NoStdLib>
<UseVSHostingProcess>false</UseVSHostingProcess>
<PlatformTarget>AnyCPU</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RunCodeAnalysis>false</RunCodeAnalysis>
<Prefer32Bit>false</Prefer32Bit>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<Commandlineparameters>
</Commandlineparameters>
<LangVersion>6</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>none</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>CuttingEdge NoUpdate</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<NoStdLib>true</NoStdLib>
<UseVSHostingProcess>false</UseVSHostingProcess>
<PlatformTarget>AnyCPU</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Prefer32Bit>false</Prefer32Bit>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<Win32Resource>
</Win32Resource>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>lazer.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>Properties\app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'VisualTests|AnyCPU'">
<DebugSymbols>true</DebugSymbols>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG</DefineConstants>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<WarningLevel>0</WarningLevel>
<NoStdLib>true</NoStdLib>
<DebugType>full</DebugType>
<PlatformTarget>AnyCPU</PlatformTarget>
<UseVSHostingProcess>false</UseVSHostingProcess>
<LangVersion>6</LangVersion>
<ErrorReport>prompt</ErrorReport>
<StartArguments>--tests</StartArguments>
</PropertyGroup>
<ItemGroup>
<Reference Include="DeltaCompressionDotNet, Version=1.1.0.0, Culture=neutral, PublicKeyToken=1d14d6e5194e7f4a, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="DeltaCompressionDotNet.MsDelta, Version=1.1.0.0, Culture=neutral, PublicKeyToken=46b2138a390abf55, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.MsDelta.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="DeltaCompressionDotNet.PatchApi, Version=1.1.0.0, Culture=neutral, PublicKeyToken=3e8888ee913ed789, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\DeltaCompressionDotNet.1.1.0\lib\net20\DeltaCompressionDotNet.PatchApi.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>
</Reference>
<Reference Include="Mono.Cecil.Mdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Cecil.Pdb, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Mono.Cecil.Rocks, Version=0.9.6.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756, processorArchitecture=MSIL">
<HintPath>$(SolutionDir)\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Rocks.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="mscorlib" />
<Reference Include="NuGet.Squirrel, Version=3.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\squirrel.windows.1.7.8\lib\Net45\NuGet.Squirrel.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="OpenTK, Version=3.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4, processorArchitecture=MSIL">
<HintPath>..\packages\OpenTK.3.0.0-git00009\lib\net20\OpenTK.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="SharpCompress, Version=0.18.1.0, Culture=neutral, PublicKeyToken=afb0a02973931d96, processorArchitecture=MSIL">
<HintPath>..\packages\SharpCompress.0.18.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.7.8.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\squirrel.windows.1.7.8\lib\Net45\Squirrel.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Drawing" />
<Reference Include="System.Net.Http" />
<Reference Include="System.ValueTuple, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51">
<HintPath>../packages/System.ValueTuple.4.4.0/lib/net461/System.ValueTuple.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<None Include="..\osu.licenseheader">
<Link>osu.licenseheader</Link>
</None>
<None Include="app.config" />
<None Include="OpenTK.dll.config" />
<None Include="osu!.res" />
<None Include="packages.config" />
<None Include="Properties\app.manifest" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.2.0">
<Visible>False</Visible>
<ProductName>.NET Framework 2.0 %28x86%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.0">
<Visible>False</Visible>
<ProductName>.NET Framework 3.0 %28x86%29</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<Compile Include="OsuGameDesktop.cs" />
<Compile Include="OsuTestBrowser.cs" />
<Compile Include="Overlays\VersionManager.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="lazer.ico" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu-framework\osu.Framework\osu.Framework.csproj">
<Project>{c76bf5b3-985e-4d39-95fe-97c9c879b83a}</Project>
<Name>osu.Framework</Name>
</ProjectReference>
<ProjectReference Include="..\osu-resources\osu.Game.Resources\osu.Game.Resources.csproj">
<Project>{d9a367c9-4c1a-489f-9b05-a0cea2b53b58}</Project>
<Name>osu.Game.Resources</Name>
</ProjectReference>
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj">
<Project>{58f6c80c-1253-4a0e-a465-b8c85ebeadf3}</Project>
<Name>osu.Game.Rulesets.Catch</Name>
</ProjectReference>
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj">
<Project>{48f4582b-7687-4621-9cbe-5c24197cb536}</Project>
<Name>osu.Game.Rulesets.Mania</Name>
</ProjectReference>
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj">
<Project>{c92a607b-1fdd-4954-9f92-03ff547d9080}</Project>
<Name>osu.Game.Rulesets.Osu</Name>
</ProjectReference>
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj">
<Project>{f167e17a-7de6-4af5-b920-a5112296c695}</Project>
<Name>osu.Game.Rulesets.Taiko</Name>
</ProjectReference>
<ProjectReference Include="..\osu.Game\osu.Game.csproj">
<Project>{2a66dd92-adb1-4994-89e2-c94e04acda0d}</Project>
<Name>osu.Game</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
<ProjectExtensions>
<VisualStudio>
</VisualStudio>
</ProjectExtensions>
<PropertyGroup>
<PreBuildEvent>
</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
<PostBuildEvent>
</PostBuildEvent>
</PropertyGroup>
</Project>

26
osu.Desktop/osu.nuspec Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>osulazer</id>
<version>0.0.0</version>
<title>osulazer</title>
<authors>ppy Pty Ltd</authors>
<owners>Dean Herbert</owners>
<projectUrl>https://osu.ppy.sh/</projectUrl>
<iconUrl>https://puu.sh/tYyXZ/9a01a5d1b0.ico</iconUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>click the circles. to the beat.</description>
<summary>click the circles.</summary>
<releaseNotes>testing</releaseNotes>
<copyright>Copyright ppy Pty Ltd 2007-2017</copyright>
<language>en-AU</language>
</metadata>
<files>
<file src="*.exe" target="lib\net45\" exclude="**vshost**"/>
<file src="*.dll" target="lib\net45\"/>
<file src="*.config" target="lib\net45\"/>
<file src="x86\*.dll" target="lib\net45\x86\"/>
<file src="x64\*.dll" target="lib\net45\x64\"/>
</files>
</package>

View File

@ -0,0 +1,14 @@
<?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="DeltaCompressionDotNet" version="1.1.0" targetFramework="net45" />
<package id="Mono.Cecil" version="0.9.6.4" targetFramework="net45" />
<package id="OpenTK" version="3.0.0-git00009" targetFramework="net461" />
<package id="SharpCompress" version="0.18.1" targetFramework="net461" />
<package id="Splat" version="2.0.0" targetFramework="net45" />
<package id="squirrel.windows" version="1.7.8" targetFramework="net461" />
<package id="System.ValueTuple" version="4.4.0" targetFramework="net461" />
</packages>

View File

@ -5,9 +5,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic;
using System;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
@ -17,14 +17,37 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
protected override IEnumerable<CatchBaseHit> ConvertHitObject(HitObject obj, Beatmap beatmap)
{
if (!(obj is IHasXPosition))
var curveData = obj as IHasCurve;
var positionData = obj as IHasPosition;
var comboData = obj as IHasCombo;
if (positionData == null)
yield break;
if (curveData != null)
{
yield return new JuiceStream
{
StartTime = obj.StartTime,
Samples = obj.Samples,
ControlPoints = curveData.ControlPoints,
CurveType = curveData.CurveType,
Distance = curveData.Distance,
RepeatSamples = curveData.RepeatSamples,
RepeatCount = curveData.RepeatCount,
X = positionData.X / CatchPlayfield.BASE_WIDTH,
NewCombo = comboData?.NewCombo ?? false
};
yield break;
}
yield return new Fruit
{
StartTime = obj.StartTime,
NewCombo = (obj as IHasCombo)?.NewCombo ?? false,
X = ((IHasXPosition)obj).X / OsuPlayfield.BASE_SIZE.X
Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false,
X = positionData.X / CatchPlayfield.BASE_WIDTH
};
}
}

View File

@ -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 System;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public abstract class DrawableCatchHitObject<TObject> : DrawableCatchHitObject
where TObject : CatchBaseHit
{
public new TObject HitObject;
protected DrawableCatchHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
}
}
public abstract class DrawableCatchHitObject : DrawableScrollingHitObject<CatchBaseHit>
{
protected DrawableCatchHitObject(CatchBaseHit hitObject)
: base(hitObject)
{
RelativePositionAxes = Axes.Both;
X = hitObject.X;
Y = (float)HitObject.StartTime;
}
public Func<CatchBaseHit, bool> CheckPosition;
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
{
if (timeOffset > 0)
AddJudgement(new Judgement { Result = CheckPosition?.Invoke(HitObject) ?? false ? HitResult.Perfect : HitResult.Miss });
}
private const float preempt = 1000;
protected override void UpdateState(ArmedState state)
{
using (BeginAbsoluteSequence(HitObject.StartTime - preempt))
{
// animation
this.FadeIn(200);
}
switch (state)
{
case ArmedState.Miss:
using (BeginAbsoluteSequence(HitObject.StartTime, true))
this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out);
break;
}
}
}
}

View File

@ -0,0 +1,34 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces;
using OpenTK;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public class DrawableDroplet : DrawableCatchHitObject<Droplet>
{
public DrawableDroplet(Droplet h)
: base(h)
{
Origin = Anchor.Centre;
Size = new Vector2(Pulp.PULP_SIZE);
AccentColour = h.ComboColour;
Masking = false;
}
[BackgroundDependencyLoader]
private void load()
{
Child = new Pulp
{
AccentColour = AccentColour,
Scale = new Vector2(0.8f),
};
}
}
}

View File

@ -1,72 +1,29 @@
// 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.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.MathUtils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public class DrawableFruit : DrawableScrollingHitObject<CatchBaseHit>
public class DrawableFruit : DrawableCatchHitObject<Fruit>
{
private const float pulp_size = 20;
private class Pulp : Circle, IHasAccentColour
{
public Pulp()
{
Size = new Vector2(pulp_size);
Blending = BlendingMode.Additive;
Colour = Color4.White.Opacity(0.9f);
}
private Color4 accentColour;
public Color4 AccentColour
{
get { return accentColour; }
set
{
accentColour = value;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 5,
Colour = accentColour.Lighten(100),
};
}
}
}
public DrawableFruit(CatchBaseHit h)
public DrawableFruit(Fruit h)
: base(h)
{
Origin = Anchor.Centre;
Size = new Vector2(pulp_size * 2.2f, pulp_size * 2.8f);
RelativePositionAxes = Axes.Both;
X = h.X;
Size = new Vector2(Pulp.PULP_SIZE * 2.2f, Pulp.PULP_SIZE * 2.8f);
AccentColour = HitObject.ComboColour;
Masking = false;
Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
}
public Func<CatchBaseHit, bool> CheckPosition;
[BackgroundDependencyLoader]
private void load()
{
@ -114,30 +71,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
}
};
}
private const float preempt = 1000;
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
{
if (timeOffset > 0)
AddJudgement(new Judgement { Result = CheckPosition?.Invoke(HitObject) ?? false ? HitResult.Perfect : HitResult.Miss });
}
protected override void UpdateState(ArmedState state)
{
using (BeginAbsoluteSequence(HitObject.StartTime - preempt))
{
// animation
this.FadeIn(200);
}
switch (state)
{
case ArmedState.Miss:
using (BeginAbsoluteSequence(HitObject.StartTime, true))
this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out);
break;
}
}
}
}

View File

@ -0,0 +1,54 @@
// 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 OpenTK;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
public class DrawableJuiceStream : DrawableCatchHitObject<JuiceStream>
{
private readonly Container dropletContainer;
public DrawableJuiceStream(JuiceStream s) : base(s)
{
RelativeSizeAxes = Axes.Both;
Height = (float)HitObject.Duration;
X = 0;
Child = dropletContainer = new Container
{
RelativeSizeAxes = Axes.Both,
RelativeChildOffset = new Vector2(0, (float)HitObject.StartTime),
RelativeChildSize = new Vector2(1, (float)HitObject.Duration)
};
foreach (CatchBaseHit tick in s.Ticks)
{
TinyDroplet tiny = tick as TinyDroplet;
if (tiny != null)
{
AddNested(new DrawableDroplet(tiny) { Scale = new Vector2(0.5f) });
continue;
}
Droplet droplet = tick as Droplet;
if (droplet != null)
AddNested(new DrawableDroplet(droplet));
Fruit fruit = tick as Fruit;
if (fruit != null)
AddNested(new DrawableFruit(fruit));
}
}
protected override void AddNested(DrawableHitObject<CatchBaseHit> h)
{
((DrawableCatchHitObject)h).CheckPosition = o => CheckPosition?.Invoke(o) ?? false;
dropletContainer.Add(h);
base.AddNested(h);
}
}
}

View File

@ -0,0 +1,43 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces
{
public class Pulp : Circle, IHasAccentColour
{
public const float PULP_SIZE = 20;
public Pulp()
{
Size = new Vector2(PULP_SIZE);
Blending = BlendingMode.Additive;
Colour = Color4.White.Opacity(0.9f);
}
private Color4 accentColour;
public Color4 AccentColour
{
get { return accentColour; }
set
{
accentColour = value;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 5,
Colour = accentColour.Lighten(100),
};
}
}
}
}

View File

@ -0,0 +1,169 @@
// 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.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using OpenTK;
using osu.Framework.Lists;
namespace osu.Game.Rulesets.Catch.Objects
{
public class JuiceStream : CatchBaseHit, IHasCurve
{
/// <summary>
/// Positional distance that results in a duration of one second, before any speed adjustments.
/// </summary>
private const float base_scoring_distance = 100;
public readonly SliderCurve Curve = new SliderCurve();
public int RepeatCount { get; set; } = 1;
public double Velocity;
public double TickDistance;
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate;
}
public IEnumerable<CatchBaseHit> Ticks
{
get
{
SortedList<CatchBaseHit> ticks = new SortedList<CatchBaseHit>((a, b) => a.StartTime.CompareTo(b.StartTime));
if (TickDistance == 0)
return ticks;
var length = Curve.Distance;
var tickDistance = Math.Min(TickDistance, length);
var repeatDuration = length / Velocity;
var minDistanceFromEnd = Velocity * 0.01;
ticks.Add(new Fruit
{
Samples = Samples,
ComboColour = ComboColour,
StartTime = StartTime,
X = X
});
for (var repeat = 0; repeat < RepeatCount; repeat++)
{
var repeatStartTime = StartTime + repeat * repeatDuration;
var reversed = repeat % 2 == 1;
for (var d = tickDistance; d <= length; d += tickDistance)
{
if (d > length - minDistanceFromEnd)
break;
var timeProgress = d / length;
var distanceProgress = reversed ? 1 - timeProgress : timeProgress;
var lastTickTime = repeatStartTime + timeProgress * repeatDuration;
ticks.Add(new Droplet
{
StartTime = lastTickTime,
ComboColour = ComboColour,
X = Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
{
Bank = s.Bank,
Name = @"slidertick",
Volume = s.Volume
}))
});
}
double tinyTickInterval = tickDistance / length * repeatDuration;
while (tinyTickInterval > 100)
tinyTickInterval /= 2;
for (double t = 0; t < repeatDuration; t += tinyTickInterval)
{
double progress = reversed ? 1 - t / repeatDuration : t / repeatDuration;
ticks.Add(new TinyDroplet
{
StartTime = repeatStartTime + t,
ComboColour = ComboColour,
X = Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
{
Bank = s.Bank,
Name = @"slidertick",
Volume = s.Volume
}))
});
}
ticks.Add(new Fruit
{
Samples = Samples,
ComboColour = ComboColour,
StartTime = repeatStartTime + repeatDuration,
X = Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
});
}
return ticks;
}
}
public double EndTime => StartTime + RepeatCount * Curve.Distance / Velocity;
public float EndX => Curve.PositionAt(ProgressAt(1)).X / CatchPlayfield.BASE_WIDTH;
public double Duration => EndTime - StartTime;
public double Distance
{
get { return Curve.Distance; }
set { Curve.Distance = value; }
}
public List<Vector2> ControlPoints
{
get { return Curve.ControlPoints; }
set { Curve.ControlPoints = value; }
}
public List<SampleInfoList> RepeatSamples { get; set; } = new List<SampleInfoList>();
public CurveType CurveType
{
get { return Curve.CurveType; }
set { Curve.CurveType = value; }
}
public Vector2 PositionAt(double progress) => Curve.PositionAt(ProgressAt(progress));
public double ProgressAt(double progress)
{
double p = progress * RepeatCount % 1;
if (RepeatAt(progress) % 2 == 1)
p = 1 - p;
return p;
}
public int RepeatAt(double progress) => (int)(progress * RepeatCount);
}
}

View File

@ -0,0 +1,9 @@
// 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.Catch.Objects
{
public class TinyDroplet : Droplet
{
}
}

View File

@ -8,11 +8,11 @@ using System.Runtime.InteropServices;
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("osu.Game.Rulesets.Catch")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyDescription("catch the fruit. to the beat.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyCompany("ppy Pty Ltd")]
[assembly: AssemblyProduct("osu.Game.Rulesets.Catch")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyCopyright("ppy Pty Ltd 2007-2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@ -24,15 +24,5 @@ using System.Runtime.InteropServices;
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("58f6c80c-1253-4a0e-a465-b8c85ebeadf3")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -21,6 +21,19 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
foreach (var obj in beatmap.HitObjects)
{
var stream = obj as JuiceStream;
if (stream != null)
{
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
foreach (var unused in stream.Ticks)
AddJudgement(new CatchJudgement { Result = HitResult.Perfect });
continue;
}
var fruit = obj as Fruit;
if (fruit != null)

View File

@ -2,22 +2,14 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class TestCaseCatchPlayer : Game.Tests.Visual.TestCasePlayer
{
protected override Beatmap CreateBeatmap()
public TestCaseCatchPlayer() : base(typeof(CatchRuleset))
{
var beatmap = new Beatmap();
for (int i = 0; i < 256; i++)
beatmap.HitObjects.Add(new Fruit { X = 0.5f, StartTime = i * 100, NewCombo = i % 8 == 0 });
return beatmap;
}
}
}

View File

@ -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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class TestCaseCatchStacker : Game.Tests.Visual.TestCasePlayer
{
public TestCaseCatchStacker() : base(typeof(CatchRuleset))
{
}
protected override Beatmap CreateBeatmap()
{
var beatmap = new Beatmap();
for (int i = 0; i < 256; i++)
beatmap.HitObjects.Add(new Fruit { X = 0.5f, StartTime = i * 100, NewCombo = i % 8 == 0 });
return beatmap;
}
}
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(CatcherArea),
typeof(Catcher),
};
[BackgroundDependencyLoader]
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Tests
new CatchInputManager(rulesets.GetRuleset(2))
{
RelativeSizeAxes = Axes.Both,
Child = new CatcherArea
Child = new Catcher
{
RelativePositionAxes = Axes.Both,
RelativeSizeAxes = Axes.Both,

View File

@ -1,10 +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
using System;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI;
using OpenTK;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
@ -13,16 +15,20 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatchPlayfield : ScrollingPlayfield
{
public static readonly float BASE_WIDTH = 512;
protected override Container<Drawable> Content => content;
private readonly Container<Drawable> content;
private readonly CatcherArea catcherArea;
private readonly Container catcherContainer;
private readonly Catcher catcher;
public CatchPlayfield()
: base(Axes.Y)
{
Reversed.Value = true;
Container explodingFruitContainer;
Size = new Vector2(1);
Reversed.Value = true;
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
@ -33,24 +39,43 @@ namespace osu.Game.Rulesets.Catch.UI
{
RelativeSizeAxes = Axes.Both,
},
catcherArea = new CatcherArea
explodingFruitContainer = new Container
{
RelativeSizeAxes = Axes.Both,
},
catcherContainer = new Container
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
Height = 0.3f
Height = 180,
Child = catcher = new Catcher
{
ExplodingFruitTarget = explodingFruitContainer,
RelativePositionAxes = Axes.Both,
Origin = Anchor.TopCentre,
X = 0.5f,
}
}
};
}
protected override void Update()
{
base.Update();
catcher.Size = new Vector2(catcherContainer.DrawSize.Y);
}
public bool CheckIfWeCanCatch(CatchBaseHit obj) => Math.Abs(catcher.Position.X - obj.X) < catcher.DrawSize.X / DrawSize.X / 2;
public override void Add(DrawableHitObject h)
{
h.Depth = (float)h.HitObject.StartTime;
base.Add(h);
var fruit = (DrawableFruit)h;
fruit.CheckPosition = catcherArea.CheckIfWeCanCatch;
var fruit = (DrawableCatchHitObject)h;
fruit.CheckPosition = CheckIfWeCanCatch;
}
public override void OnJudgement(DrawableHitObject judgedObject, Judgement judgement)
@ -58,8 +83,12 @@ namespace osu.Game.Rulesets.Catch.UI
if (judgement.IsHit)
{
Vector2 screenPosition = judgedObject.ScreenSpaceDrawQuad.Centre;
Remove(judgedObject);
catcherArea.Add(judgedObject, screenPosition);
// todo: don't do this
(judgedObject.Parent as Container<DrawableHitObject>)?.Remove(judgedObject);
(judgedObject.Parent as Container)?.Remove(judgedObject);
catcher.Add(judgedObject, screenPosition);
}
}
}

View File

@ -32,8 +32,13 @@ namespace osu.Game.Rulesets.Catch.UI
protected override DrawableHitObject<CatchBaseHit> GetVisualRepresentation(CatchBaseHit h)
{
if (h is Fruit)
return new DrawableFruit(h);
var fruit = h as Fruit;
if (fruit != null)
return new DrawableFruit(fruit);
var stream = h as JuiceStream;
if (stream != null)
return new DrawableJuiceStream(stream);
return null;
}

View File

@ -0,0 +1,193 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class Catcher : Container, IKeyBindingHandler<CatchAction>
{
private Texture texture;
private Container<DrawableHitObject> caughtFruit;
public Container ExplodingFruitTarget;
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
texture = textures.Get(@"Play/Catch/fruit-catcher-idle");
Children = new Drawable[]
{
createCatcherSprite(),
caughtFruit = new Container<DrawableHitObject>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
}
};
}
private int currentDirection;
private bool dashing;
protected bool Dashing
{
get { return dashing; }
set
{
if (value == dashing) return;
dashing = value;
if (dashing)
Schedule(addAdditiveSprite);
}
}
private void addAdditiveSprite()
{
if (!dashing) return;
var additive = createCatcherSprite();
additive.RelativePositionAxes = Axes.Both;
additive.Blending = BlendingMode.Additive;
additive.Position = Position;
additive.Scale = Scale;
((Container)Parent).Add(additive);
additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint).Expire();
Scheduler.AddDelayed(addAdditiveSprite, 50);
}
private Sprite createCatcherSprite() => new Sprite
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Texture = texture,
OriginPosition = new Vector2(DrawWidth / 2, 10) //temporary until the sprite is aligned correctly.
};
public bool OnPressed(CatchAction action)
{
switch (action)
{
case CatchAction.MoveLeft:
currentDirection--;
return true;
case CatchAction.MoveRight:
currentDirection++;
return true;
case CatchAction.Dash:
Dashing = true;
return true;
}
return false;
}
public bool OnReleased(CatchAction action)
{
switch (action)
{
case CatchAction.MoveLeft:
currentDirection++;
return true;
case CatchAction.MoveRight:
currentDirection--;
return true;
case CatchAction.Dash:
Dashing = false;
return true;
}
return false;
}
/// <summary>
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
/// </summary>
private const double base_speed = 1.0 / 512;
protected override void Update()
{
base.Update();
if (currentDirection == 0) return;
double dashModifier = Dashing ? 1 : 0.5;
Scale = new Vector2(Math.Sign(currentDirection), 1);
X = (float)MathHelper.Clamp(X + Math.Sign(currentDirection) * Clock.ElapsedFrameTime * base_speed * dashModifier, 0, 1);
}
public void Add(DrawableHitObject fruit, Vector2 absolutePosition)
{
fruit.RelativePositionAxes = Axes.None;
fruit.Position = new Vector2(ToLocalSpace(absolutePosition).X - DrawSize.X / 2, 0);
fruit.Anchor = Anchor.TopCentre;
fruit.Origin = Anchor.BottomCentre;
fruit.Scale *= 0.7f;
fruit.LifetimeEnd = double.MaxValue;
float distance = fruit.DrawSize.X / 2 * fruit.Scale.X;
while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance))
{
fruit.X += RNG.Next(-5, 5);
fruit.Y -= RNG.Next(0, 5);
}
caughtFruit.Add(fruit);
if (((CatchBaseHit)fruit.HitObject).LastInCombo)
explode();
}
private void explode()
{
var fruit = caughtFruit.ToArray();
foreach (var f in fruit)
{
var originalX = f.X * Scale.X;
if (ExplodingFruitTarget != null)
{
f.Anchor = Anchor.TopLeft;
f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget);
caughtFruit.Remove(f);
ExplodingFruitTarget.Add(f);
}
f.MoveToY(f.Y - 50, 250, Easing.OutSine)
.Then()
.MoveToY(f.Y + 50, 500, Easing.InSine);
f.MoveToX(f.X + originalX * 6, 1000);
f.FadeOut(750);
f.Expire();
}
}
}
}

View File

@ -1,229 +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.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherArea : Container
{
private Catcher catcher;
private Container explodingFruitContainer;
public void Add(DrawableHitObject fruit, Vector2 screenPosition) => catcher.AddToStack(fruit, screenPosition);
public bool CheckIfWeCanCatch(CatchBaseHit obj) => Math.Abs(catcher.Position.X - obj.X) < catcher.DrawSize.X / DrawSize.X / 2;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
explodingFruitContainer = new Container
{
RelativeSizeAxes = Axes.Both,
},
catcher = new Catcher
{
RelativePositionAxes = Axes.Both,
ExplodingFruitTarget = explodingFruitContainer,
Origin = Anchor.TopCentre,
X = 0.5f,
}
};
}
protected override void Update()
{
base.Update();
catcher.Size = new Vector2(DrawSize.Y);
}
private class Catcher : Container, IKeyBindingHandler<CatchAction>
{
private Texture texture;
private Container<DrawableHitObject> caughtFruit;
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
texture = textures.Get(@"Play/Catch/fruit-catcher-idle");
Children = new Drawable[]
{
createCatcherSprite(),
caughtFruit = new Container<DrawableHitObject>
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
}
};
}
private int currentDirection;
private bool dashing;
public Container ExplodingFruitTarget;
protected bool Dashing
{
get { return dashing; }
set
{
if (value == dashing) return;
dashing = value;
if (dashing)
Schedule(addAdditiveSprite);
}
}
private void addAdditiveSprite()
{
if (!dashing) return;
var additive = createCatcherSprite();
additive.RelativePositionAxes = Axes.Both;
additive.Blending = BlendingMode.Additive;
additive.Position = Position;
additive.Scale = Scale;
((CatcherArea)Parent).Add(additive);
additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint).Expire();
Scheduler.AddDelayed(addAdditiveSprite, 50);
}
private Sprite createCatcherSprite() => new Sprite
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Texture = texture,
OriginPosition = new Vector2(DrawWidth / 2, 10) //temporary until the sprite is aligned correctly.
};
public bool OnPressed(CatchAction action)
{
switch (action)
{
case CatchAction.MoveLeft:
currentDirection--;
return true;
case CatchAction.MoveRight:
currentDirection++;
return true;
case CatchAction.Dash:
Dashing = true;
return true;
}
return false;
}
public bool OnReleased(CatchAction action)
{
switch (action)
{
case CatchAction.MoveLeft:
currentDirection++;
return true;
case CatchAction.MoveRight:
currentDirection--;
return true;
case CatchAction.Dash:
Dashing = false;
return true;
}
return false;
}
/// <summary>
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
/// </summary>
private const double base_speed = 1.0 / 512;
protected override void Update()
{
base.Update();
if (currentDirection == 0) return;
double dashModifier = Dashing ? 1 : 0.5;
Scale = new Vector2(Math.Sign(currentDirection), 1);
X = (float)MathHelper.Clamp(X + Math.Sign(currentDirection) * Clock.ElapsedFrameTime * base_speed * dashModifier, 0, 1);
}
public void AddToStack(DrawableHitObject fruit, Vector2 absolutePosition)
{
fruit.RelativePositionAxes = Axes.None;
fruit.Position = new Vector2(ToLocalSpace(absolutePosition).X - DrawSize.X / 2, 0);
fruit.Anchor = Anchor.TopCentre;
fruit.Origin = Anchor.BottomCentre;
fruit.Scale *= 0.7f;
fruit.LifetimeEnd = double.MaxValue;
float distance = fruit.DrawSize.X / 2 * fruit.Scale.X;
while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance))
{
fruit.X += RNG.Next(-5, 5);
fruit.Y -= RNG.Next(0, 5);
}
caughtFruit.Add(fruit);
if (((CatchBaseHit)fruit.HitObject).LastInCombo)
explode();
}
private void explode()
{
var fruit = caughtFruit.ToArray();
foreach (var f in fruit)
{
var originalX = f.X * Scale.X;
if (ExplodingFruitTarget != null)
{
f.Anchor = Anchor.TopLeft;
f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget);
caughtFruit.Remove(f);
ExplodingFruitTarget.Add(f);
}
f.MoveToY(f.Y - 50, 250, Easing.OutSine)
.Then()
.MoveToY(f.Y + 50, 500, Easing.InSine);
f.MoveToX(f.X + originalX * 6, 1000);
f.FadeOut(750);
f.Expire();
}
}
}
}
}

View File

@ -10,6 +10,10 @@
<assemblyIdentity name="SharpCompress" publicKeyToken="afb0a02973931d96" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.18.1.0" newVersion="0.18.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -16,7 +16,7 @@
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\osu.Game\bin\Debug\</OutputPath>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
@ -26,7 +26,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\osu.Game\bin\Release\</OutputPath>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
@ -50,16 +50,23 @@
<Compile Include="Beatmaps\CatchBeatmapProcessor.cs" />
<Compile Include="CatchDifficultyCalculator.cs" />
<Compile Include="CatchInputManager.cs" />
<Compile Include="Objects\Drawable\DrawableCatchHitObject.cs" />
<Compile Include="Objects\Drawable\DrawableDroplet.cs" />
<Compile Include="Objects\Drawable\DrawableJuiceStream.cs" />
<Compile Include="Objects\Drawable\Pieces\Pulp.cs" />
<Compile Include="Objects\JuiceStream.cs" />
<Compile Include="Scoring\CatchScoreProcessor.cs" />
<Compile Include="Judgements\CatchJudgement.cs" />
<Compile Include="Objects\CatchBaseHit.cs" />
<Compile Include="Objects\Drawable\DrawableFruit.cs" />
<Compile Include="Objects\Droplet.cs" />
<Compile Include="Objects\Fruit.cs" />
<Compile Include="Objects\TinyDroplet.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Tests\TestCaseCatcher.cs" />
<Compile Include="Tests\TestCaseCatchStacker.cs" />
<Compile Include="Tests\TestCaseCatchPlayer.cs" />
<Compile Include="UI\CatcherArea.cs" />
<Compile Include="UI\Catcher.cs" />
<Compile Include="UI\CatchRulesetContainer.cs" />
<Compile Include="UI\CatchPlayfield.cs" />
<Compile Include="CatchRuleset.cs" />
@ -79,11 +86,6 @@
<Name>osu.Framework</Name>
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj">
<Project>{C92A607B-1FDD-4954-9F92-03FF547D9080}</Project>
<Name>osu.Game.Rulesets.Osu</Name>
<Private>False</Private>
</ProjectReference>
<ProjectReference Include="..\osu.Game\osu.Game.csproj">
<Project>{2a66dd92-adb1-4994-89e2-c94e04acda0d}</Project>
<Name>osu.Game</Name>

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
{
beatmap = original;
BeatmapDifficulty difficulty = original.BeatmapInfo.Difficulty;
BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty;
int seed = (int)Math.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)Math.Round(difficulty.ApproachRate);
random = new FastRandom(seed);

View File

@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
// 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 * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;

View File

@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (drainTime == 0)
drainTime = 10000;
BeatmapDifficulty difficulty = Beatmap.BeatmapInfo.Difficulty;
BeatmapDifficulty difficulty = Beatmap.BeatmapInfo.BaseDifficulty;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);

View File

@ -21,6 +21,6 @@ namespace osu.Game.Rulesets.Mania
return 0;
}
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter() => new ManiaBeatmapConverter(true, (int)Math.Max(1, Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize)));
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter() => new ManiaBeatmapConverter(true, (int)Math.Max(1, Math.Round(Beatmap.BeatmapInfo.BaseDifficulty.CircleSize)));
}
}

View File

@ -8,11 +8,11 @@ using System.Runtime.InteropServices;
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("osu.Game.Rulesets.Mania")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyDescription("smash the keys. to the beat.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyCompany("ppy Pty Ltd")]
[assembly: AssemblyProduct("osu.Game.Rulesets.Mania")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyCopyright("ppy Pty Ltd 2007-2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@ -24,15 +24,5 @@ using System.Runtime.InteropServices;
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("48f4582b-7687-4621-9cbe-5c24197cb536")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
protected override void SimulateAutoplay(Beatmap<ManiaHitObject> beatmap)
{
BeatmapDifficulty difficulty = beatmap.BeatmapInfo.Difficulty;
BeatmapDifficulty difficulty = beatmap.BeatmapInfo.BaseDifficulty;
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);

View File

@ -10,7 +10,7 @@ using osu.Game.Tests.Visual;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.Testing
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
internal class TestCaseManiaHitObjects : OsuTestCase

View File

@ -17,7 +17,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Timing;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Testing
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
internal class TestCaseManiaPlayfield : OsuTestCase

View File

@ -88,18 +88,18 @@ namespace osu.Game.Rulesets.Mania.UI
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter()
{
if (IsForCurrentRuleset)
AvailableColumns = (int)Math.Max(1, Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.CircleSize));
AvailableColumns = (int)Math.Max(1, Math.Round(WorkingBeatmap.BeatmapInfo.BaseDifficulty.CircleSize));
else
{
float percentSliderOrSpinner = (float)WorkingBeatmap.Beatmap.HitObjects.Count(h => h is IHasEndTime) / WorkingBeatmap.Beatmap.HitObjects.Count;
if (percentSliderOrSpinner < 0.2)
AvailableColumns = 7;
else if (percentSliderOrSpinner < 0.3 || Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.CircleSize) >= 5)
AvailableColumns = Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty) > 5 ? 7 : 6;
else if (percentSliderOrSpinner < 0.3 || Math.Round(WorkingBeatmap.BeatmapInfo.BaseDifficulty.CircleSize) >= 5)
AvailableColumns = Math.Round(WorkingBeatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) > 5 ? 7 : 6;
else if (percentSliderOrSpinner > 0.6)
AvailableColumns = Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty) > 4 ? 5 : 4;
AvailableColumns = Math.Round(WorkingBeatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) > 4 ? 5 : 4;
else
AvailableColumns = Math.Max(4, Math.Min((int)Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty) + 1, 7));
AvailableColumns = Math.Max(4, Math.Min((int)Math.Round(WorkingBeatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) + 1, 7));
}
return new ManiaBeatmapConverter(IsForCurrentRuleset, AvailableColumns);

View File

@ -10,6 +10,10 @@
<assemblyIdentity name="SharpCompress" publicKeyToken="afb0a02973931d96" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.18.1.0" newVersion="0.18.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -16,7 +16,7 @@
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\osu.Game\bin\Debug\</OutputPath>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
@ -26,7 +26,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\osu.Game\bin\Release\</OutputPath>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
@ -80,8 +80,8 @@
<Compile Include="Objects\Note.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ManiaInputManager.cs" />
<Compile Include="Testing\TestCaseManiaHitObjects.cs" />
<Compile Include="Testing\TestCaseManiaPlayfield.cs" />
<Compile Include="Tests\TestCaseManiaHitObjects.cs" />
<Compile Include="Tests\TestCaseManiaPlayfield.cs" />
<Compile Include="Timing\GravityScrollingContainer.cs" />
<Compile Include="Timing\ScrollingAlgorithm.cs" />
<Compile Include="UI\Column.cs" />

View File

@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override bool RemoveWhenNotAlive => false;
public override bool DisplayJudgement => false;
public DrawableSliderTick(SliderTick sliderTick) : base(sliderTick)
{
this.sliderTick = sliderTick;

View File

@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly SpinnerDisc disc;
private readonly SpinnerTicks ticks;
private readonly SpinnerSpmCounter spmCounter;
private readonly Container mainContainer;
@ -103,6 +104,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
},
}
},
spmCounter = new SpinnerSpmCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = 120,
Alpha = 0
}
};
}
@ -157,6 +165,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected override void Update()
{
disc.Tracking = OsuActionInputManager.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton);
if (!spmCounter.IsPresent && disc.Tracking)
spmCounter.FadeIn(TIME_FADEIN);
base.Update();
}
@ -167,6 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
circle.Rotation = disc.Rotation;
ticks.Rotation = disc.Rotation;
spmCounter.SetRotation(disc.RotationAbsolute);
float relativeCircleScale = spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint);

View File

@ -77,7 +77,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private float lastAngle;
private float currentRotation;
public float RotationAbsolute;
private int completeTick;
private bool updateCompleteTick() => completeTick != (completeTick = (int)(RotationAbsolute / 360));

View File

@ -0,0 +1,75 @@
// 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.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class SpinnerSpmCounter : Container
{
private readonly OsuSpriteText spmText;
public SpinnerSpmCounter()
{
Children = new Drawable[]
{
spmText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"0",
Font = @"Venera",
TextSize = 24
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = @"SPINS PER MINUTE",
Font = @"Venera",
TextSize = 12,
Y = 30
}
};
}
private double spm;
public double SpinsPerMinute
{
get { return spm; }
private set
{
if (value == spm) return;
spm = value;
spmText.Text = Math.Truncate(value).ToString(@"#0");
}
}
private struct RotationRecord
{
public float Rotation;
public double Time;
}
private readonly Queue<RotationRecord> records = new Queue<RotationRecord>();
private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
public void SetRotation(float currentRotation)
{
if (records.Count > 0)
{
var record = records.Peek();
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
record = records.Dequeue();
SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
}
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
}
}
}

View File

@ -8,11 +8,11 @@ using System.Runtime.InteropServices;
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("osu.Game.Mode.Osu")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyDescription("click the circles. to the beat.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyCompany("ppy Pty Ltd")]
[assembly: AssemblyProduct("osu.Game.Mode.Osu")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyCopyright("ppy Pty Ltd 2007-2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@ -24,15 +24,5 @@ using System.Runtime.InteropServices;
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("c92a607b-1fdd-4954-9f92-03ff547d9080")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
protected override void SimulateAutoplay(Beatmap<OsuHitObject> beatmap)
{
hpDrainRate = beatmap.BeatmapInfo.Difficulty.DrainRate;
hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate;
foreach (var obj in beatmap.HitObjects)
{

View File

@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (autoCursorScale && beatmap.Value != null)
{
// if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier.
scale *= (float)(1 - 0.7 * (1 + beatmap.Value.BeatmapInfo.Difficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY);
scale *= (float)(1 - 0.7 * (1 + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY);
}
cursorContainer.Scale = new Vector2(scale);

View File

@ -91,6 +91,9 @@ namespace osu.Game.Rulesets.Osu.UI
var osuJudgement = (OsuJudgement)judgement;
var osuObject = (OsuHitObject)judgedObject.HitObject;
if (!judgedObject.DisplayJudgement)
return;
DrawableOsuJudgement explosion = new DrawableOsuJudgement(osuJudgement)
{
Origin = Anchor.Centre,

View File

@ -10,6 +10,10 @@
<assemblyIdentity name="SharpCompress" publicKeyToken="afb0a02973931d96" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.18.1.0" newVersion="0.18.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -17,7 +17,7 @@
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\osu.Game\bin\Debug\</OutputPath>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
@ -27,7 +27,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\osu.Game\bin\Release\</OutputPath>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
@ -68,6 +68,7 @@
<Compile Include="Objects\Drawables\DrawableSliderTick.cs" />
<Compile Include="Objects\Drawables\Pieces\RingPiece.cs" />
<Compile Include="Objects\Drawables\Pieces\SpinnerDisc.cs" />
<Compile Include="Objects\Drawables\Pieces\SpinnerSpmCounter.cs" />
<Compile Include="Objects\Drawables\Pieces\SpinnerTicks.cs" />
<Compile Include="Objects\Drawables\Pieces\TrianglesPiece.cs" />
<Compile Include="Objects\Drawables\Pieces\SliderBall.cs" />

View File

@ -51,18 +51,21 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
// Rewrite the beatmap info to add the slider velocity multiplier
BeatmapInfo info = original.BeatmapInfo.DeepClone();
info.Difficulty.SliderMultiplier *= legacy_velocity_multiplier;
info.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier;
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original);
// Post processing step to transform hit objects with the same start time into strong hits
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
if (original.BeatmapInfo.RulesetID == 3)
{
TaikoHitObject first = x.First();
if (x.Skip(1).Any())
first.IsStrong = true;
return first;
}).ToList();
// Post processing step to transform mania hit objects with the same start time into strong hits
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
{
TaikoHitObject first = x.First();
if (x.Skip(1).Any())
first.IsStrong = true;
return first;
}).ToList();
}
return converted;
}
@ -93,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * repeats * legacy_velocity_multiplier;
// The velocity of the taiko hit object - calculated as the velocity of a drum roll
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength;
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength;
// The duration of the taiko hit object
double taikoDuration = distance / taikoVelocity;
@ -103,12 +106,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
speedAdjustedBeatLength *= speedAdjustment;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength;
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.Difficulty.SliderTickRate, taikoDuration / repeats);
double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / repeats);
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
{
@ -151,13 +154,13 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
Samples = obj.Samples,
IsStrong = strong,
Duration = taikoDuration,
TickRate = beatmap.BeatmapInfo.Difficulty.SliderTickRate == 3 ? 3 : 4,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4,
};
}
}
else if (endTimeData != null)
{
double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
yield return new Swell
{

View File

@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
FillMode = FillMode.Fit;
}
public override bool DisplayJudgement => false;
protected override void LoadComplete()
{
base.LoadComplete();

View File

@ -8,11 +8,11 @@ using System.Runtime.InteropServices;
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("osu.Game.Rulesets.Taiko")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyDescription("bash the drum. to the beat.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyCompany("ppy Pty Ltd")]
[assembly: AssemblyProduct("osu.Game.Rulesets.Taiko")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyCopyright("ppy Pty Ltd 2007-2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@ -24,15 +24,5 @@ using System.Runtime.InteropServices;
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("f167e17a-7de6-4af5-b920-a5112296c695")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -71,12 +71,12 @@ namespace osu.Game.Rulesets.Taiko.Scoring
protected override void SimulateAutoplay(Beatmap<TaikoHitObject> beatmap)
{
double hpMultiplierNormal = 1 / (hp_hit_great * beatmap.HitObjects.FindAll(o => o is Hit).Count * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.DrainRate, 0.5, 0.75, 0.98));
double hpMultiplierNormal = 1 / (hp_hit_great * beatmap.HitObjects.FindAll(o => o is Hit).Count * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98));
hpIncreaseTick = hp_hit_tick;
hpIncreaseGreat = hpMultiplierNormal * hp_hit_great;
hpIncreaseGood = hpMultiplierNormal * hp_hit_good;
hpIncreaseMiss = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.Difficulty.DrainRate, hp_miss_min, hp_miss_mid, hp_miss_max);
hpIncreaseMiss = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, hp_miss_min, hp_miss_mid, hp_miss_max);
foreach (var obj in beatmap.HitObjects)
{

View File

@ -67,12 +67,12 @@ namespace osu.Game.Rulesets.Taiko.Tests
HitObjects = new List<HitObject> { new CentreHit() },
BeatmapInfo = new BeatmapInfo
{
Difficulty = new BeatmapDifficulty(),
BaseDifficulty = new BeatmapDifficulty(),
Metadata = new BeatmapMetadata
{
Artist = @"Unknown",
Title = @"Sample Beatmap",
Author = @"peppy",
AuthorString = @"peppy",
},
},
ControlPointInfo = controlPointInfo

View File

@ -221,7 +221,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override void OnJudgement(DrawableHitObject judgedObject, Judgement judgement)
{
if (judgementContainer.FirstOrDefault(j => j.JudgedObject == judgedObject) == null)
if (judgedObject.DisplayJudgement && judgementContainer.FirstOrDefault(j => j.JudgedObject == judgedObject) == null)
{
judgementContainer.Add(new DrawableTaikoJudgement(judgedObject, judgement)
{

View File

@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Taiko.UI
StartTime = time,
};
barLine.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.Difficulty);
barLine.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty);
bool isMajor = currentBeat % (int)currentPoint.TimeSignature == 0;
Playfield.Add(isMajor ? new DrawableBarLineMajor(barLine) : new DrawableBarLine(barLine));

View File

@ -10,6 +10,10 @@
<assemblyIdentity name="SharpCompress" publicKeyToken="afb0a02973931d96" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-0.18.1.0" newVersion="0.18.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -16,7 +16,7 @@
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\osu.Game\bin\Debug\</OutputPath>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
@ -26,7 +26,7 @@
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\osu.Game\bin\Release\</OutputPath>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.AreEqual("Soleily", meta.Artist);
Assert.AreEqual("Soleily", meta.ArtistUnicode);
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile);
Assert.AreEqual("Gamu", meta.Author);
Assert.AreEqual("Gamu", meta.AuthorString);
Assert.AreEqual("machinetop_background.jpg", meta.BackgroundFile);
Assert.AreEqual(164471, meta.PreviewTime);
Assert.AreEqual(string.Empty, meta.Source);
@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var stream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
{
var beatmap = decoder.Decode(new StreamReader(stream));
var difficulty = beatmap.BeatmapInfo.Difficulty;
var difficulty = beatmap.BeatmapInfo.BaseDifficulty;
Assert.AreEqual(6.5f, difficulty.DrainRate);
Assert.AreEqual(4, difficulty.CircleSize);
Assert.AreEqual(8, difficulty.OverallDifficulty);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public void TestImportWhenClosed()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new HeadlessGameHost())
using (HeadlessGameHost host = new HeadlessGameHost("TestImportWhenClosed"))
{
var osu = loadOsu(host);
@ -36,7 +36,7 @@ namespace osu.Game.Tests.Beatmaps.IO
ensureLoaded(osu);
Assert.IsFalse(File.Exists(temp));
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
}
}
@ -61,15 +61,14 @@ namespace osu.Game.Tests.Beatmaps.IO
ensureLoaded(osu);
Assert.IsFalse(File.Exists(temp));
waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000);
}
}
[Test]
public void TestImportWhenFileOpen()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new HeadlessGameHost())
using (HeadlessGameHost host = new HeadlessGameHost("TestImportWhenFileOpen"))
{
var osu = loadOsu(host);
@ -96,13 +95,10 @@ namespace osu.Game.Tests.Beatmaps.IO
private OsuGameBase loadOsu(GameHost host)
{
host.Storage.DeleteDatabase(@"client");
var osu = new OsuGameBase();
Task.Run(() => host.Run(osu));
while (!osu.IsLoaded)
Thread.Sleep(1);
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
@ -113,37 +109,33 @@ namespace osu.Game.Tests.Beatmaps.IO
var store = osu.Dependencies.Get<BeatmapManager>();
Action waitAction = () =>
{
while (!(resultSets = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526)).Any())
Thread.Sleep(50);
};
Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(timeout),
@"BeatmapSet did not import to the database in allocated time.");
waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526)).Any(),
@"BeatmapSet did not import to the database in allocated time.", timeout);
//ensure we were stored to beatmap database backing...
Assert.IsTrue(resultSets.Count() == 1, $@"Incorrect result count found ({resultSets.Count()} but should be 1).");
IEnumerable<BeatmapInfo> resultBeatmaps = null;
Func<IEnumerable<BeatmapInfo>> queryBeatmaps = () => store.QueryBeatmaps(s => s.BeatmapSet.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0);
Func<IEnumerable<BeatmapSetInfo>> queryBeatmapSets = () => store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526);
//if we don't re-check here, the set will be inserted but the beatmaps won't be present yet.
waitAction = () =>
{
while ((resultBeatmaps = store.QueryBeatmaps(s => s.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0)).Count() != 12)
Thread.Sleep(50);
};
waitForOrAssert(() => queryBeatmaps().Count() == 12,
@"Beatmaps did not import to the database in allocated time", timeout);
Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(timeout),
@"Beatmaps did not import to the database in allocated time");
waitForOrAssert(() => queryBeatmapSets().Count() == 1,
@"BeatmapSet did not import to the database in allocated time", timeout);
var set = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526).First();
int countBeatmapSetBeatmaps = 0;
int countBeatmaps = 0;
Assert.IsTrue(set.Beatmaps.Count == resultBeatmaps.Count(),
$@"Incorrect database beatmap count post-import ({resultBeatmaps.Count()} but should be {set.Beatmaps.Count}).");
waitForOrAssert(() =>
(countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) ==
(countBeatmaps = queryBeatmaps().Count()),
$@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout);
foreach (BeatmapInfo b in resultBeatmaps)
var set = queryBeatmapSets().First();
foreach (BeatmapInfo b in set.Beatmaps)
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineBeatmapID == b.OnlineBeatmapID));
Assert.IsTrue(set.Beatmaps.Count > 0);
@ -160,5 +152,11 @@ namespace osu.Game.Tests.Beatmaps.IO
beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap;
Assert.IsTrue(beatmap?.HitObjects.Count > 0);
}
private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
{
Action waitAction = () => { while (!result()) Thread.Sleep(200); };
Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(timeout), failureMessage);
}
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Tests.Beatmaps.IO
Assert.AreEqual("Soleily", meta.Artist);
Assert.AreEqual("Soleily", meta.ArtistUnicode);
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile);
Assert.AreEqual("Deif", meta.Author);
Assert.AreEqual("Deif", meta.AuthorString);
Assert.AreEqual("machinetop_background.jpg", meta.BackgroundFile);
Assert.AreEqual(164471, meta.PreviewTime);
Assert.AreEqual(string.Empty, meta.Source);

View File

@ -6,6 +6,10 @@
<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="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -39,14 +39,9 @@
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="SQLite.Net">
<HintPath>$(SolutionDir)\packages\SQLite.Net.Core-PCL.3.1.1\lib\portable-win8+net45+wp8+wpa81+MonoAndroid1+MonoTouch1\SQLite.Net.dll</HintPath>
</Reference>
<Reference Include="SQLite.Net.Platform.Win32">
<HintPath>$(SolutionDir)\packages\SQLite.Net-PCL.3.1.1\lib\net4\SQLite.Net.Platform.Win32.dll</HintPath>
</Reference>
<Reference Include="SQLite.Net.Platform.Generic">
<HintPath>$(SolutionDir)\packages\SQLite.Net-PCL.3.1.1\lib\net40\SQLite.Net.Platform.Generic.dll</HintPath>
<Reference Include="System.ValueTuple, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51">
<HintPath>$(SolutionDir)\packages\System.ValueTuple.4.4.0\lib\net461\System.ValueTuple.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>

View File

@ -6,6 +6,5 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/maste
<packages>
<package id="NUnit" version="3.8.1" targetFramework="net461" />
<package id="OpenTK" version="3.0.0-git00009" targetFramework="net461" />
<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="System.ValueTuple" version="4.4.0" targetFramework="net461" />
</packages>

View File

@ -58,6 +58,23 @@ namespace osu.Game.Beatmaps
ComboColors = original?.ComboColors ?? ComboColors;
HitObjects = original?.HitObjects ?? HitObjects;
Storyboard = original?.Storyboard ?? Storyboard;
if (original == null && Metadata == null)
{
// we may have no metadata in cases we weren't sourced from the database.
// let's fill it (and other related fields) so we don't need to null-check it in future usages.
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Artist = @"Unknown",
Title = @"Unknown",
AuthorString = @"Unknown Creator",
},
Version = @"Normal",
BaseDifficulty = new BeatmapDifficulty()
};
}
}
}

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 SQLite.Net.Attributes;
using System.ComponentModel.DataAnnotations.Schema;
namespace osu.Game.Beatmaps
{
@ -12,8 +12,9 @@ namespace osu.Game.Beatmaps
/// </summary>
public const float DEFAULT_DIFFICULTY = 5;
[PrimaryKey, AutoIncrement]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public float DrainRate { get; set; } = DEFAULT_DIFFICULTY;
public float CircleSize { get; set; } = DEFAULT_DIFFICULTY;
public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY;

View File

@ -2,49 +2,45 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets;
using SQLite.Net.Attributes;
using SQLiteNetExtensions.Attributes;
namespace osu.Game.Beatmaps
{
public class BeatmapInfo : IEquatable<BeatmapInfo>, IJsonSerializable
{
[PrimaryKey, AutoIncrement]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
//TODO: should be in database
public int BeatmapVersion;
[JsonProperty("id")]
public int? OnlineBeatmapID { get; set; }
[JsonProperty("beatmapset_id")]
[NotMapped]
public int? OnlineBeatmapSetID { get; set; }
[ForeignKey(typeof(BeatmapSetInfo))]
public int BeatmapSetInfoID { get; set; }
[ManyToOne]
[Required]
public BeatmapSetInfo BeatmapSet { get; set; }
[ForeignKey(typeof(BeatmapMetadata))]
public int BeatmapMetadataID { get; set; }
[OneToOne(CascadeOperations = CascadeOperation.All)]
public BeatmapMetadata Metadata { get; set; }
[ForeignKey(typeof(BeatmapDifficulty)), NotNull]
public int BaseDifficultyID { get; set; }
[OneToOne(CascadeOperations = CascadeOperation.All)]
public BeatmapDifficulty Difficulty { get; set; }
public BeatmapDifficulty BaseDifficulty { get; set; }
[Ignore]
[NotMapped]
public BeatmapMetrics Metrics { get; set; }
[Ignore]
[NotMapped]
public BeatmapOnlineInfo OnlineInfo { get; set; }
public string Path { get; set; }
@ -57,7 +53,6 @@ namespace osu.Game.Beatmaps
/// <summary>
/// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.).
/// </summary>
[Indexed]
[JsonProperty("file_md5")]
public string MD5Hash { get; set; }
@ -67,10 +62,8 @@ namespace osu.Game.Beatmaps
public float StackLeniency { get; set; }
public bool SpecialStyle { get; set; }
[ForeignKey(typeof(RulesetInfo))]
public int RulesetID { get; set; }
[OneToOne(CascadeOperations = CascadeOperation.CascadeRead)]
public RulesetInfo Ruleset { get; set; }
public bool LetterboxInBreaks { get; set; }
@ -99,7 +92,7 @@ namespace osu.Game.Beatmaps
}
}
[Ignore]
[NotMapped]
public int[] Bookmarks { get; set; } = new int[0];
public double DistanceSpacing { get; set; }

View File

@ -5,8 +5,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Ionic.Zip;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
@ -15,14 +16,13 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.IO;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.IPC;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using SQLite.Net;
using osu.Game.Online.API.Requests;
using System.Threading.Tasks;
using osu.Game.Online.API;
namespace osu.Game.Beatmaps
{
@ -58,9 +58,19 @@ namespace osu.Game.Beatmaps
private readonly Storage storage;
private readonly FileStore files;
private BeatmapStore createBeatmapStore(Func<OsuDbContext> context)
{
var store = new BeatmapStore(context);
store.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s);
store.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s);
store.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
store.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
return store;
}
private readonly SQLiteConnection connection;
private readonly Func<OsuDbContext> createContext;
private readonly FileStore files;
private readonly RulesetStore rulesets;
@ -83,22 +93,27 @@ namespace osu.Game.Beatmaps
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
public BeatmapManager(Storage storage, FileStore files, SQLiteConnection connection, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
public BeatmapManager(Storage storage, Func<OsuDbContext> context, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null)
{
beatmaps = new BeatmapStore(connection);
beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s);
beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s);
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
createContext = context;
importContext = new Lazy<OsuDbContext>(() =>
{
var c = createContext();
c.Database.AutoTransactionsEnabled = false;
return c;
});
this.storage = storage;
this.files = files;
this.connection = connection;
beatmaps = createBeatmapStore(context);
files = new FileStore(context, storage);
this.storage = files.Storage;
this.rulesets = rulesets;
this.api = api;
if (importHost != null)
ipc = new BeatmapIPCChannel(importHost, this);
beatmaps.Cleanup();
}
/// <summary>
@ -156,7 +171,7 @@ namespace osu.Game.Beatmaps
notification.State = ProgressNotificationState.Completed;
}
private readonly object importLock = new object();
private readonly Lazy<OsuDbContext> importContext;
/// <summary>
/// Import a beatmap from an <see cref="ArchiveReader"/>.
@ -164,13 +179,29 @@ namespace osu.Game.Beatmaps
/// <param name="archiveReader">The beatmap to be imported.</param>
public BeatmapSetInfo Import(ArchiveReader archiveReader)
{
BeatmapSetInfo set = null;
// let's only allow one concurrent import at a time for now.
lock (importLock)
connection.RunInTransaction(() => Import(set = importToStorage(archiveReader)));
lock (importContext)
{
var context = importContext.Value;
return set;
using (var transaction = context.Database.BeginTransaction())
{
// create local stores so we can isolate and thread safely, and share a context/transaction.
var iFiles = new FileStore(() => context, storage);
var iBeatmaps = createBeatmapStore(() => context);
BeatmapSetInfo set = importToStorage(iFiles, iBeatmaps, archiveReader);
if (set.ID == 0)
{
iBeatmaps.Add(set);
context.SaveChanges();
}
transaction.Commit();
return set;
}
}
}
/// <summary>
@ -182,7 +213,7 @@ namespace osu.Game.Beatmaps
// If we have an ID then we already exist in the database.
if (beatmapSetInfo.ID != 0) return;
beatmaps.Add(beatmapSetInfo);
createBeatmapStore(createContext).Add(beatmapSetInfo);
}
/// <summary>
@ -260,10 +291,33 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapSet">The beatmap set to delete.</param>
public void Delete(BeatmapSetInfo beatmapSet)
{
if (!beatmaps.Delete(beatmapSet)) return;
lock (importContext)
{
var context = importContext.Value;
if (!beatmapSet.Protected)
files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
using (var transaction = context.Database.BeginTransaction())
{
context.ChangeTracker.AutoDetectChangesEnabled = false;
// re-fetch the beatmap set on the import context.
beatmapSet = context.BeatmapSetInfo.Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == beatmapSet.ID);
// create local stores so we can isolate and thread safely, and share a context/transaction.
var iFiles = new FileStore(() => context, storage);
var iBeatmaps = createBeatmapStore(() => context);
if (iBeatmaps.Delete(beatmapSet))
{
if (!beatmapSet.Protected)
iFiles.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray());
}
context.ChangeTracker.AutoDetectChangesEnabled = true;
context.SaveChanges();
transaction.Commit();
}
}
}
/// <summary>
@ -283,7 +337,7 @@ namespace osu.Game.Beatmaps
/// Is a no-op for already usable beatmaps.
/// </summary>
/// <param name="beatmapSet">The beatmap to restore.</param>
public void Undelete(BeatmapSetInfo beatmapSet)
private void undelete(BeatmapStore beatmaps, FileStore files, BeatmapSetInfo beatmapSet)
{
if (!beatmaps.Undelete(beatmapSet)) return;
@ -302,9 +356,6 @@ namespace osu.Game.Beatmaps
if (beatmapInfo == null || beatmapInfo == DefaultBeatmap?.BeatmapInfo)
return DefaultBeatmap;
lock (beatmaps)
beatmaps.Populate(beatmapInfo);
if (beatmapInfo.BeatmapSet == null)
throw new InvalidOperationException($@"Beatmap set {beatmapInfo.BeatmapSetInfoID} is not in the local database.");
@ -318,32 +369,12 @@ namespace osu.Game.Beatmaps
return working;
}
/// <summary>
/// Reset the manager to an empty state.
/// </summary>
public void Reset()
{
lock (beatmaps)
beatmaps.Reset();
}
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapSetInfo QueryBeatmapSet(Func<BeatmapSetInfo, bool> query)
{
lock (beatmaps)
{
BeatmapSetInfo set = beatmaps.Query<BeatmapSetInfo>().FirstOrDefault(query);
if (set != null)
beatmaps.Populate(set);
return set;
}
}
public BeatmapSetInfo QueryBeatmapSet(Func<BeatmapSetInfo, bool> query) => beatmaps.BeatmapSets.FirstOrDefault(query);
/// <summary>
/// Refresh an existing instance of a <see cref="BeatmapSetInfo"/> from the store.
@ -357,35 +388,21 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="query">The query.</param>
/// <returns>Results from the provided query.</returns>
public List<BeatmapSetInfo> QueryBeatmapSets(Expression<Func<BeatmapSetInfo, bool>> query)
{
return beatmaps.QueryAndPopulate(query);
}
public List<BeatmapSetInfo> QueryBeatmapSets(Func<BeatmapSetInfo, bool> query) => beatmaps.BeatmapSets.Where(query).ToList();
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapInfo QueryBeatmap(Func<BeatmapInfo, bool> query)
{
BeatmapInfo set = beatmaps.Query<BeatmapInfo>().FirstOrDefault(query);
if (set != null)
beatmaps.Populate(set);
return set;
}
public BeatmapInfo QueryBeatmap(Func<BeatmapInfo, bool> query) => beatmaps.Beatmaps.FirstOrDefault(query);
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>Results from the provided query.</returns>
public List<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query)
{
lock (beatmaps) return beatmaps.QueryAndPopulate(query);
}
public List<BeatmapInfo> QueryBeatmaps(Func<BeatmapInfo, bool> query) => beatmaps.Beatmaps.Where(query).ToList();
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
@ -395,9 +412,9 @@ namespace osu.Game.Beatmaps
private ArchiveReader getReaderFrom(string path)
{
if (ZipFile.IsZipFile(path))
// ReSharper disable once InconsistentlySynchronizedField
return new OszArchiveReader(storage.GetStream(path));
else
return new LegacyFilesystemReader(path);
return new LegacyFilesystemReader(path);
}
/// <summary>
@ -406,7 +423,7 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="reader">The beatmap archive to be read.</param>
/// <returns>The imported beatmap, or an existing instance if it is already present.</returns>
private BeatmapSetInfo importToStorage(ArchiveReader reader)
private BeatmapSetInfo importToStorage(FileStore files, BeatmapStore beatmaps, ArchiveReader reader)
{
// let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
@ -422,13 +439,11 @@ namespace osu.Game.Beatmaps
var hash = hashable.ComputeSHA2Hash();
// check if this beatmap has already been imported and exit early if so.
BeatmapSetInfo beatmapSet;
lock (beatmaps)
beatmapSet = beatmaps.QueryAndPopulate<BeatmapSetInfo>(b => b.Hash == hash).FirstOrDefault();
var beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == hash);
if (beatmapSet != null)
{
Undelete(beatmapSet);
undelete(beatmaps, files, beatmapSet);
// ensure all files are present and accessible
foreach (var f in beatmapSet.Files)
@ -438,6 +453,8 @@ namespace osu.Game.Beatmaps
files.Add(s, false);
}
// todo: delete any files which shouldn't exist any more.
return beatmapSet;
}
@ -487,10 +504,11 @@ namespace osu.Game.Beatmaps
// TODO: Diff beatmap metadata with set metadata and leave it here if necessary
beatmap.BeatmapInfo.Metadata = null;
RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID);
// TODO: this should be done in a better place once we actually need to dynamically update it.
beatmap.BeatmapInfo.Ruleset = rulesets.Query<RulesetInfo>().FirstOrDefault(r => r.ID == beatmap.BeatmapInfo.RulesetID);
beatmap.BeatmapInfo.StarDifficulty = rulesets.Query<RulesetInfo>().FirstOrDefault(r => r.ID == beatmap.BeatmapInfo.RulesetID)?.CreateInstance()?.CreateDifficultyCalculator(beatmap)
.Calculate() ?? 0;
beatmap.BeatmapInfo.Ruleset = ruleset;
beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0;
beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo);
}
@ -502,17 +520,10 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// </summary>
/// <param name="populate">Whether returned objects should be pre-populated with all data.</param>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public List<BeatmapSetInfo> GetAllUsableBeatmapSets(bool populate = true)
public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
{
lock (beatmaps)
{
if (populate)
return beatmaps.QueryAndPopulate<BeatmapSetInfo>(b => !b.DeletePending).ToList();
else
return beatmaps.Query<BeatmapSetInfo>(b => !b.DeletePending).ToList();
}
return beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList();
}
protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap
@ -547,10 +558,13 @@ namespace osu.Game.Beatmaps
return beatmap;
}
catch { return null; }
catch
{
return null;
}
}
private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => f.Filename == filename).FileInfo.StoragePath;
private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath;
protected override Texture GetBackground()
{
@ -561,7 +575,10 @@ namespace osu.Game.Beatmaps
{
return new TextureStore(new RawTextureLoaderStore(store), false).Get(getPathForFile(Metadata.BackgroundFile));
}
catch { return null; }
catch
{
return null;
}
}
protected override Track GetTrack()
@ -571,8 +588,13 @@ namespace osu.Game.Beatmaps
var trackData = store.GetStream(getPathForFile(Metadata.AudioFile));
return trackData == null ? null : new TrackBass(trackData);
}
catch { return new TrackVirtual(); }
catch
{
return new TrackVirtual();
}
}
protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile)));
}
/// <summary>
@ -593,9 +615,9 @@ namespace osu.Game.Beatmaps
public void DeleteAll()
{
var maps = GetAllUsableBeatmapSets().ToArray();
var maps = GetAllUsableBeatmapSets();
if (maps.Length == 0) return;
if (maps.Count == 0) return;
var notification = new ProgressNotification
{
@ -613,8 +635,8 @@ namespace osu.Game.Beatmaps
// user requested abort
return;
notification.Text = $"Deleting ({i} of {maps.Length})";
notification.Progress = (float)++i / maps.Length;
notification.Text = $"Deleting ({i} of {maps.Count})";
notification.Progress = (float)++i / maps.Count;
Delete(b);
}

View File

@ -1,17 +1,20 @@
// 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 System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using SQLite.Net.Attributes;
using osu.Game.Users;
namespace osu.Game.Beatmaps
{
public class BeatmapMetadata
{
[PrimaryKey, AutoIncrement]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[NotMapped]
public int? OnlineBeatmapSetID { get; set; }
public string Title { get; set; }
@ -19,8 +22,24 @@ namespace osu.Game.Beatmaps
public string Artist { get; set; }
public string ArtistUnicode { get; set; }
public List<BeatmapInfo> Beatmaps { get; set; }
public List<BeatmapSetInfo> BeatmapSets { get; set; }
/// <summary>
/// Helper property to deserialize a username to <see cref="User"/>.
/// </summary>
[JsonProperty(@"creator")]
public string Author { get; set; }
[Column("Author")]
public string AuthorString
{
get { return Author?.Username; }
set { Author = new User { Username = value }; }
}
/// <summary>
/// The author of the beatmaps in this set.
/// </summary>
public User Author;
public string Source { get; set; }
@ -32,7 +51,7 @@ namespace osu.Game.Beatmaps
public string[] SearchableTerms => new[]
{
Author,
Author?.Username,
Artist,
ArtistUnicode,
Title,

View File

@ -1,27 +1,24 @@
// 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.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using osu.Game.IO;
using SQLite.Net.Attributes;
using SQLiteNetExtensions.Attributes;
namespace osu.Game.Beatmaps
{
public class BeatmapSetFileInfo
{
[PrimaryKey, AutoIncrement]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[ForeignKey(typeof(BeatmapSetInfo)), NotNull]
public int BeatmapSetInfoID { get; set; }
[ForeignKey(typeof(FileInfo)), NotNull]
public int FileInfoID { get; set; }
[OneToOne(CascadeOperations = CascadeOperation.CascadeRead)]
public FileInfo FileInfo { get; set; }
[NotNull]
[Required]
public string Filename { get; set; }
}
}

View File

@ -2,41 +2,34 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using SQLite.Net.Attributes;
using SQLiteNetExtensions.Attributes;
namespace osu.Game.Beatmaps
{
public class BeatmapSetInfo
{
[PrimaryKey, AutoIncrement]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public int? OnlineBeatmapSetID { get; set; }
[OneToOne(CascadeOperations = CascadeOperation.All)]
public BeatmapMetadata Metadata { get; set; }
[NotNull, ForeignKey(typeof(BeatmapMetadata))]
public int BeatmapMetadataID { get; set; }
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<BeatmapInfo> Beatmaps { get; set; }
[Ignore]
[NotMapped]
public BeatmapSetOnlineInfo OnlineInfo { get; set; }
public double MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty);
[Indexed]
[NotMapped]
public bool DeletePending { get; set; }
public string Hash { get; set; }
public string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(".osb"))?.Filename;
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<BeatmapSetFileInfo> Files { get; set; }
public bool Protected { get; set; }

View File

@ -3,7 +3,6 @@
using System;
using Newtonsoft.Json;
using osu.Game.Users;
namespace osu.Game.Beatmaps
{
@ -12,11 +11,6 @@ namespace osu.Game.Beatmaps
/// </summary>
public class BeatmapSetOnlineInfo
{
/// <summary>
/// The author of the beatmaps in this set.
/// </summary>
public User Author;
/// <summary>
/// The date this beatmap set was submitted to the online listing.
/// </summary>

View File

@ -2,9 +2,10 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using osu.Game.Database;
using SQLite.Net;
using SQLiteNetExtensions.Extensions;
namespace osu.Game.Beatmaps
{
@ -19,76 +20,23 @@ namespace osu.Game.Beatmaps
public event Action<BeatmapInfo> BeatmapHidden;
public event Action<BeatmapInfo> BeatmapRestored;
/// <summary>
/// The current version of this store. Used for migrations (see <see cref="PerformMigration(int, int)"/>).
/// The initial version is 1.
/// </summary>
protected override int StoreVersion => 4;
public BeatmapStore(SQLiteConnection connection)
: base(connection)
public BeatmapStore(Func<OsuDbContext> factory)
: base(factory)
{
}
protected override Type[] ValidTypes => new[]
{
typeof(BeatmapSetInfo),
typeof(BeatmapInfo),
typeof(BeatmapMetadata),
typeof(BeatmapDifficulty),
};
protected override void Prepare(bool reset = false)
{
if (reset)
{
Connection.DropTable<BeatmapMetadata>();
Connection.DropTable<BeatmapDifficulty>();
Connection.DropTable<BeatmapSetInfo>();
Connection.DropTable<BeatmapSetFileInfo>();
Connection.DropTable<BeatmapInfo>();
}
var context = GetContext();
Connection.CreateTable<BeatmapMetadata>();
Connection.CreateTable<BeatmapDifficulty>();
Connection.CreateTable<BeatmapSetInfo>();
Connection.CreateTable<BeatmapSetFileInfo>();
Connection.CreateTable<BeatmapInfo>();
}
protected override void StartupTasks()
{
base.StartupTasks();
cleanupPendingDeletions();
}
/// <summary>
/// Perform migrations between two store versions.
/// </summary>
/// <param name="currentVersion">The current store version. This will be zero on a fresh database initialisation.</param>
/// <param name="targetVersion">The target version which we are migrating to (equal to the current <see cref="StoreVersion"/>).</param>
protected override void PerformMigration(int currentVersion, int targetVersion)
{
base.PerformMigration(currentVersion, targetVersion);
while (currentVersion++ < targetVersion)
{
switch (currentVersion)
{
case 1:
case 2:
// cannot migrate; breaking underlying changes.
Reset();
break;
case 3:
// Added MD5Hash column to BeatmapInfo
Connection.MigrateTable<BeatmapInfo>();
break;
case 4:
// Added Hidden column to BeatmapInfo
Connection.MigrateTable<BeatmapInfo>();
break;
}
// https://stackoverflow.com/a/10450893
context.Database.ExecuteSqlCommand("DELETE FROM BeatmapMetadata");
context.Database.ExecuteSqlCommand("DELETE FROM BeatmapDifficulty");
context.Database.ExecuteSqlCommand("DELETE FROM BeatmapSetInfo");
context.Database.ExecuteSqlCommand("DELETE FROM BeatmapSetFileInfo");
context.Database.ExecuteSqlCommand("DELETE FROM BeatmapInfo");
}
}
@ -98,10 +46,10 @@ namespace osu.Game.Beatmaps
/// <param name="beatmapSet">The beatmap to add.</param>
public void Add(BeatmapSetInfo beatmapSet)
{
Connection.RunInTransaction(() =>
{
Connection.InsertOrReplaceWithChildren(beatmapSet, true);
});
var context = GetContext();
context.BeatmapSetInfo.Attach(beatmapSet);
context.SaveChanges();
BeatmapSetAdded?.Invoke(beatmapSet);
}
@ -113,10 +61,13 @@ namespace osu.Game.Beatmaps
/// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns>
public bool Delete(BeatmapSetInfo beatmapSet)
{
var context = GetContext();
if (beatmapSet.DeletePending) return false;
beatmapSet.DeletePending = true;
Connection.Update(beatmapSet);
context.Update(beatmapSet);
context.SaveChanges();
BeatmapSetRemoved?.Invoke(beatmapSet);
return true;
@ -129,10 +80,13 @@ namespace osu.Game.Beatmaps
/// <returns>Whether the beatmap's <see cref="BeatmapSetInfo.DeletePending"/> was changed.</returns>
public bool Undelete(BeatmapSetInfo beatmapSet)
{
var context = GetContext();
if (!beatmapSet.DeletePending) return false;
beatmapSet.DeletePending = false;
Connection.Update(beatmapSet);
context.Update(beatmapSet);
context.SaveChanges();
BeatmapSetAdded?.Invoke(beatmapSet);
return true;
@ -145,10 +99,13 @@ namespace osu.Game.Beatmaps
/// <returns>Whether the beatmap's <see cref="BeatmapInfo.Hidden"/> was changed.</returns>
public bool Hide(BeatmapInfo beatmap)
{
var context = GetContext();
if (beatmap.Hidden) return false;
beatmap.Hidden = true;
Connection.Update(beatmap);
context.Update(beatmap);
context.SaveChanges();
BeatmapHidden?.Invoke(beatmap);
return true;
@ -161,22 +118,50 @@ namespace osu.Game.Beatmaps
/// <returns>Whether the beatmap's <see cref="BeatmapInfo.Hidden"/> was changed.</returns>
public bool Restore(BeatmapInfo beatmap)
{
var context = GetContext();
if (!beatmap.Hidden) return false;
beatmap.Hidden = false;
Connection.Update(beatmap);
context.Update(beatmap);
context.SaveChanges();
BeatmapRestored?.Invoke(beatmap);
return true;
}
private void cleanupPendingDeletions()
public override void Cleanup()
{
Connection.RunInTransaction(() =>
{
foreach (var b in QueryAndPopulate<BeatmapSetInfo>(b => b.DeletePending && !b.Protected))
Connection.Delete(b, true);
});
var context = GetContext();
var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected)
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Metadata);
// metadata is M-N so we can't rely on cascades
context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata));
context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null)));
// todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly.
context.BeatmapDifficulty.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty)));
// cascades down to beatmaps.
context.BeatmapSetInfo.RemoveRange(purgeable);
context.SaveChanges();
}
public IEnumerable<BeatmapSetInfo> BeatmapSets => GetContext().BeatmapSetInfo
.Include(s => s.Metadata)
.Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset)
.Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty)
.Include(s => s.Beatmaps).ThenInclude(b => b.Metadata)
.Include(s => s.Files).ThenInclude(f => f.FileInfo);
public IEnumerable<BeatmapInfo> Beatmaps => GetContext().BeatmapInfo
.Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata)
.Include(b => b.Metadata)
.Include(b => b.Ruleset)
.Include(b => b.BaseDifficulty);
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps
Objects = CreateBeatmapConverter().Convert(beatmap).HitObjects;
foreach (var h in Objects)
h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.Difficulty);
h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
PreprocessHitObjects();
}

View File

@ -31,6 +31,8 @@ namespace osu.Game.Beatmaps.Drawables
public Action<BeatmapInfo> HideDifficultyRequested;
public Action<BeatmapInfo> EditRequested;
public BeatmapSetHeader Header;
public List<BeatmapPanel> BeatmapPanels;
@ -81,13 +83,13 @@ namespace osu.Game.Beatmaps.Drawables
RelativeSizeAxes = Axes.X,
};
BeatmapSet.Beatmaps = BeatmapSet.Beatmaps.Where(b => !b.Hidden).OrderBy(b => b.StarDifficulty).ToList();
BeatmapPanels = BeatmapSet.Beatmaps.Select(b => new BeatmapPanel(b)
BeatmapPanels = BeatmapSet.Beatmaps.Where(b => !b.Hidden).OrderBy(b => b.StarDifficulty).Select(b => new BeatmapPanel(b)
{
Alpha = 0,
GainedSelection = panelGainedSelection,
HideRequested = p => HideDifficultyRequested?.Invoke(p),
StartRequested = p => { StartRequested?.Invoke(p.Beatmap); },
StartRequested = p => StartRequested?.Invoke(p.Beatmap),
EditRequested = p => EditRequested?.Invoke(p.Beatmap),
RelativeSizeAxes = Axes.X,
}).ToList();

View File

@ -135,7 +135,7 @@ namespace osu.Game.Beatmaps.Drawables
new OsuSpriteText
{
Font = @"Exo2.0-MediumItalic",
Text = $"{(beatmap.Metadata ?? beatmap.BeatmapSet.Metadata).Author}",
Text = $"{(beatmap.Metadata ?? beatmap.BeatmapSet.Metadata).Author.Username}",
TextSize = 16,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft

View File

@ -22,10 +22,10 @@ namespace osu.Game.Beatmaps
{
Artist = "please load a beatmap!",
Title = "no beatmaps available!",
Author = "no one",
AuthorString = "no one",
},
BeatmapSet = new BeatmapSetInfo(),
Difficulty = new BeatmapDifficulty
BaseDifficulty = new BeatmapDifficulty
{
DrainRate = 0,
CircleSize = 0,

View File

@ -49,7 +49,7 @@ namespace osu.Game.Beatmaps.Formats
BeatmapInfo = new BeatmapInfo
{
Metadata = new BeatmapMetadata(),
Difficulty = new BeatmapDifficulty(),
BaseDifficulty = new BeatmapDifficulty(),
},
};

View File

@ -171,7 +171,7 @@ namespace osu.Game.Beatmaps.Formats
metadata.ArtistUnicode = pair.Value;
break;
case @"Creator":
metadata.Author = pair.Value;
metadata.AuthorString = pair.Value;
break;
case @"Version":
beatmap.BeatmapInfo.Version = pair.Value;
@ -196,7 +196,7 @@ namespace osu.Game.Beatmaps.Formats
{
var pair = splitKeyVal(line, ':');
var difficulty = beatmap.BeatmapInfo.Difficulty;
var difficulty = beatmap.BeatmapInfo.BaseDifficulty;
switch (pair.Key)
{
case @"HPDrainRate":
@ -613,7 +613,7 @@ namespace osu.Game.Beatmaps.Formats
string line;
while ((line = stream.ReadLine()) != null)
{
if (string.IsNullOrEmpty(line))
if (string.IsNullOrWhiteSpace(line))
continue;
if (line.StartsWith("//"))
@ -674,15 +674,17 @@ namespace osu.Game.Beatmaps.Formats
}
foreach (var hitObject in beatmap.HitObjects)
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.Difficulty);
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
}
private KeyValuePair<string, string> splitKeyVal(string line, char separator)
{
var split = line.Trim().Split(new[] { separator }, 2);
return new KeyValuePair<string, string>
(
line.Remove(line.IndexOf(separator)).Trim(),
line.Substring(line.IndexOf(separator) + 1).Trim()
split[0].Trim(),
split.Length > 1 ? split[1].Trim() : string.Empty
);
}

View File

@ -8,7 +8,7 @@ namespace osu.Game.Beatmaps.Timing
/// <summary>
/// The minimum duration required for a break to have any effect.
/// </summary>
private const double min_break_duration = 650;
public const double MIN_BREAK_DURATION = 650;
/// <summary>
/// The break start time.
@ -28,6 +28,6 @@ namespace osu.Game.Beatmaps.Timing
/// <summary>
/// Whether the break has any effect. Breaks that are too short are culled before they are added to the beatmap.
/// </summary>
public bool HasEffect => Duration >= min_break_duration;
public bool HasEffect => Duration >= MIN_BREAK_DURATION;
}
}

View File

@ -43,6 +43,7 @@ namespace osu.Game.Beatmaps
protected abstract Beatmap GetBeatmap();
protected abstract Texture GetBackground();
protected abstract Track GetTrack();
protected virtual Waveform GetWaveform() => new Waveform();
private Beatmap beatmap;
private readonly object beatmapLock = new object();
@ -96,6 +97,17 @@ namespace osu.Game.Beatmaps
}
}
private Waveform waveform;
private readonly object waveformLock = new object();
public Waveform Waveform
{
get
{
lock (waveformLock)
return waveform ?? (waveform = GetWaveform());
}
}
public bool TrackLoaded => track != null;
public void TransferTo(WorkingBeatmap other)
@ -114,6 +126,8 @@ namespace osu.Game.Beatmaps
{
background?.Dispose();
background = null;
waveform?.Dispose();
}
public void DisposeTrack()

View File

@ -2,27 +2,21 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using SQLite.Net;
using SQLiteNetExtensions.Extensions;
namespace osu.Game.Database
{
public abstract class DatabaseBackedStore
{
protected readonly Storage Storage;
protected readonly SQLiteConnection Connection;
protected virtual int StoreVersion => 1;
protected readonly Func<OsuDbContext> GetContext;
protected DatabaseBackedStore(SQLiteConnection connection, Storage storage = null)
protected DatabaseBackedStore(Func<OsuDbContext> getContext, Storage storage = null)
{
Storage = storage;
Connection = connection;
GetContext = getContext;
try
{
@ -33,46 +27,15 @@ namespace osu.Game.Database
Logger.Error(e, $@"Failed to initialise the {GetType()}! Trying again with a clean database...");
Prepare(true);
}
checkMigrations();
}
private void checkMigrations()
{
var storeName = GetType().Name;
var reportedVersion = Connection.Table<StoreVersion>().Where(s => s.StoreName == storeName).FirstOrDefault() ?? new StoreVersion
{
StoreName = storeName,
Version = 0
};
if (reportedVersion.Version != StoreVersion)
PerformMigration(reportedVersion.Version, reportedVersion.Version = StoreVersion);
Connection.InsertOrReplace(reportedVersion);
StartupTasks();
}
/// <summary>
/// Called when the database version of this store doesn't match the local version.
/// Any manual migration operations should be performed in this.
/// Perform any common clean-up tasks. Should be run when idle, or whenever necessary.
/// </summary>
/// <param name="currentVersion">The current store version. This will be zero on a fresh database initialisation.</param>
/// <param name="targetVersion">The target version which we are migrating to (equal to the current <see cref="StoreVersion"/>).</param>
protected virtual void PerformMigration(int currentVersion, int targetVersion)
public virtual void Cleanup()
{
}
/// <summary>
/// Perform any common startup tasks. Runs after <see cref="Prepare(bool)"/> and <see cref="PerformMigration(int, int)"/>.
/// </summary>
protected virtual void StartupTasks()
{
}
/// <summary>
/// Prepare this database for use. Tables should be created here.
/// </summary>
@ -82,50 +45,5 @@ namespace osu.Game.Database
/// Reset this database to a default state. Undo all changes to database and storage backings.
/// </summary>
public void Reset() => Prepare(true);
public TableQuery<T> Query<T>(Expression<Func<T, bool>> filter = null) where T : class
{
checkType(typeof(T));
var query = Connection.Table<T>();
if (filter != null)
query = query.Where(filter);
return query;
}
/// <summary>
/// Query and populate results.
/// </summary>
/// <param name="filter">An filter to refine results.</param>
/// <returns></returns>
public List<T> QueryAndPopulate<T>(Expression<Func<T, bool>> filter)
where T : class
{
checkType(typeof(T));
return Connection.GetAllWithChildren(filter, true);
}
/// <summary>
/// Populate a database-backed item.
/// </summary>
/// <param name="item"></param>
/// <param name="recursive">Whether population should recurse beyond a single level.</param>
public void Populate<T>(T item, bool recursive = true)
{
checkType(item.GetType());
Connection.GetChildren(item, recursive);
}
private void checkType(Type type)
{
if (!ValidTypes.Contains(type))
throw new InvalidOperationException($"The requested operation specified a type of {type}, which is invalid for this {nameof(DatabaseBackedStore)}.");
}
protected abstract Type[] ValidTypes { get; }
}
}

View File

@ -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.Framework.Platform;
namespace osu.Game.Database
{
public class DatabaseContextFactory
{
private readonly GameHost host;
private const string database_name = @"client";
public DatabaseContextFactory(GameHost host)
{
this.host = host;
}
public OsuDbContext GetContext() => new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name));
public void ResetDatabase()
{
// todo: we probably want to make sure there are no active contexts before performing this operation.
host.Storage.DeleteDatabase(database_name);
}
}
}

View File

@ -0,0 +1,265 @@
// 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 Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.IO;
using osu.Game.Rulesets;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace osu.Game.Database
{
public class OsuDbContext : DbContext
{
public DbSet<BeatmapInfo> BeatmapInfo { get; set; }
public DbSet<BeatmapDifficulty> BeatmapDifficulty { get; set; }
public DbSet<BeatmapMetadata> BeatmapMetadata { get; set; }
public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; }
public DbSet<DatabasedKeyBinding> DatabasedKeyBinding { get; set; }
public DbSet<FileInfo> FileInfo { get; set; }
public DbSet<RulesetInfo> RulesetInfo { get; set; }
private readonly string connectionString;
private static readonly Lazy<OsuDbLoggerFactory> logger = new Lazy<OsuDbLoggerFactory>(() => new OsuDbLoggerFactory());
static OsuDbContext()
{
// required to initialise native SQLite libraries on some platforms.
SQLitePCL.Batteries_V2.Init();
}
/// <summary>
/// Create a new in-memory OsuDbContext instance.
/// </summary>
public OsuDbContext()
: this("DataSource=:memory:")
{
// required for tooling (see https://wildermuth.com/2017/07/06/Program-cs-in-ASP-NET-Core-2-0).
}
/// <summary>
/// Create a new OsuDbContext instance.
/// </summary>
/// <param name="connectionString">A valid SQLite connection string.</param>
public OsuDbContext(string connectionString)
{
this.connectionString = connectionString;
Database.SetCommandTimeout(new TimeSpan(TimeSpan.TicksPerSecond * 10));
var connection = Database.GetDbConnection();
connection.Open();
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "PRAGMA journal_mode=WAL;";
cmd.ExecuteNonQuery();
}
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder
// this is required for the time being due to the way we are querying in places like BeatmapStore.
// if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled.
.ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning))
.UseSqlite(connectionString)
.UseLoggerFactory(logger.Value);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.MD5Hash);
modelBuilder.Entity<BeatmapInfo>().HasIndex(b => b.Hash);
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.DeletePending);
modelBuilder.Entity<BeatmapSetInfo>().HasIndex(b => b.Hash);
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => b.Variant);
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => b.IntAction);
modelBuilder.Entity<FileInfo>().HasIndex(b => b.Hash).IsUnique();
modelBuilder.Entity<FileInfo>().HasIndex(b => b.ReferenceCount);
modelBuilder.Entity<RulesetInfo>().HasIndex(b => b.Available);
modelBuilder.Entity<BeatmapInfo>().HasOne(b => b.BaseDifficulty);
}
private class OsuDbLoggerFactory : ILoggerFactory
{
#region Disposal
public void Dispose()
{
}
#endregion
public ILogger CreateLogger(string categoryName) => new OsuDbLogger();
public void AddProvider(ILoggerProvider provider)
{
// no-op. called by tooling.
}
private class OsuDbLoggerProvider : ILoggerProvider
{
#region Disposal
public void Dispose()
{
}
#endregion
public ILogger CreateLogger(string categoryName) => new OsuDbLogger();
}
private class OsuDbLogger : ILogger
{
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (logLevel < LogLevel.Information)
return;
Framework.Logging.LogLevel frameworkLogLevel;
switch (logLevel)
{
default:
frameworkLogLevel = Framework.Logging.LogLevel.Debug;
break;
case LogLevel.Warning:
frameworkLogLevel = Framework.Logging.LogLevel.Important;
break;
case LogLevel.Error:
case LogLevel.Critical:
frameworkLogLevel = Framework.Logging.LogLevel.Error;
break;
}
Logger.Log(formatter(state, exception), LoggingTarget.Database, frameworkLogLevel);
}
public bool IsEnabled(LogLevel logLevel)
{
#if DEBUG
return logLevel > LogLevel.Debug;
#else
return logLevel > LogLevel.Information;
#endif
}
public IDisposable BeginScope<TState>(TState state) => null;
}
}
public void Migrate()
{
migrateFromSqliteNet();
try
{
Database.Migrate();
}
catch (Exception e)
{
throw new MigrationFailedException(e);
}
}
private void migrateFromSqliteNet()
{
try
{
// will fail if EF hasn't touched the database yet.
Database.ExecuteSqlCommand("SELECT * FROM __EFMigrationsHistory LIMIT 1");
}
catch
{
try
{
// will fail (intentionally) if we don't have sqlite-net data present.
Database.ExecuteSqlCommand("SELECT OnlineBeatmapSetId FROM BeatmapMetadata LIMIT 1");
try
{
Logger.Log("Performing migration from sqlite-net to EF...", LoggingTarget.Database, Framework.Logging.LogLevel.Important);
// we are good to perform messy migration of data!.
Database.ExecuteSqlCommand("ALTER TABLE BeatmapDifficulty RENAME TO BeatmapDifficulty_Old");
Database.ExecuteSqlCommand("ALTER TABLE BeatmapMetadata RENAME TO BeatmapMetadata_Old");
Database.ExecuteSqlCommand("ALTER TABLE FileInfo RENAME TO FileInfo_Old");
Database.ExecuteSqlCommand("ALTER TABLE KeyBinding RENAME TO KeyBinding_Old");
Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetInfo RENAME TO BeatmapSetInfo_Old");
Database.ExecuteSqlCommand("ALTER TABLE BeatmapInfo RENAME TO BeatmapInfo_Old");
Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetFileInfo RENAME TO BeatmapSetFileInfo_Old");
Database.ExecuteSqlCommand("ALTER TABLE RulesetInfo RENAME TO RulesetInfo_Old");
Database.ExecuteSqlCommand("DROP TABLE StoreVersion");
// perform EF migrations to create sane table structure.
Database.Migrate();
// copy data table by table to new structure, dropping old tables as we go.
Database.ExecuteSqlCommand("INSERT INTO FileInfo SELECT * FROM FileInfo_Old");
Database.ExecuteSqlCommand("DROP TABLE FileInfo_Old");
Database.ExecuteSqlCommand("INSERT INTO KeyBinding SELECT ID, [Action], Keys, RulesetID, Variant FROM KeyBinding_Old");
Database.ExecuteSqlCommand("DROP TABLE KeyBinding_Old");
Database.ExecuteSqlCommand(
"INSERT INTO BeatmapMetadata SELECT ID, Artist, ArtistUnicode, AudioFile, Author, BackgroundFile, PreviewTime, Source, Tags, Title, TitleUnicode FROM BeatmapMetadata_Old");
Database.ExecuteSqlCommand("DROP TABLE BeatmapMetadata_Old");
Database.ExecuteSqlCommand(
"INSERT INTO BeatmapDifficulty SELECT `ID`, `ApproachRate`, `CircleSize`, `DrainRate`, `OverallDifficulty`, `SliderMultiplier`, `SliderTickRate` FROM BeatmapDifficulty_Old");
Database.ExecuteSqlCommand("DROP TABLE BeatmapDifficulty_Old");
Database.ExecuteSqlCommand("INSERT INTO BeatmapSetInfo SELECT ID, DeletePending, Hash, BeatmapMetadataID, OnlineBeatmapSetID, Protected FROM BeatmapSetInfo_Old");
Database.ExecuteSqlCommand("DROP TABLE BeatmapSetInfo_Old");
Database.ExecuteSqlCommand("INSERT INTO BeatmapSetFileInfo SELECT ID, BeatmapSetInfoID, FileInfoID, Filename FROM BeatmapSetFileInfo_Old");
Database.ExecuteSqlCommand("DROP TABLE BeatmapSetFileInfo_Old");
Database.ExecuteSqlCommand("INSERT INTO RulesetInfo SELECT ID, Available, InstantiationInfo, Name FROM RulesetInfo_Old");
Database.ExecuteSqlCommand("DROP TABLE RulesetInfo_Old");
Database.ExecuteSqlCommand(
"INSERT INTO BeatmapInfo SELECT ID, AudioLeadIn, BaseDifficultyID, BeatDivisor, BeatmapSetInfoID, Countdown, DistanceSpacing, GridSize, Hash, IFNULL(Hidden, 0), LetterboxInBreaks, MD5Hash, NULLIF(BeatmapMetadataID, 0), OnlineBeatmapID, Path, RulesetID, SpecialStyle, StackLeniency, StarDifficulty, StoredBookmarks, TimelineZoom, Version, WidescreenStoryboard FROM BeatmapInfo_Old");
Database.ExecuteSqlCommand("DROP TABLE BeatmapInfo_Old");
Logger.Log("Migration complete!", LoggingTarget.Database, Framework.Logging.LogLevel.Important);
}
catch (Exception e)
{
throw new MigrationFailedException(e);
}
}
catch (MigrationFailedException e)
{
throw;
}
catch
{
}
}
}
}
public class MigrationFailedException : Exception
{
public MigrationFailedException(Exception exception)
: base("sqlite-net migration failed", exception)
{
}
}
}

View File

@ -1,15 +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 SQLite.Net.Attributes;
namespace osu.Game.Database
{
public class StoreVersion
{
[PrimaryKey]
public string StoreName { get; set; }
public int Version { get; set; }
}
}

View File

@ -0,0 +1,33 @@
// 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.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input;
namespace osu.Game.Graphics.Containers
{
public class OsuHoverContainer : OsuClickableContainer
{
private Color4 hoverColour;
protected override bool OnHover(InputState state)
{
this.FadeColour(hoverColour, 500, Easing.OutQuint);
return base.OnHover(state);
}
protected override void OnHoverLost(InputState state)
{
this.FadeColour(Color4.White, 500, Easing.OutQuint);
base.OnHoverLost(state);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
hoverColour = colours.Yellow;
}
}
}

View File

@ -1,27 +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 osu.Framework.Graphics.Containers;
using OpenTK;
using osu.Framework.Graphics;
namespace osu.Game.Graphics.Processing
{
internal class RatioAdjust : Container
{
public RatioAdjust()
{
RelativeSizeAxes = Axes.Both;
}
protected override void Update()
{
base.Update();
Vector2 parent = Parent.DrawSize;
Scale = new Vector2(Math.Min(parent.Y / 768f, parent.X / 1024f));
Size = new Vector2(1 / Scale.X);
}
}
}

View File

@ -15,32 +15,91 @@ namespace osu.Game.Graphics.UserInterface
{
public class IconButton : OsuClickableContainer
{
private readonly SpriteIcon icon;
private readonly Box hover;
private readonly Container content;
private const float button_size = 30;
private Color4? flashColour;
/// <summary>
/// The colour that should be flashed when the <see cref="IconButton"/> is clicked.
/// </summary>
public Color4 FlashColour
{
get { return flashColour ?? Color4.White; }
set { flashColour = value; }
}
private Color4? iconColour;
/// <summary>
/// The icon colour. This does not affect <see cref="IconButton.Colour"/>.
/// </summary>
public Color4 IconColour
{
get { return iconColour ?? Color4.White; }
set
{
iconColour = value;
icon.Colour = value;
}
}
private Color4? iconHoverColour;
/// <summary>
/// The icon colour while the <see cref="IconButton"/> is hovered.
/// </summary>
public Color4 IconHoverColour
{
get { return iconHoverColour ?? IconColour; }
set { iconHoverColour = value; }
}
private Color4? hoverColour;
/// <summary>
/// The background colour of the <see cref="IconButton"/> while it is hovered.
/// </summary>
public Color4 HoverColour
{
get { return hoverColour ?? Color4.White; }
set
{
hoverColour = value;
hover.Colour = value;
}
}
/// <summary>
/// The icon.
/// </summary>
public FontAwesome Icon
{
get { return icon.Icon; }
set { icon.Icon = value; }
}
private const float button_size = 30;
private Color4 flashColour;
/// <summary>
/// The icon scale. This does not affect <see cref="IconButton.Scale"/>.
/// </summary>
public Vector2 IconScale
{
get { return icon.Scale; }
set { icon.Scale = value; }
}
/// <summary>
/// The size of the <see cref="IconButton"/> while it is not being pressed.
/// </summary>
public Vector2 ButtonSize
{
get { return content.Size; }
set { content.Size = value; }
}
private readonly Container content;
private readonly SpriteIcon icon;
private readonly Box hover;
public IconButton()
{
AutoSizeAxes = Axes.Both;
Origin = Anchor.Centre;
Anchor = Anchor.Centre;
Children = new Drawable[]
{
content = new Container
@ -48,7 +107,6 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(button_size),
CornerRadius = 5,
Masking = true,
EdgeEffect = new EdgeEffectParameters
@ -78,8 +136,11 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
hover.Colour = colours.Yellow.Opacity(0.6f);
flashColour = colours.Yellow;
if (hoverColour == null)
HoverColour = colours.Yellow.Opacity(0.6f);
if (flashColour == null)
FlashColour = colours.Yellow;
Enabled.ValueChanged += enabled => this.FadeColour(enabled ? Color4.White : colours.Gray9, 200, Easing.OutQuint);
}
@ -87,18 +148,20 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(InputState state)
{
hover.FadeIn(500, Easing.OutQuint);
icon.FadeColour(IconHoverColour, 500, Easing.OutQuint);
return base.OnHover(state);
}
protected override void OnHoverLost(InputState state)
{
hover.FadeOut(500, Easing.OutQuint);
icon.FadeColour(IconColour, 500, Easing.OutQuint);
base.OnHoverLost(state);
}
protected override bool OnClick(InputState state)
{
hover.FlashColour(flashColour, 800, Easing.OutQuint);
hover.FlashColour(FlashColour, 800, Easing.OutQuint);
return base.OnClick(state);
}

View File

@ -59,7 +59,7 @@ namespace osu.Game.Graphics.UserInterface
public class OsuTabItem : TabItem<T>, IHasAccentColour
{
protected readonly SpriteText Text;
private readonly Box box;
protected readonly Box Bar;
private Color4 accentColour;
public Color4 AccentColour
@ -77,13 +77,13 @@ namespace osu.Game.Graphics.UserInterface
private void fadeActive()
{
box.FadeIn(transition_length, Easing.OutQuint);
Bar.FadeIn(transition_length, Easing.OutQuint);
Text.FadeColour(Color4.White, transition_length, Easing.OutQuint);
}
private void fadeInactive()
{
box.FadeOut(transition_length, Easing.OutQuint);
Bar.FadeOut(transition_length, Easing.OutQuint);
Text.FadeColour(AccentColour, transition_length, Easing.OutQuint);
}
@ -123,7 +123,7 @@ namespace osu.Game.Graphics.UserInterface
TextSize = 14,
Font = @"Exo2.0-Bold", // Font should only turn bold when active?
},
box = new Box
Bar = new Box
{
RelativeSizeAxes = Axes.X,
Height = 1,

View File

@ -1,22 +1,20 @@
// 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.DataAnnotations.Schema;
using System.IO;
using SQLite.Net.Attributes;
namespace osu.Game.IO
{
public class FileInfo
{
[PrimaryKey, AutoIncrement]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[Indexed(Unique = true)]
public string Hash { get; set; }
public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash);
[Indexed]
public int ReferenceCount { get; set; }
}
}

View File

@ -4,12 +4,12 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Database;
using SQLite.Net;
namespace osu.Game.IO
{
@ -18,75 +18,37 @@ namespace osu.Game.IO
/// </summary>
public class FileStore : DatabaseBackedStore
{
private const string prefix = "files";
public readonly IResourceStore<byte[]> Store;
public readonly ResourceStore<byte[]> Store;
public Storage Storage => base.Storage;
protected override int StoreVersion => 2;
public FileStore(SQLiteConnection connection, Storage storage) : base(connection, storage)
public FileStore(Func<OsuDbContext> getContext, Storage storage) : base(getContext, storage.GetStorageForDirectory(@"files"))
{
Store = new NamespacedResourceStore<byte[]>(new StorageBackedResourceStore(storage), prefix);
Store = new StorageBackedResourceStore(Storage);
}
protected override Type[] ValidTypes => new[] {
typeof(FileInfo),
};
protected override void Prepare(bool reset = false)
{
if (reset)
{
// in earlier versions we stored beatmaps as solid archives, but not any more.
if (Storage.ExistsDirectory("beatmaps"))
Storage.DeleteDirectory("beatmaps");
if (Storage.ExistsDirectory(string.Empty))
Storage.DeleteDirectory(string.Empty);
if (Storage.ExistsDirectory(prefix))
Storage.DeleteDirectory(prefix);
Connection.DropTable<FileInfo>();
}
Connection.CreateTable<FileInfo>();
}
protected override void StartupTasks()
{
base.StartupTasks();
deletePending();
}
/// <summary>
/// Perform migrations between two store versions.
/// </summary>
/// <param name="currentVersion">The current store version. This will be zero on a fresh database initialisation.</param>
/// <param name="targetVersion">The target version which we are migrating to (equal to the current <see cref="StoreVersion"/>).</param>
protected override void PerformMigration(int currentVersion, int targetVersion)
{
base.PerformMigration(currentVersion, targetVersion);
while (currentVersion++ < targetVersion)
{
switch (currentVersion)
{
case 1:
case 2:
// cannot migrate; breaking underlying changes.
Reset();
break;
}
GetContext().Database.ExecuteSqlCommand("DELETE FROM FileInfo");
}
}
public FileInfo Add(Stream data, bool reference = true)
{
var context = GetContext();
string hash = data.ComputeSHA2Hash();
var existing = Connection.Table<FileInfo>().Where(f => f.Hash == hash).FirstOrDefault();
var existing = context.FileInfo.FirstOrDefault(f => f.Hash == hash);
var info = existing ?? new FileInfo { Hash = hash };
string path = Path.Combine(prefix, info.StoragePath);
string path = info.StoragePath;
// we may be re-adding a file to fix missing store entries.
if (!Storage.Exists(path))
@ -99,62 +61,58 @@ namespace osu.Game.IO
data.Seek(0, SeekOrigin.Begin);
}
if (existing == null)
Connection.Insert(info);
if (reference || existing == null)
Reference(info);
return info;
}
public void Reference(params FileInfo[] files)
{
Connection.RunInTransaction(() =>
{
var incrementedFiles = files.GroupBy(f => f.ID).Select(f =>
{
var accurateRefCount = Connection.Get<FileInfo>(f.First().ID);
accurateRefCount.ReferenceCount += f.Count();
return accurateRefCount;
});
public void Reference(params FileInfo[] files) => reference(GetContext(), files);
Connection.UpdateAll(incrementedFiles);
});
private void reference(OsuDbContext context, FileInfo[] files)
{
foreach (var f in files.GroupBy(f => f.ID))
{
var refetch = context.Find<FileInfo>(f.First().ID) ?? f.First();
refetch.ReferenceCount += f.Count();
context.FileInfo.Update(refetch);
}
context.SaveChanges();
}
public void Dereference(params FileInfo[] files)
{
Connection.RunInTransaction(() =>
{
var incrementedFiles = files.GroupBy(f => f.ID).Select(f =>
{
var accurateRefCount = Connection.Get<FileInfo>(f.First().ID);
accurateRefCount.ReferenceCount -= f.Count();
return accurateRefCount;
});
public void Dereference(params FileInfo[] files) => dereference(GetContext(), files);
Connection.UpdateAll(incrementedFiles);
});
private void dereference(OsuDbContext context, FileInfo[] files)
{
foreach (var f in files.GroupBy(f => f.ID))
{
var refetch = context.FileInfo.Find(f.Key);
refetch.ReferenceCount -= f.Count();
context.FileInfo.Update(refetch);
}
context.SaveChanges();
}
private void deletePending()
public override void Cleanup()
{
Connection.RunInTransaction(() =>
var context = GetContext();
foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1))
{
foreach (var f in Query<FileInfo>(f => f.ReferenceCount < 1))
try
{
try
{
Storage.Delete(Path.Combine(prefix, f.StoragePath));
Connection.Delete(f);
}
catch (Exception e)
{
Logger.Error(e, $@"Could not delete beatmap {f}");
}
Storage.Delete(f.StoragePath);
context.FileInfo.Remove(f);
}
});
catch (Exception e)
{
Logger.Error(e, $@"Could not delete beatmap {f}");
}
}
context.SaveChanges();
}
}
}

View File

@ -1,23 +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.ComponentModel.DataAnnotations.Schema;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets;
using SQLite.Net.Attributes;
using SQLiteNetExtensions.Attributes;
namespace osu.Game.Input.Bindings
{
[Table("KeyBinding")]
public class DatabasedKeyBinding : KeyBinding
{
[PrimaryKey, AutoIncrement]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[ForeignKey(typeof(RulesetInfo))]
public int? RulesetID { get; set; }
[Indexed]
public int? Variant { get; set; }
[Column("Keys")]
@ -27,7 +23,6 @@ namespace osu.Game.Input.Bindings
private set { KeyCombination = value; }
}
[Indexed]
[Column("Action")]
public int IntAction
{

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