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

Compare commits

...

489 Commits

221 changed files with 4859 additions and 2243 deletions
@@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="VisualTests (netcoreapp2.0)" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Tests/bin/Debug/netcoreapp2.0/osu.Game.Tests.dll" />
<configuration default="false" name="VisualTests (netcoreapp2.1)" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Tests/bin/Debug/netcoreapp2.1/osu.Game.Tests.dll" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Tests" />
<option name="PASS_PARENT_ENVS" value="1" />
@@ -12,7 +12,7 @@
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value=".NETCoreApp,Version=v2.0" />
<option name="PROJECT_TFM" value=".NETCoreApp,Version=v2.1" />
<method />
</configuration>
</component>
@@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="osu! (netcoreapp2.0)" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/netcoreapp2.0/osu!.dll" />
<configuration default="false" name="osu! (netcoreapp2.1)" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Desktop/bin/Debug/netcoreapp2.1/osu!.dll" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Desktop" />
<option name="PASS_PARENT_ENVS" value="1" />
@@ -12,7 +12,7 @@
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value=".NETCoreApp,Version=v2.0" />
<option name="PROJECT_TFM" value=".NETCoreApp,Version=v2.1" />
<method />
</configuration>
</component>
+13 -13
View File
@@ -22,7 +22,7 @@
},
"type": "mono",
"request": "launch",
"program": "${workspaceRoot}/osu.Game.Tests/bin/Debug/net471/osu.Game.Tests.exe",
"program": "${workspaceRoot}/osu.Game.Tests/bin/Release/net471/osu.Game.Tests.exe",
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, msbuild)",
"runtimeExecutable": null,
@@ -58,54 +58,54 @@
"console": "internalConsole"
},
{
"name": "VisualTests (Debug, netcoreapp2.0)",
"name": "VisualTests (Debug, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.0/osu.Game.Tests.dll"
"${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.1/osu.Game.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug, dotnet)",
"preLaunchTask": "Build tests (Debug, dotnet)",
"env": {},
"console": "internalConsole"
},
{
"name": "VisualTests (Release, netcoreapp2.0)",
"name": "VisualTests (Release, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Game.Tests/bin/Debug/netcoreapp2.0/osu.Game.Tests.dll"
"${workspaceRoot}/osu.Game.Tests/bin/Release/netcoreapp2.1/osu.Game.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, dotnet)",
"preLaunchTask": "Build tests (Release, dotnet)",
"env": {},
"console": "internalConsole"
},
{
"name": "osu! (Debug, netcoreapp2.0)",
"name": "osu! (Debug, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.0/osu!.dll",
"${workspaceRoot}/osu.Desktop/bin/Debug/netcoreapp2.1/osu!.dll",
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug, dotnet)",
"preLaunchTask": "Build osu! (Debug, dotnet)",
"env": {},
"console": "internalConsole"
},
{
"name": "osu! (Release, netcoreapp2.0)",
"name": "osu! (Release, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.0/osu!.dll",
"${workspaceRoot}/osu.Desktop/bin/Release/netcoreapp2.1/osu!.dll",
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, dotnet)",
"preLaunchTask": "Build osu! (Release, dotnet)",
"env": {},
"console": "internalConsole"
}
+38 -5
View File
@@ -31,14 +31,14 @@
"problemMatcher": "$msCompile"
},
{
"label": "Build (Debug, dotnet)",
"label": "Build osu! (Debug, dotnet)",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Desktop",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:GenerateFullPaths=true",
"/m",
"/verbosity:m"
@@ -47,14 +47,47 @@
"problemMatcher": "$msCompile"
},
{
"label": "Build (Release, dotnet)",
"label": "Build osu! (Release, dotnet)",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Desktop",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:Configuration=Release",
"/p:GenerateFullPaths=true",
"/m",
"/verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Build tests (Debug, dotnet)",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tests",
"/p:TargetFramework=netcoreapp2.1",
"/p:GenerateFullPaths=true",
"/m",
"/verbosity:m"
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Build tests (Release, dotnet)",
"type": "shell",
"command": "dotnet",
"args": [
"build",
"--no-restore",
"osu.Game.Tests",
"/p:TargetFramework=netcoreapp2.1",
"/p:Configuration=Release",
"/p:GenerateFullPaths=true",
"/m",
@@ -73,7 +106,7 @@
"problemMatcher": []
},
{
"label": "Restore (netcoreapp2.0)",
"label": "Restore (netcoreapp2.1)",
"type": "shell",
"command": "dotnet",
"args": [
+1 -1
View File
@@ -1,6 +1,6 @@
clone_depth: 1
version: '{branch}-{build}'
image: Visual Studio 2017
image: Visual Studio 2017 preview
configuration: Debug
cache:
- C:\ProgramData\chocolatey\bin -> appveyor.yml
+17 -17
View File
@@ -1,29 +1,26 @@
branches:
only:
- release
skip_tags: true
skip_branch_with_pr: true
clone_depth: 1
version: '{branch}-{build}'
version: '{build}'
# skip_non_tags: true
image: Visual Studio 2017
configuration: Debug
cache:
- packages -> **\packages.config
- '%USERPROFILE%\.nuget\packages -> **\*.csproj'
install:
- cmd: git submodule update --init --recursive --depth=5
- git clone https://github.com/ppy/osu-deploy
before_build:
- ps: if($env:appveyor_repo_tag -eq 'True') { Update-AppveyorBuild -Version $env:appveyor_repo_tag_name }
- cmd: git submodule update --init --recursive --depth=5
- cmd: nuget restore -verbosity quiet
build:
project: osu.Desktop.Deploy/osu.Desktop.Deploy.csproj
verbosity: minimal
after_build:
build_script:
- ps: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/secure-file/master/install.ps1'))
- appveyor DownloadFile https://puu.sh/A6g5K/4d08705438.enc # signing certificate
- cmd: appveyor-tools\secure-file -decrypt 4d08705438.enc -secret %decode_secret% -out %HOMEPATH%\deanherbert.pfx
- appveyor DownloadFile https://puu.sh/A6g75/fdc6f19b04.enc # deploy configuration
- cmd: appveyor-tools\secure-file -decrypt fdc6f19b04.enc -secret %decode_secret% -out osu.Desktop.Deploy\bin\Debug\net471\osu.Desktop.Deploy.exe.config
- cd osu.Desktop.Deploy\bin\Debug\net471\
- osu.Desktop.Deploy.exe %code_signing_password%
- cd osu-deploy
- nuget restore -verbosity quiet
- msbuild osu.Desktop.Deploy.csproj
- cmd: ..\appveyor-tools\secure-file -decrypt ..\fdc6f19b04.enc -secret %decode_secret% -out bin\Debug\net471\osu-deploy.exe.config
- cd bin\Debug\net471\
- osu.Desktop.Deploy.exe %code_signing_password% %APPVEYOR_REPO_TAG_NAME%
environment:
TargetFramework: net471
decode_secret:
@@ -31,4 +28,7 @@ environment:
code_signing_password:
secure: 34tLNqvjmmZEi97MLKfrnQ==
artifacts:
- path: 'Releases\*'
- path: 'Releases\*'
deploy:
- provider: Environment
name: github
-21
View File
@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
-->
<configuration>
<appSettings>
<add key="StagingFolder" value="Staging" />
<add key="ReleasesFolder" value="Releases" />
<add key="GitHubAccessToken" value="" />
<add key="GitHubUsername" value="ppy" />
<add key="GitHubRepoName" value="osu" />
<add key="ProjectName" value="osu.Desktop" />
<add key="NuSpecName" value="osu.Desktop\osu.nuspec" />
<add key="SolutionName" value="osu" />
<add key="TargetName" value="osu.Desktop" />
<add key="PackageName" value="osulazer" />
<add key="IconName" value="lazer.ico" />
<add key="CodeSigningCertificate" value="" />
</appSettings>
</configuration>
-16
View File
@@ -1,16 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
namespace osu.Desktop.Deploy
{
public class GitHubObject
{
[JsonProperty(@"id")]
public int Id;
[JsonProperty(@"name")]
public string Name;
}
}
-28
View File
@@ -1,28 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using Newtonsoft.Json;
namespace osu.Desktop.Deploy
{
public class GitHubRelease
{
[JsonProperty(@"id")]
public int Id;
[JsonProperty(@"tag_name")]
public string TagName => $"v{Name}";
[JsonProperty(@"name")]
public string Name;
[JsonProperty(@"draft")]
public bool Draft;
[JsonProperty(@"prerelease")]
public bool PreRelease;
[JsonProperty(@"upload_url")]
public string UploadUrl;
}
}
-471
View File
@@ -1,471 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using FileWebRequest = osu.Framework.IO.Network.FileWebRequest;
using WebRequest = osu.Framework.IO.Network.WebRequest;
namespace osu.Desktop.Deploy
{
internal static class Program
{
private static string packages => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
private static string nugetPath => Path.Combine(packages, @"nuget.commandline\4.5.1\tools\NuGet.exe");
private static string squirrelPath => Path.Combine(packages, @"squirrel.windows\1.8.0\tools\Squirrel.exe");
private const string msbuild_path = @"C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\MSBuild.exe";
public static string StagingFolder = ConfigurationManager.AppSettings["StagingFolder"];
public static string ReleasesFolder = ConfigurationManager.AppSettings["ReleasesFolder"];
public static string GitHubAccessToken = ConfigurationManager.AppSettings["GitHubAccessToken"];
public static string GitHubUsername = ConfigurationManager.AppSettings["GitHubUsername"];
public static string GitHubRepoName = ConfigurationManager.AppSettings["GitHubRepoName"];
public static string SolutionName = ConfigurationManager.AppSettings["SolutionName"];
public static string ProjectName = ConfigurationManager.AppSettings["ProjectName"];
public static string NuSpecName = ConfigurationManager.AppSettings["NuSpecName"];
public static string TargetNames = ConfigurationManager.AppSettings["TargetName"];
public static string PackageName = ConfigurationManager.AppSettings["PackageName"];
public static string IconName = ConfigurationManager.AppSettings["IconName"];
public static string CodeSigningCertificate = ConfigurationManager.AppSettings["CodeSigningCertificate"];
public static string GitHubApiEndpoint => $"https://api.github.com/repos/{GitHubUsername}/{GitHubRepoName}/releases";
public static string GitHubReleasePage => $"https://github.com/{GitHubUsername}/{GitHubRepoName}/releases";
/// <summary>
/// How many previous build deltas we want to keep when publishing.
/// </summary>
private const int keep_delta_count = 4;
private static string codeSigningCmd => string.IsNullOrEmpty(codeSigningPassword) ? "" : $"-n \"/a /f {codeSigningCertPath} /p {codeSigningPassword} /t http://timestamp.comodoca.com/authenticode\"";
private static string homeDir => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
private static string codeSigningCertPath => Path.Combine(homeDir, CodeSigningCertificate);
private static string solutionPath => Environment.CurrentDirectory;
private static string stagingPath => Path.Combine(solutionPath, StagingFolder);
private static string iconPath => Path.Combine(solutionPath, ProjectName, IconName);
private static string nupkgFilename(string ver) => $"{PackageName}.{ver}.nupkg";
private static string nupkgDistroFilename(string ver) => $"{PackageName}-{ver}-full.nupkg";
private static readonly Stopwatch sw = new Stopwatch();
private static string codeSigningPassword;
private static bool interactive;
public static void Main(string[] args)
{
interactive = args.Length == 0;
displayHeader();
findSolutionPath();
if (!Directory.Exists(ReleasesFolder))
{
write("WARNING: No release directory found. Make sure you want this!", ConsoleColor.Yellow);
Directory.CreateDirectory(ReleasesFolder);
}
checkGitHubReleases();
refreshDirectory(StagingFolder);
//increment build number until we have a unique one.
string verBase = DateTime.Now.ToString("yyyy.Mdd.");
int increment = 0;
while (Directory.GetFiles(ReleasesFolder, $"*{verBase}{increment}*").Any())
increment++;
string version = $"{verBase}{increment}";
Console.ForegroundColor = ConsoleColor.White;
Console.Write($"Ready to deploy {version}!");
pauseIfInteractive();
sw.Start();
if (!string.IsNullOrEmpty(CodeSigningCertificate))
{
Console.Write("Enter code signing password: ");
codeSigningPassword = args.Length > 0 ? args[0] : readLineMasked();
}
write("Updating AssemblyInfo...");
updateCsprojVersion(version);
updateAppveyorVersion(version);
write("Running build process...");
foreach (string targetName in TargetNames.Split(','))
runCommand(msbuild_path, $"/v:quiet /m /t:{targetName.Replace('.', '_')} /p:OutputPath={stagingPath};Targets=\"Clean;Build\";Configuration=Release {SolutionName}.sln");
write("Creating NuGet deployment package...");
runCommand(nugetPath, $"pack {NuSpecName} -Version {version} -Properties Configuration=Deploy -OutputDirectory {stagingPath} -BasePath {stagingPath}");
//prune once before checking for files so we can avoid erroring on files which aren't even needed for this build.
pruneReleases();
checkReleaseFiles();
write("Running squirrel build...");
runCommand(squirrelPath, $"--releasify {stagingPath}\\{nupkgFilename(version)} --framework-version=net471 --setupIcon {iconPath} --icon {iconPath} {codeSigningCmd} --no-msi");
//prune again to clean up before upload.
pruneReleases();
//rename setup to install.
File.Copy(Path.Combine(ReleasesFolder, "Setup.exe"), Path.Combine(ReleasesFolder, "install.exe"), true);
File.Delete(Path.Combine(ReleasesFolder, "Setup.exe"));
uploadBuild(version);
//reset assemblyinfo.
updateCsprojVersion("0.0.0");
write("Done!", ConsoleColor.White);
pauseIfInteractive();
}
private static void displayHeader()
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine();
Console.WriteLine(" Please note that OSU! and PPY are registered trademarks and as such covered by trademark law.");
Console.WriteLine(" Do not distribute builds of this project publicly that make use of these.");
Console.ResetColor();
Console.WriteLine();
}
/// <summary>
/// Ensure we have all the files in the release directory which are expected to be there.
/// This should have been accounted for in earlier steps, and just serves as a verification step.
/// </summary>
private static void checkReleaseFiles()
{
if (!canGitHub) return;
var releaseLines = getReleaseLines();
//ensure we have all files necessary
foreach (var l in releaseLines)
if (!File.Exists(Path.Combine(ReleasesFolder, l.Filename)))
error($"Local file missing {l.Filename}");
}
private static IEnumerable<ReleaseLine> getReleaseLines() => File.ReadAllLines(Path.Combine(ReleasesFolder, "RELEASES")).Select(l => new ReleaseLine(l));
private static void pruneReleases()
{
if (!canGitHub) return;
write("Pruning RELEASES...");
var releaseLines = getReleaseLines().ToList();
var fulls = releaseLines.Where(l => l.Filename.Contains("-full")).Reverse().Skip(1);
//remove any FULL releases (except most recent)
foreach (var l in fulls)
{
write($"- Removing old release {l.Filename}", ConsoleColor.Yellow);
File.Delete(Path.Combine(ReleasesFolder, l.Filename));
releaseLines.Remove(l);
}
//remove excess deltas
var deltas = releaseLines.Where(l => l.Filename.Contains("-delta")).ToArray();
if (deltas.Length > keep_delta_count)
{
foreach (var l in deltas.Take(deltas.Length - keep_delta_count))
{
write($"- Removing old delta {l.Filename}", ConsoleColor.Yellow);
File.Delete(Path.Combine(ReleasesFolder, l.Filename));
releaseLines.Remove(l);
}
}
var lines = new List<string>();
releaseLines.ForEach(l => lines.Add(l.ToString()));
File.WriteAllLines(Path.Combine(ReleasesFolder, "RELEASES"), lines);
}
private static void uploadBuild(string version)
{
if (!canGitHub || string.IsNullOrEmpty(CodeSigningCertificate))
return;
write("Publishing to GitHub...");
write($"- Creating release {version}...", ConsoleColor.Yellow);
var req = new JsonWebRequest<GitHubRelease>($"{GitHubApiEndpoint}")
{
Method = HttpMethod.POST,
};
req.AddRaw(JsonConvert.SerializeObject(new GitHubRelease
{
Name = version,
Draft = true,
PreRelease = true
}));
req.AuthenticatedBlockingPerform();
var assetUploadUrl = req.ResponseObject.UploadUrl.Replace("{?name,label}", "?name={0}");
foreach (var a in Directory.GetFiles(ReleasesFolder).Reverse()) //reverse to upload RELEASES first.
{
write($"- Adding asset {a}...", ConsoleColor.Yellow);
var upload = new WebRequest(assetUploadUrl, Path.GetFileName(a))
{
Method = HttpMethod.POST,
Timeout = 240000,
ContentType = "application/octet-stream",
};
upload.AddRaw(File.ReadAllBytes(a));
upload.AuthenticatedBlockingPerform();
}
openGitHubReleasePage();
}
private static void openGitHubReleasePage() => Process.Start(GitHubReleasePage);
private static bool canGitHub => !string.IsNullOrEmpty(GitHubAccessToken);
private static void checkGitHubReleases()
{
if (!canGitHub) return;
write("Checking GitHub releases...");
var req = new JsonWebRequest<List<GitHubRelease>>($"{GitHubApiEndpoint}");
req.AuthenticatedBlockingPerform();
var lastRelease = req.ResponseObject.FirstOrDefault();
if (lastRelease == null)
return;
if (lastRelease.Draft)
{
openGitHubReleasePage();
error("There's a pending draft release! You probably don't want to push a build with this present.");
}
//there's a previous release for this project.
var assetReq = new JsonWebRequest<List<GitHubObject>>($"{GitHubApiEndpoint}/{lastRelease.Id}/assets");
assetReq.AuthenticatedBlockingPerform();
var assets = assetReq.ResponseObject;
//make sure our RELEASES file is the same as the last build on the server.
var releaseAsset = assets.FirstOrDefault(a => a.Name == "RELEASES");
//if we don't have a RELEASES asset then the previous release likely wasn't a Squirrel one.
if (releaseAsset == null) return;
write($"Last GitHub release was {lastRelease.Name}.");
bool requireDownload = false;
if (!File.Exists(Path.Combine(ReleasesFolder, nupkgDistroFilename(lastRelease.Name))))
{
write("Last version's package not found locally.", ConsoleColor.Red);
requireDownload = true;
}
else
{
var lastReleases = new RawFileWebRequest($"{GitHubApiEndpoint}/assets/{releaseAsset.Id}");
lastReleases.AuthenticatedBlockingPerform();
if (File.ReadAllText(Path.Combine(ReleasesFolder, "RELEASES")) != lastReleases.ResponseString)
{
write("Server's RELEASES differed from ours.", ConsoleColor.Red);
requireDownload = true;
}
}
if (!requireDownload) return;
write("Refreshing local releases directory...");
refreshDirectory(ReleasesFolder);
foreach (var a in assets)
{
if (a.Name.EndsWith(".exe")) continue;
write($"- Downloading {a.Name}...", ConsoleColor.Yellow);
new FileWebRequest(Path.Combine(ReleasesFolder, a.Name), $"{GitHubApiEndpoint}/assets/{a.Id}").AuthenticatedBlockingPerform();
}
}
private static void refreshDirectory(string directory)
{
if (Directory.Exists(directory))
Directory.Delete(directory, true);
Directory.CreateDirectory(directory);
}
private static void updateCsprojVersion(string version)
{
var toUpdate = new[] { "<Version>", "<FileVersion>" };
string file = Path.Combine(ProjectName, $"{ProjectName}.csproj");
var l1 = File.ReadAllLines(file);
List<string> l2 = new List<string>();
foreach (var l in l1)
{
string line = l;
foreach (var tag in toUpdate)
{
int startIndex = l.IndexOf(tag, StringComparison.InvariantCulture);
if (startIndex == -1)
continue;
startIndex += tag.Length;
int endIndex = l.IndexOf("<", startIndex, StringComparison.InvariantCulture);
line = $"{l.Substring(0, startIndex)}{version}{l.Substring(endIndex)}";
}
l2.Add(line);
}
File.WriteAllLines(file, l2);
}
/// <summary>
/// Find the base path of the active solution (git checkout location)
/// </summary>
private static void findSolutionPath()
{
string path = Path.GetDirectoryName(Environment.CommandLine.Replace("\"", "").Trim());
if (string.IsNullOrEmpty(path))
path = Environment.CurrentDirectory;
while (!File.Exists(Path.Combine(path, $"{SolutionName}.sln")))
path = path.Remove(path.LastIndexOf(Path.DirectorySeparatorChar));
path += Path.DirectorySeparatorChar;
Environment.CurrentDirectory = path;
}
private static bool runCommand(string command, string args)
{
var psi = new ProcessStartInfo(command, args)
{
WorkingDirectory = solutionPath,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
WindowStyle = ProcessWindowStyle.Hidden
};
Process p = Process.Start(psi);
if (p == null) return false;
string output = p.StandardOutput.ReadToEnd();
output += p.StandardError.ReadToEnd();
if (p.ExitCode == 0) return true;
write(output);
error($"Command {command} {args} failed!");
return false;
}
private static string readLineMasked()
{
var fg = Console.ForegroundColor;
Console.ForegroundColor = Console.BackgroundColor;
var ret = Console.ReadLine();
Console.ForegroundColor = fg;
return ret;
}
private static void error(string message)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"FATAL ERROR: {message}");
pauseIfInteractive();
Environment.Exit(-1);
}
private static void pauseIfInteractive()
{
if (interactive)
Console.ReadLine();
else
Console.WriteLine();
}
private static bool updateAppveyorVersion(string version)
{
try
{
using (PowerShell ps = PowerShell.Create())
{
ps.AddScript($"Update-AppveyorBuild -Version \"{version}\"");
ps.Invoke();
}
return true;
}
catch
{
// we don't have appveyor and don't care
}
return false;
}
private static void write(string message, ConsoleColor col = ConsoleColor.Gray)
{
if (sw.ElapsedMilliseconds > 0)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write(sw.ElapsedMilliseconds.ToString().PadRight(8));
}
Console.ForegroundColor = col;
Console.WriteLine(message);
}
public static void AuthenticatedBlockingPerform(this WebRequest r)
{
r.AddHeader("Authorization", $"token {GitHubAccessToken}");
r.Perform();
}
}
internal class RawFileWebRequest : WebRequest
{
public RawFileWebRequest(string url) : base(url)
{
}
protected override string Accept => "application/octet-stream";
}
internal class ReleaseLine
{
public string Hash;
public string Filename;
public int Filesize;
public ReleaseLine(string line)
{
var split = line.Split(' ');
Hash = split[0];
Filename = split[1];
Filesize = int.Parse(split[2]);
}
public override string ToString() => $"{Hash} {Filename} {Filesize}";
}
}
@@ -1,19 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.Game.props" />
<PropertyGroup Label="Project">
<TargetFrameworks>net471</TargetFrameworks>
<OutputType>Exe</OutputType>
<PlatformTarget>AnyCPU</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu-framework\osu.Framework\osu.Framework.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="NuGet.CommandLine" Version="4.5.1" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="squirrel.windows" Version="1.8.0" Condition="'$(TargetFramework)' == 'net471'" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="4.4.0" />
<PackageReference Include="System.Management.Automation.dll" Version="10.0.10586" />
</ItemGroup>
</Project>
+2 -1
View File
@@ -12,6 +12,7 @@ using osu.Framework.Platform;
using osu.Game;
using OpenTK.Input;
using Microsoft.Win32;
using osu.Framework.Platform.Windows;
namespace osu.Desktop
{
@@ -40,7 +41,7 @@ namespace osu.Desktop
/// <summary>
/// A method of accessing an osu-stable install in a controlled fashion.
/// </summary>
private class StableStorage : DesktopStorage
private class StableStorage : WindowsStorage
{
protected override string LocateBasePath()
{
+4 -4
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.Game.props" />
<PropertyGroup Label="Project">
<TargetFrameworks>net471;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>net471;netcoreapp2.1</TargetFrameworks>
<OutputType>WinExe</OutputType>
<PlatformTarget>AnyCPU</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -30,10 +30,10 @@
<PackageReference Include="Microsoft.Win32.Registry" Version="4.4.0" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.3" />
<PackageReference Include="squirrel.windows" Version="1.8.0" Condition="'$(TargetFramework)' == 'net471'" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />
</ItemGroup>
</Project>
</Project>
+5 -5
View File
@@ -22,7 +22,7 @@
},
"type": "mono",
"request": "launch",
"program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Catch.Tests.exe",
"program": "${workspaceRoot}/bin/Release/net471/osu.Game.Rulesets.Catch.Tests.exe",
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, msbuild)",
"runtimeExecutable": null,
@@ -30,12 +30,12 @@
"console": "internalConsole"
},
{
"name": "VisualTests (Debug, netcoreapp2.0)",
"name": "VisualTests (Debug, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Catch.Tests.dll"
"${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug, dotnet)",
@@ -43,12 +43,12 @@
"console": "internalConsole"
},
{
"name": "VisualTests (Release, netcoreapp2.0)",
"name": "VisualTests (Release, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Catch.Tests.dll"
"${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Catch.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, dotnet)",
+3 -3
View File
@@ -40,7 +40,7 @@
"build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:GenerateFullPaths=true",
"/m",
"/verbosity:m"
@@ -56,7 +56,7 @@
"build",
"--no-restore",
"osu.Game.Rulesets.Catch.Tests.csproj",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:Configuration=Release",
"/p:GenerateFullPaths=true",
"/m",
@@ -75,7 +75,7 @@
"problemMatcher": []
},
{
"label": "Restore (netcoreapp2.0)",
"label": "Restore (netcoreapp2.1)",
"type": "shell",
"command": "dotnet",
"args": [
@@ -5,8 +5,6 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects;
@@ -14,7 +12,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
{
public class CatchBeatmapConversionTest : BeatmapConversionTest<TestCatchRuleset, ConvertValue>
internal class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
@@ -47,10 +45,10 @@ namespace osu.Game.Rulesets.Catch.Tests
}
}
protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap);
protected override Ruleset CreateRuleset() => new CatchRuleset();
}
public struct ConvertValue : IEquatable<ConvertValue>
internal struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
@@ -64,8 +62,4 @@ namespace osu.Game.Rulesets.Catch.Tests
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(Position, other.Position, conversion_lenience);
}
public class TestCatchRuleset : CatchRuleset
{
}
}
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Tests
Child = catcherArea = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.BottomLeft
Origin = Anchor.TopLeft
},
};
}
@@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Catch.Tests
typeof(DrawableCatchHitObject),
typeof(DrawableFruit),
typeof(DrawableDroplet),
typeof(BananaShower),
typeof(Pulp),
};
@@ -53,12 +54,19 @@ namespace osu.Game.Rulesets.Catch.Tests
private DrawableFruit createDrawable(int index)
{
var fruit = new Fruit
{
StartTime = 1000000000000,
IndexInBeatmap = index,
Scale = 1.5f,
};
Fruit fruit = index == 5
? new BananaShower.Banana
{
StartTime = 1000000000000,
IndexInBeatmap = index,
Scale = 1.5f,
}
: new Fruit
{
StartTime = 1000000000000,
IndexInBeatmap = index,
Scale = 1.5f,
};
return new DrawableFruit(fruit)
{
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
<TargetFrameworks>netcoreapp2.0;net471</TargetFrameworks>
<TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks>
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />
@@ -28,7 +28,20 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
var endTime = obj as IHasEndTime;
if (positionData == null)
{
if (endTime != null)
{
yield return new BananaShower
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = endTime.Duration,
NewCombo = comboData?.NewCombo ?? false
};
}
yield break;
}
if (curveData != null)
{
@@ -48,19 +61,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
yield break;
}
if (endTime != null)
{
yield return new BananaShower
{
StartTime = obj.StartTime,
Samples = obj.Samples,
Duration = endTime.Duration,
NewCombo = comboData?.NewCombo ?? false
};
yield break;
}
yield return new Fruit
{
StartTime = obj.StartTime,
+2
View File
@@ -14,6 +14,8 @@ using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Catch
{
@@ -1,10 +1,11 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
namespace osu.Game.Rulesets.Catch
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyCalculator : DifficultyCalculator
{
@@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Catch.Objects
Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5;
}
protected override HitWindows CreateHitWindows() => null;
}
public enum FruitVisualRepresentation
@@ -18,12 +18,19 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
private Circle border;
private const float drawable_radius = (float)CatchHitObject.OBJECT_RADIUS * radius_adjust;
/// <summary>
/// Because we're adding a border around the fruit, we need to scale down some.
/// </summary>
private const float radius_adjust = 1.1f;
public DrawableFruit(Fruit h)
: base(h)
{
Origin = Anchor.Centre;
Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS);
Size = new Vector2(drawable_radius);
Masking = false;
Rotation = (float)(RNG.NextDouble() - 0.5f) * 40;
@@ -44,14 +51,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
Hollow = !HitObject.HyperDash,
Type = EdgeEffectType.Glow,
Radius = 4,
Radius = 4 * radius_adjust,
Colour = HitObject.HyperDash ? Color4.Red : AccentColour.Darken(1).Opacity(0.6f)
},
Size = new Vector2(Height * 1.5f),
Size = new Vector2(Height),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BorderColour = Color4.White,
BorderThickness = 4f,
BorderThickness = 3f * radius_adjust,
Children = new Framework.Graphics.Drawable[]
{
new Box
@@ -82,8 +89,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
private Framework.Graphics.Drawable createPulp(FruitVisualRepresentation representation)
{
const float large_pulp_3 = 13f;
const float distance_from_centre_3 = 0.23f;
const float large_pulp_3 = 8f * radius_adjust;
const float distance_from_centre_3 = 0.15f;
const float large_pulp_4 = large_pulp_3 * 0.925f;
const float distance_from_centre_4 = distance_from_centre_3 / 0.925f;
@@ -106,11 +113,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
AccentColour = AccentColour,
Size = new Vector2(small_pulp),
Y = 0.05f,
Y = -0.34f,
},
new Pulp
{
@@ -146,11 +151,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
AccentColour = AccentColour,
Size = new Vector2(small_pulp),
Y = 0.1f,
Y = -0.3f,
},
new Pulp
{
@@ -186,11 +189,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AccentColour = AccentColour,
Size = new Vector2(small_pulp),
Y = -0.1f,
Y = -0.33f,
},
new Pulp
{
@@ -220,10 +221,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AccentColour = AccentColour,
Size = new Vector2(small_pulp),
Y = -0.25f,
},
new Pulp
{
@@ -253,16 +253,15 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable
{
new Pulp
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AccentColour = AccentColour,
Size = new Vector2(small_pulp),
Y = -0.15f
Y = -0.3f
},
new Pulp
{
AccentColour = AccentColour,
Size = new Vector2(large_pulp_4 * 1.2f, large_pulp_4 * 3),
Size = new Vector2(large_pulp_4 * 0.8f, large_pulp_4 * 2.5f),
Y = 0.05f,
},
}
};
@@ -29,14 +29,24 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces
set
{
accentColour = value;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 8,
Colour = accentColour.Darken(0.2f).Opacity(0.75f)
};
if (IsLoaded) updateAccentColour();
}
}
private void updateAccentColour()
{
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = Size.X / 2,
Colour = accentColour.Darken(0.2f).Opacity(0.75f)
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateAccentColour();
}
}
}
@@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.UI
public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
protected override Vector2 PlayfieldArea => new Vector2(0.86f); // matches stable's vertical offset for catcher plate
protected override DrawableHitObject<CatchHitObject> GetVisualRepresentation(CatchHitObject h)
{
switch (h)
+20 -13
View File
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherArea : Container
{
public const float CATCHER_SIZE = 172;
public const float CATCHER_SIZE = 84;
protected readonly Catcher MovableCatcher;
@@ -99,8 +99,6 @@ namespace osu.Game.Rulesets.Catch.UI
public class Catcher : Container, IKeyBindingHandler<CatchAction>
{
private Texture texture;
private Container<DrawableHitObject> caughtFruit;
public Container ExplodingFruitTarget;
@@ -121,10 +119,8 @@ namespace osu.Game.Rulesets.Catch.UI
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
private void load()
{
texture = textures.Get(@"Play/Catch/fruit-catcher-idle");
Children = new Drawable[]
{
caughtFruit = new Container<DrawableHitObject>
@@ -196,13 +192,7 @@ namespace osu.Game.Rulesets.Catch.UI
Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50);
}
private Sprite createCatcherSprite() => new Sprite
{
Size = new Vector2(CATCHER_SIZE),
FillMode = FillMode.Fill,
Texture = texture,
OriginPosition = new Vector2(-3, 10) // temporary until the sprite is aligned correctly.
};
private Sprite createCatcherSprite() => new CatcherSprite();
/// <summary>
/// Add a caught fruit to the catcher's stack.
@@ -411,6 +401,23 @@ namespace osu.Game.Rulesets.Catch.UI
f.Expire();
}
}
private class CatcherSprite : Sprite
{
public CatcherSprite()
{
Size = new Vector2(CATCHER_SIZE);
// Sets the origin roughly to the centre of the catcher's plate to allow for correct scaling.
OriginPosition = new Vector2(-0.02f, 0.06f) * CATCHER_SIZE;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Texture = textures.Get(@"Play/Catch/fruit-catcher-idle");
}
}
}
}
}
+5 -5
View File
@@ -22,7 +22,7 @@
},
"type": "mono",
"request": "launch",
"program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Mania.Tests.exe",
"program": "${workspaceRoot}/bin/Release/net471/osu.Game.Rulesets.Mania.Tests.exe",
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, msbuild)",
"runtimeExecutable": null,
@@ -30,12 +30,12 @@
"console": "internalConsole"
},
{
"name": "VisualTests (Debug, netcoreapp2.0)",
"name": "VisualTests (Debug, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Mania.Tests.dll"
"${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Mania.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug, dotnet)",
@@ -43,12 +43,12 @@
"console": "internalConsole"
},
{
"name": "VisualTests (Release, netcoreapp2.0)",
"name": "VisualTests (Release, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Mania.Tests.dll"
"${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Mania.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, dotnet)",
+3 -3
View File
@@ -40,7 +40,7 @@
"build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:GenerateFullPaths=true",
"/m",
"/verbosity:m"
@@ -56,7 +56,7 @@
"build",
"--no-restore",
"osu.Game.Rulesets.Mania.Tests.csproj",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:Configuration=Release",
"/p:GenerateFullPaths=true",
"/m",
@@ -75,7 +75,7 @@
"problemMatcher": []
},
{
"label": "Restore (netcoreapp2.0)",
"label": "Restore (netcoreapp2.1)",
"type": "shell",
"command": "dotnet",
"args": [
@@ -5,8 +5,6 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@@ -14,7 +12,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
{
public class ManiaBeatmapConversionTest : BeatmapConversionTest<TestManiaRuleset, ConvertValue>
internal class ManiaBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
@@ -35,10 +33,10 @@ namespace osu.Game.Rulesets.Mania.Tests
};
}
protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
protected override Ruleset CreateRuleset() => new ManiaRuleset();
}
public struct ConvertValue : IEquatable<ConvertValue>
internal struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
@@ -54,8 +52,4 @@ namespace osu.Game.Rulesets.Mania.Tests
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
&& Column == other.Column;
}
public class TestManiaRuleset : ManiaRuleset
{
}
}
@@ -4,6 +4,8 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Tests.Visual;
@@ -17,6 +19,14 @@ namespace osu.Game.Rulesets.Mania.Tests
{
public TestCaseManiaHitObjects()
{
Note note1 = new Note();
Note note2 = new Note();
HoldNote holdNote = new HoldNote { StartTime = 1000 };
note1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
note2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
holdNote.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
Add(new FillFlowContainer
{
Anchor = Anchor.Centre,
@@ -43,14 +53,14 @@ namespace osu.Game.Rulesets.Mania.Tests
RelativeChildSize = new Vector2(1, 10000),
Children = new[]
{
new DrawableNote(new Note(), ManiaAction.Key1)
new DrawableNote(note1, ManiaAction.Key1)
{
Y = 5000,
LifetimeStart = double.MinValue,
LifetimeEnd = double.MaxValue,
AccentColour = Color4.Red
},
new DrawableNote(new Note(), ManiaAction.Key1)
new DrawableNote(note2, ManiaAction.Key1)
{
Y = 6000,
LifetimeStart = double.MinValue,
@@ -77,13 +87,13 @@ namespace osu.Game.Rulesets.Mania.Tests
RelativeChildSize = new Vector2(1, 10000),
Children = new[]
{
new DrawableHoldNote(new HoldNote { Duration = 1000 } , ManiaAction.Key1)
new DrawableHoldNote(holdNote, ManiaAction.Key1)
{
Y = 5000,
Height = 1000,
LifetimeStart = double.MinValue,
LifetimeEnd = double.MaxValue,
AccentColour = Color4.Red
AccentColour = Color4.Red,
}
}
}
@@ -8,6 +8,8 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
@@ -83,13 +85,16 @@ namespace osu.Game.Rulesets.Mania.Tests
int col = rng.Next(0, 4);
var note = new DrawableNote(new Note { Column = col }, ManiaAction.Key1)
var note = new Note { Column = col };
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
var drawableNote = new DrawableNote(note, ManiaAction.Key1)
{
AccentColour = playfield.Columns.ElementAt(col).AccentColour
};
playfield.OnJudgement(note, new ManiaJudgement { Result = HitResult.Perfect });
playfield.Columns[col].OnJudgement(note, new ManiaJudgement { Result = HitResult.Perfect });
playfield.OnJudgement(drawableNote, new ManiaJudgement { Result = HitResult.Perfect });
playfield.Columns[col].OnJudgement(drawableNote, new ManiaJudgement { Result = HitResult.Perfect });
});
}
@@ -162,32 +167,24 @@ namespace osu.Game.Rulesets.Mania.Tests
for (double t = start_time; t <= start_time + duration; t += 100)
{
playfield.Add(new DrawableNote(new Note
{
StartTime = t,
Column = 0
}, ManiaAction.Key1));
var note1 = new Note { StartTime = t, Column = 0 };
var note2 = new Note { StartTime = t, Column = 3 };
playfield.Add(new DrawableNote(new Note
{
StartTime = t,
Column = 3
}, ManiaAction.Key4));
note1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
note2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
playfield.Add(new DrawableNote(note1, ManiaAction.Key1));
playfield.Add(new DrawableNote(note2, ManiaAction.Key4));
}
playfield.Add(new DrawableHoldNote(new HoldNote
{
StartTime = start_time,
Duration = duration,
Column = 1
}, ManiaAction.Key2));
var holdNote1 = new HoldNote { StartTime = start_time, Duration = duration, Column = 1 };
var holdNote2 = new HoldNote { StartTime = start_time, Duration = duration, Column = 2 };
playfield.Add(new DrawableHoldNote(new HoldNote
{
StartTime = start_time,
Duration = duration,
Column = 2
}, ManiaAction.Key3));
holdNote1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
holdNote2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
playfield.Add(new DrawableHoldNote(holdNote1, ManiaAction.Key2));
playfield.Add(new DrawableHoldNote(holdNote2, ManiaAction.Key3));
}
}
}
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
<TargetFrameworks>netcoreapp2.0;net471</TargetFrameworks>
<TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks>
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" />
@@ -59,7 +59,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap<ManiaHitObject> ConvertBeatmap(IBeatmap original)
{
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);
@@ -1,14 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
using System;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Mania
namespace osu.Game.Rulesets.Mania.Difficulty
{
internal class ManiaDifficultyCalculator : DifficultyCalculator
{
@@ -48,18 +50,17 @@ namespace osu.Game.Rulesets.Mania
int columnCount = (Beatmap as ManiaBeatmap)?.TotalColumns ?? 7;
foreach (var hitObject in Beatmap.HitObjects)
difficultyHitObjects.Add(new ManiaHitObjectDifficulty((ManiaHitObject)hitObject, columnCount));
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
// Note: Stable sort is done so that the ordering of hitobjects with equal start times doesn't change
difficultyHitObjects.AddRange(Beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime));
if (!calculateStrainValues())
return 0;
double starRating = calculateDifficulty() * star_scaling_factor;
categoryDifficulty?.Add("Strain", starRating);
if (categoryDifficulty != null)
categoryDifficulty["Strain"] = starRating;
return starRating;
}
@@ -0,0 +1,127 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaPerformanceCalculator : PerformanceCalculator
{
private Mod[] mods;
// Score after being scaled by non-difficulty-increasing mods
private double scaledScore;
private int countPerfect;
private int countGreat;
private int countGood;
private int countOk;
private int countMeh;
private int countMiss;
public ManiaPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
mods = Score.Mods;
scaledScore = Score.TotalScore;
countPerfect = Convert.ToInt32(Score.Statistics[HitResult.Perfect]);
countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
countOk = Convert.ToInt32(Score.Statistics[HitResult.Ok]);
countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
if (mods.Any(m => !m.Ranked))
return 0;
IEnumerable<Mod> scoreIncreaseMods = Ruleset.GetModsFor(ModType.DifficultyIncrease);
double scoreMultiplier = 1.0;
foreach (var m in mods.Where(m => !scoreIncreaseMods.Contains(m)))
scoreMultiplier *= m.ScoreMultiplier;
// Scale score up, so it's comparable to other keymods
scaledScore *= 1.0 / scoreMultiplier;
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed.
double multiplier = 0.8;
if (mods.Any(m => m is ModNoFail))
multiplier *= 0.9;
if (mods.Any(m => m is ModEasy))
multiplier *= 0.5;
double strainValue = computeStrainValue();
double accValue = computeAccuracyValue(strainValue);
double totalValue =
Math.Pow(
Math.Pow(strainValue, 1.1) +
Math.Pow(accValue, 1.1), 1.0 / 1.1
) * multiplier;
if (categoryDifficulty != null)
{
categoryDifficulty["Strain"] = strainValue;
categoryDifficulty["Accuracy"] = accValue;
}
return totalValue;
}
private double computeStrainValue()
{
// Obtain strain difficulty
double strainValue = Math.Pow(5 * Math.Max(1, Attributes["Strain"] / 0.2) - 4.0, 2.2) / 135.0;
// Longer maps are worth more
strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
if (scaledScore <= 500000)
strainValue = 0;
else if (scaledScore <= 600000)
strainValue *= (scaledScore - 500000) / 100000 * 0.3;
else if (scaledScore <= 700000)
strainValue *= 0.3 + (scaledScore - 600000) / 100000 * 0.25;
else if (scaledScore <= 800000)
strainValue *= 0.55 + (scaledScore - 700000) / 100000 * 0.20;
else if (scaledScore <= 900000)
strainValue *= 0.75 + (scaledScore - 800000) / 100000 * 0.15;
else
strainValue *= 0.90 + (scaledScore - 900000) / 100000 * 0.1;
return strainValue;
}
private double computeAccuracyValue(double strainValue)
{
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate;
if (hitWindowGreat <= 0)
return 0;
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Max(0.0, 0.2 - (hitWindowGreat - 34) * 0.006667)
* strainValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
// accuracyValue *= Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
return accuracyValue;
}
private double totalHits => countPerfect + countOk + countGreat + countGood + countMeh + countMiss;
}
}
@@ -0,0 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteJudgement : ManiaJudgement
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result) => 0;
}
}
+4
View File
@@ -15,7 +15,10 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania
{
@@ -23,6 +26,7 @@ namespace osu.Game.Rulesets.Mania
{
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new ManiaRulesetContainer(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score);
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
{
@@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Name => "Dual Stages";
public override string ShortenedName => "DS";
public override string Description => @"Double the stages, double the fun!";
public override double ScoreMultiplier => 0;
public override double ScoreMultiplier => 1;
private bool isForCurrentRuleset;
@@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string ShortenedName => "RD";
public override FontAwesome Icon => FontAwesome.fa_osu_dice;
public override string Description => @"Shuffle around the keys!";
public override double ScoreMultiplier => 0;
public override double ScoreMultiplier => 1;
public void ApplyToRulesetContainer(RulesetContainer<ManiaHitObject> rulesetContainer)
{
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
public class DrawableHoldNote : DrawableManiaHitObject<HoldNote>, IKeyBindingHandler<ManiaAction>
{
public override bool DisplayJudgement => false;
private readonly DrawableNote head;
private readonly DrawableNote tail;
@@ -99,6 +101,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void UpdateState(ArmedState state)
{
switch (state)
{
case ArmedState.Hit:
// Good enough for now, we just want them to have a lifetime end
this.Delay(2000).Expire();
break;
}
}
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
{
if (tail.AllJudged)
AddJudgement(new HoldNoteJudgement { Result = HitResult.Perfect });
}
protected override void Update()
@@ -191,6 +206,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary>
private class DrawableTailNote : DrawableNote
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps
/// </summary>
private const double release_window_lenience = 1.5;
private readonly DrawableHoldNote holdNote;
public DrawableTailNote(DrawableHoldNote holdNote, ManiaAction action)
@@ -203,6 +225,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected override void CheckForJudgements(bool userTriggered, double timeOffset)
{
// Factor in the release lenience
timeOffset /= release_window_lenience;
if (!userTriggered)
{
if (!HitObject.HitWindows.CanBeHit(timeOffset))
+1 -20
View File
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary>
/// The tail note of the hold.
/// </summary>
public readonly Note Tail = new TailNote();
public readonly Note Tail = new Note();
/// <summary>
/// The time between ticks of this hold.
@@ -94,24 +94,5 @@ namespace osu.Game.Rulesets.Mania.Objects
});
}
}
/// <summary>
/// The tail of the hold note.
/// </summary>
private class TailNote : Note
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// </summary>
private const double release_window_lenience = 1.5;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
HitWindows *= release_window_lenience;
}
}
}
}
@@ -1,8 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Objects.Types;
using osu.Game.Rulesets.Objects;
@@ -12,12 +10,6 @@ namespace osu.Game.Rulesets.Mania.Objects
{
public virtual int Column { get; set; }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
HitWindows.AllowsPerfect = true;
HitWindows.AllowsOk = true;
}
protected override HitWindows CreateHitWindows() => new ManiaHitWindows();
}
}
@@ -0,0 +1,36 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Objects
{
public class ManiaHitWindows : HitWindows
{
private static readonly IReadOnlyDictionary<HitResult, (double od0, double od5, double od10)> base_ranges = new Dictionary<HitResult, (double, double, double)>
{
{ HitResult.Perfect, (44.8, 38.8, 27.8) },
{ HitResult.Great, (128, 98, 68 ) },
{ HitResult.Good, (194, 164, 134) },
{ HitResult.Ok, (254, 224, 194) },
{ HitResult.Meh, (302, 272, 242) },
{ HitResult.Miss, (376, 346, 316) },
};
public override void SetDifficulty(double difficulty)
{
AllowsPerfect = true;
AllowsOk = true;
Perfect = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Perfect]);
Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]);
Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]);
Ok = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Ok]);
Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]);
Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]);
}
}
}
+1 -1
View File
@@ -213,7 +213,7 @@ namespace osu.Game.Rulesets.Mania.UI
internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement)
{
if (!judgement.IsHit)
if (!judgement.IsHit || !judgedObject.DisplayJudgement)
return;
explosionContainer.Add(new HitExplosion(judgedObject));
+3
View File
@@ -171,6 +171,9 @@ namespace osu.Game.Rulesets.Mania.UI
internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement)
{
if (!judgedObject.DisplayJudgement)
return;
judgements.Clear();
judgements.Add(new DrawableManiaJudgement(judgement, judgedObject)
{
+5 -5
View File
@@ -22,7 +22,7 @@
},
"type": "mono",
"request": "launch",
"program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Osu.Tests.exe",
"program": "${workspaceRoot}/bin/Release/net471/osu.Game.Rulesets.Osu.Tests.exe",
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, msbuild)",
"runtimeExecutable": null,
@@ -30,12 +30,12 @@
"console": "internalConsole"
},
{
"name": "VisualTests (Debug, netcoreapp2.0)",
"name": "VisualTests (Debug, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Osu.Tests.dll"
"${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Osu.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug, dotnet)",
@@ -43,12 +43,12 @@
"console": "internalConsole"
},
{
"name": "VisualTests (Release, netcoreapp2.0)",
"name": "VisualTests (Release, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Osu.Tests.dll"
"${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Osu.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, dotnet)",
+3 -3
View File
@@ -40,7 +40,7 @@
"build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:GenerateFullPaths=true",
"/m",
"/verbosity:m"
@@ -56,7 +56,7 @@
"build",
"--no-restore",
"osu.Game.Rulesets.Osu.Tests.csproj",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:Configuration=Release",
"/p:GenerateFullPaths=true",
"/m",
@@ -75,7 +75,7 @@
"problemMatcher": []
},
{
"label": "Restore (netcoreapp2.0)",
"label": "Restore (netcoreapp2.1)",
"type": "shell",
"command": "dotnet",
"args": [
@@ -5,17 +5,15 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class OsuBeatmapConversionTest : BeatmapConversionTest<TestOsuRuleset, ConvertValue>
internal class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
@@ -42,10 +40,10 @@ namespace osu.Game.Rulesets.Osu.Tests
};
}
protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap);
protected override Ruleset CreateRuleset() => new OsuRuleset();
}
public struct ConvertValue : IEquatable<ConvertValue>
internal struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
@@ -67,8 +65,4 @@ namespace osu.Game.Rulesets.Osu.Tests
&& Precision.AlmostEquals(EndX, other.EndX, conversion_lenience)
&& Precision.AlmostEquals(EndY, other.EndY, conversion_lenience);
}
public class TestOsuRuleset : OsuRuleset
{
}
}
@@ -93,12 +93,36 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("Big Single, Large StackOffset", () => testSimpleBigLargeStackOffset());
AddStep("Big 1 Repeat, Large StackOffset", () => testSimpleBigLargeStackOffset(1));
AddStep("Distance Overflow", () => testDistanceOverflow());
AddStep("Distance Overflow 1 Repeat", () => testDistanceOverflow(1));
}
private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
private void testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
private void testDistanceOverflow(int repeats = 0)
{
var slider = new Slider
{
StartTime = Time.Current + 1000,
Position = new Vector2(239, 176),
ControlPoints = new List<Vector2>
{
Vector2.Zero,
new Vector2(154, 28),
new Vector2(52, -34)
},
Distance = 700,
RepeatCount = repeats,
RepeatSamples = createEmptySamples(repeats),
StackHeight = 10
};
addSlider(slider, 2, 2);
}
private void testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats);
private void testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats);
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
<TargetFrameworks>netcoreapp2.0;net471</TargetFrameworks>
<TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks>
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" />
@@ -50,8 +50,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
StartTime = original.StartTime,
Samples = original.Samples,
EndTime = endTimeData.EndTime,
Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2,
Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2
};
}
else
@@ -4,12 +4,13 @@
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Skills;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.OsuDifficulty.Skills;
namespace osu.Game.Rulesets.Osu.OsuDifficulty
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuDifficultyCalculator : DifficultyCalculator
{
@@ -35,18 +36,22 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty
new Speed()
};
double sectionEnd = section_length / TimeRate;
double sectionLength = section_length * TimeRate;
// The first object doesn't generate a strain, so we begin with an incremented section end
double currentSectionEnd = 2 * sectionLength;
foreach (OsuDifficultyHitObject h in beatmap)
{
while (h.BaseObject.StartTime > sectionEnd)
while (h.BaseObject.StartTime > currentSectionEnd)
{
foreach (Skill s in skills)
{
s.SaveCurrentPeak();
s.StartNewSectionFrom(sectionEnd);
s.StartNewSectionFrom(currentSectionEnd);
}
sectionEnd += section_length;
currentSectionEnd += sectionLength;
}
foreach (Skill s in skills)
@@ -5,12 +5,13 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceCalculator : PerformanceCalculator
{
@@ -18,12 +19,22 @@ namespace osu.Game.Rulesets.Osu.Scoring
private readonly int beatmapMaxCombo;
private Mod[] mods;
/// <summary>
/// Approach rate adjusted by mods.
/// </summary>
private double realApproachRate;
/// <summary>
/// Overall difficulty adjusted by mods.
/// </summary>
private double realOverallDifficulty;
private double accuracy;
private int scoreMaxCombo;
private int count300;
private int count100;
private int count50;
private int countGreat;
private int countGood;
private int countMeh;
private int countMiss;
public OsuPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
@@ -32,7 +43,8 @@ namespace osu.Game.Rulesets.Osu.Scoring
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
beatmapMaxCombo = Beatmap.HitObjects.Count();
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count) + 1;
// Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above)
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
}
public override double Calculate(Dictionary<string, double> categoryRatings = null)
@@ -40,25 +52,21 @@ namespace osu.Game.Rulesets.Osu.Scoring
mods = Score.Mods;
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo;
count300 = Convert.ToInt32(Score.Statistics[HitResult.Great]);
count100 = Convert.ToInt32(Score.Statistics[HitResult.Good]);
count50 = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
return 0;
// Todo: In the future we should apply changes to PreEmpt/AR at an OsuHitObject/BaseDifficulty level, but this is done
// locally for now as doing so would modify animations and other things unexpectedly
// DO NOT MODIFY THIS
double ar = Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate;
if (mods.Any(m => m is OsuModHardRock))
ar = Math.Min(10, ar * 1.4);
if (mods.Any(m => m is OsuModEasy))
ar = Math.Max(0, ar / 2);
double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450);
// Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate;
double preEmpt = (int)BeatmapDifficulty.DifficultyRange(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / TimeRate;
realApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5;
realOverallDifficulty = (80 - hitWindowGreat) / 6;
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
@@ -84,6 +92,9 @@ namespace osu.Game.Rulesets.Osu.Scoring
categoryRatings.Add("Aim", aimValue);
categoryRatings.Add("Speed", speedValue);
categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("OD", realOverallDifficulty);
categoryRatings.Add("AR", realApproachRate);
categoryRatings.Add("Max Combo", beatmapMaxCombo);
}
return totalValue;
@@ -120,8 +131,9 @@ namespace osu.Game.Rulesets.Osu.Scoring
aimValue *= approachRateFactor;
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.18f;
aimValue *= 1.02 + (11.0f - realApproachRate) / 50.0; // Gives a 1.04 bonus for AR10, a 1.06 bonus for AR9, a 1.02 bonus for AR11.
if (mods.Any(h => h is OsuModFlashlight))
{
@@ -132,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
// Scale the aim value with accuracy _slightly_
aimValue *= 0.5f + accuracy / 2.0f;
// It is important to also consider accuracy difficulty when doing that
aimValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500;
aimValue *= 0.98f + Math.Pow(realOverallDifficulty, 2) / 2500;
return aimValue;
}
@@ -152,10 +164,13 @@ namespace osu.Game.Rulesets.Osu.Scoring
if (beatmapMaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
if (mods.Any(m => m is OsuModHidden))
speedValue *= 1.18f;
// Scale the speed value with accuracy _slightly_
speedValue *= 0.5f + accuracy / 2.0f;
// It is important to also consider accuracy difficulty when doing that
speedValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500;
speedValue *= 0.98f + Math.Pow(realOverallDifficulty, 2) / 2500;
return speedValue;
}
@@ -167,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
int amountHitObjectsWithAccuracy = countHitCircles;
if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((count300 - (totalHits - amountHitObjectsWithAccuracy)) * 6 + count100 * 2 + count50) / (amountHitObjectsWithAccuracy * 6);
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (amountHitObjectsWithAccuracy * 6);
else
betterAccuracyPercentage = 0;
@@ -177,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Pow(1.52163f, Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f;
double accuracyValue = Math.Pow(1.52163f, realOverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f));
@@ -190,7 +205,7 @@ namespace osu.Game.Rulesets.Osu.Scoring
return accuracyValue;
}
private double totalHits => count300 + count100 + count50 + countMiss;
private double totalSuccessfulHits => count300 + count100 + count50;
private double totalHits => countGreat + countGood + countMeh + countMiss;
private double totalSuccessfulHits => countGreat + countGood + countMeh;
}
}
@@ -0,0 +1,44 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
/// <summary>
/// An enumerable container wrapping <see cref="OsuHitObject"/> input as <see cref="OsuDifficultyHitObject"/>
/// which contains extra data required for difficulty calculation.
/// </summary>
public class OsuDifficultyBeatmap : IEnumerable<OsuDifficultyHitObject>
{
private readonly IEnumerator<OsuDifficultyHitObject> difficultyObjects;
/// <summary>
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
/// <see cref="OsuDifficultyHitObject"/> which contains extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyBeatmap(List<OsuHitObject> objects, double timeRate)
{
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
// This should probably happen before the objects reach the difficulty calculator.
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate);
}
/// <summary>
/// Returns an enumerator that enumerates all <see cref="OsuDifficultyHitObject"/>s in the <see cref="OsuDifficultyBeatmap"/>.
/// </summary>
public IEnumerator<OsuDifficultyHitObject> GetEnumerator() => difficultyObjects;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects, double timeRate)
{
// The first jump is formed by the first two hitobjects of the map.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < objects.Count; i++)
yield return new OsuDifficultyHitObject(objects[i], objects[i - 1], timeRate);
}
}
}
@@ -3,16 +3,18 @@
using System;
using System.Linq;
using OpenTK;
using osu.Game.Rulesets.Osu.Objects;
using OpenTK;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
/// <summary>
/// A wrapper around <see cref="OsuHitObject"/> extending it with additional data required for difficulty calculation.
/// </summary>
public class OsuDifficultyHitObject
{
private const int normalized_radius = 52;
/// <summary>
/// The <see cref="OsuHitObject"/> this <see cref="OsuDifficultyHitObject"/> refers to.
/// </summary>
@@ -28,26 +30,19 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
/// </summary>
public double DeltaTime { get; private set; }
/// <summary>
/// Number of milliseconds until the <see cref="OsuDifficultyHitObject"/> has to be hit.
/// </summary>
public double TimeUntilHit { get; set; }
private const int normalized_radius = 52;
private readonly OsuHitObject lastObject;
private readonly double timeRate;
private readonly OsuHitObject[] t;
/// <summary>
/// Initializes the object calculating extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyHitObject(OsuHitObject[] triangle, double timeRate)
public OsuDifficultyHitObject(OsuHitObject currentObject, OsuHitObject lastObject, double timeRate)
{
this.lastObject = lastObject;
this.timeRate = timeRate;
t = triangle;
BaseObject = t[0];
BaseObject = currentObject;
setDistances();
setTimingValues();
// Calculate angle here
@@ -63,10 +58,10 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
scalingFactor *= 1 + smallCircleBonus;
}
Vector2 lastCursorPosition = t[1].StackedPosition;
Vector2 lastCursorPosition = lastObject.StackedPosition;
float lastTravelDistance = 0;
var lastSlider = t[1] as Slider;
var lastSlider = lastObject as Slider;
if (lastSlider != null)
{
computeSliderCursorPosition(lastSlider);
@@ -80,8 +75,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
private void setTimingValues()
{
// Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
DeltaTime = Math.Max(40, (t[0].StartTime - t[1].StartTime) / timeRate);
TimeUntilHit = 450; // BaseObject.PreEmpt;
DeltaTime = Math.Max(50, (BaseObject.StartTime - lastObject.StartTime) / timeRate);
}
private void computeSliderCursorPosition(Slider slider)
@@ -107,7 +101,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
}
});
var scoringTimes = slider.NestedHitObjects.Select(t => t.StartTime);
// Skip the head circle
var scoringTimes = slider.NestedHitObjects.Skip(1).Select(t => t.StartTime);
foreach (var time in scoringTimes)
computeVertex(time);
computeVertex(slider.EndTime);
@@ -2,9 +2,9 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
/// <summary>
/// Represents the skill required to correctly aim at every object in the map with a uniform CircleSize and normalized distances.
@@ -3,11 +3,11 @@
using System;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.OsuDifficulty.Utils;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
/// <summary>
/// Used to processes strain values of <see cref="OsuDifficultyHitObject"/>s, keep track of strain levels caused by the processed objects
@@ -1,9 +1,9 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Skills
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{
/// <summary>
/// Represents the skill required to press keys with regards to keeping up with the speed at which objects need to be hit.
@@ -5,7 +5,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Utils
namespace osu.Game.Rulesets.Osu.Difficulty.Utils
{
/// <summary>
/// An indexed stack with Push() only, which disposes items at the bottom after the capacity is full.
@@ -13,8 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string ShortenedName => "AP";
public override FontAwesome Icon => FontAwesome.fa_osu_mod_autopilot;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 0;
public override bool Ranked => false;
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) };
}
}
@@ -71,5 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects
}
public virtual void OffsetPosition(Vector2 offset) => Position += offset;
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
}
}
@@ -0,0 +1,29 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class OsuHitWindows : HitWindows
{
private static readonly IReadOnlyDictionary<HitResult, (double od0, double od5, double od10)> base_ranges = new Dictionary<HitResult, (double, double, double)>
{
{ HitResult.Great, (160, 100, 40) },
{ HitResult.Good, (280, 200, 120) },
{ HitResult.Meh, (400, 300, 200) },
{ HitResult.Miss, (400, 400, 400) },
};
public override void SetDifficulty(double difficulty)
{
Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]);
Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]);
Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]);
Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]);
}
}
}
@@ -1,94 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections;
using System.Collections.Generic;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
{
/// <summary>
/// An enumerable container wrapping <see cref="OsuHitObject"/> input as <see cref="OsuDifficultyHitObject"/>
/// which contains extra data required for difficulty calculation.
/// </summary>
public class OsuDifficultyBeatmap : IEnumerable<OsuDifficultyHitObject>
{
private readonly IEnumerator<OsuDifficultyHitObject> difficultyObjects;
private readonly Queue<OsuDifficultyHitObject> onScreen = new Queue<OsuDifficultyHitObject>();
/// <summary>
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
/// <see cref="OsuDifficultyHitObject"/> which contains extra data required for difficulty calculation.
/// </summary>
public OsuDifficultyBeatmap(List<OsuHitObject> objects, double timeRate)
{
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
// This should probably happen before the objects reach the difficulty calculator.
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate);
}
/// <summary>
/// Returns an enumerator that enumerates all <see cref="OsuDifficultyHitObject"/>s in the <see cref="OsuDifficultyBeatmap"/>.
/// The inner loop adds objects that appear on screen into a queue until we need to hit the next object.
/// The outer loop returns objects from this queue one at a time, only after they had to be hit, and should no longer be on screen.
/// This means that we can loop through every object that is on screen at the time when a new one appears,
/// allowing us to determine a reading strain for the object that just appeared.
/// </summary>
public IEnumerator<OsuDifficultyHitObject> GetEnumerator()
{
while (true)
{
// Add upcoming objects to the queue until we have at least one object that had been hit and can be dequeued.
// This means there is always at least one object in the queue unless we reached the end of the map.
do
{
if (!difficultyObjects.MoveNext())
break; // New objects can't be added anymore, but we still need to dequeue and return the ones already on screen.
OsuDifficultyHitObject latest = difficultyObjects.Current;
// Calculate flow values here
foreach (OsuDifficultyHitObject h in onScreen)
{
// ReSharper disable once PossibleNullReferenceException (resharper not smart enough to understand IEnumerator.MoveNext())
h.TimeUntilHit -= latest.DeltaTime;
// Calculate reading strain here
}
onScreen.Enqueue(latest);
}
while (onScreen.Peek().TimeUntilHit > 0); // Keep adding new objects on screen while there is still time before we have to hit the next one.
if (onScreen.Count == 0) break; // We have reached the end of the map and enumerated all the objects.
yield return onScreen.Dequeue(); // Remove and return objects one by one that had to be hit before the latest one appeared.
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects, double timeRate)
{
// We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object.
OsuHitObject[] triangle = new OsuHitObject[3];
// OsuDifficultyHitObject construction requires three components, an extra copy of the first OsuHitObject is used at the beginning.
if (objects.Count > 1)
{
triangle[1] = objects[0]; // This copy will get shifted to the last spot in the triangle.
triangle[0] = objects[0]; // This component corresponds to the real first OsuHitOject.
}
// The final component of the first triangle will be the second OsuHitOject of the map, which forms the first jump.
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
for (int i = 1; i < objects.Count; ++i)
{
triangle[2] = triangle[1];
triangle[1] = triangle[0];
triangle[0] = objects[i];
yield return new OsuDifficultyHitObject(triangle, timeRate);
}
}
}
}
+2 -2
View File
@@ -5,7 +5,6 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.OsuDifficulty;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using System.Collections.Generic;
@@ -13,13 +12,14 @@ using osu.Framework.Graphics;
using osu.Game.Overlays.Settings;
using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Difficulty;
namespace osu.Game.Rulesets.Osu
{
+5 -5
View File
@@ -22,7 +22,7 @@
},
"type": "mono",
"request": "launch",
"program": "${workspaceRoot}/bin/Debug/net471/osu.Game.Rulesets.Taiko.Tests.exe",
"program": "${workspaceRoot}/bin/Release/net471/osu.Game.Rulesets.Taiko.Tests.exe",
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, msbuild)",
"runtimeExecutable": null,
@@ -30,12 +30,12 @@
"console": "internalConsole"
},
{
"name": "VisualTests (Debug, netcoreapp2.0)",
"name": "VisualTests (Debug, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Taiko.Tests.dll"
"${workspaceRoot}/bin/Debug/netcoreapp2.1/osu.Game.Rulesets.Taiko.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Debug, dotnet)",
@@ -43,12 +43,12 @@
"console": "internalConsole"
},
{
"name": "VisualTests (Release, netcoreapp2.0)",
"name": "VisualTests (Release, netcoreapp2.1)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/bin/Debug/netcoreapp2.0/osu.Game.Rulesets.Taiko.Tests.dll"
"${workspaceRoot}/bin/Release/netcoreapp2.1/osu.Game.Rulesets.Taiko.Tests.dll"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build (Release, dotnet)",
+3 -3
View File
@@ -40,7 +40,7 @@
"build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:GenerateFullPaths=true",
"/m",
"/verbosity:m"
@@ -56,7 +56,7 @@
"build",
"--no-restore",
"osu.Game.Rulesets.Taiko.Tests.csproj",
"/p:TargetFramework=netcoreapp2.0",
"/p:TargetFramework=netcoreapp2.1",
"/p:Configuration=Release",
"/p:GenerateFullPaths=true",
"/m",
@@ -75,7 +75,7 @@
"problemMatcher": []
},
{
"label": "Restore (netcoreapp2.0)",
"label": "Restore (netcoreapp2.1)",
"type": "shell",
"command": "dotnet",
"args": [
@@ -5,22 +5,20 @@ using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests
{
public class TaikoBeatmapConversionTest : BeatmapConversionTest<TestTaikoRuleset, ConvertValue>
internal class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[NonParallelizable]
[TestCase("basic", false), Ignore("See: https://github.com/ppy/osu/issues/2152")]
[TestCase("slider-generating-drumroll", false)]
[TestCase("basic")]
[TestCase("slider-generating-drumroll")]
public new void Test(string name)
{
base.Test(name);
@@ -40,10 +38,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
};
}
protected override IBeatmapConverter CreateConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap);
protected override Ruleset CreateRuleset() => new TaikoRuleset();
}
public struct ConvertValue : IEquatable<ConvertValue>
internal struct ConvertValue : IEquatable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
@@ -67,8 +65,4 @@ namespace osu.Game.Rulesets.Taiko.Tests
&& IsSwell == other.IsSwell
&& IsStrong == other.IsStrong;
}
public class TestTaikoRuleset : TaikoRuleset
{
}
}
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
<TargetFrameworks>netcoreapp2.0;net471</TargetFrameworks>
<TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks>
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
@@ -8,7 +8,6 @@ using osu.Game.Rulesets.Taiko.Objects;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.IO.Serialization;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
@@ -51,8 +50,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
protected override Beatmap<TaikoHitObject> ConvertBeatmap(IBeatmap original)
{
// Rewrite the beatmap info to add the slider velocity multiplier
BeatmapInfo info = original.BeatmapInfo.DeepClone();
info.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier;
original.BeatmapInfo = original.BeatmapInfo.Clone();
original.BeatmapInfo.BaseDifficulty = original.BeatmapInfo.BaseDifficulty.Clone();
original.BeatmapInfo.BaseDifficulty.SliderMultiplier *= legacy_velocity_multiplier;
Beatmap<TaikoHitObject> converted = base.ConvertBeatmap(original);
@@ -98,12 +98,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double distance = distanceData.Distance * spans * 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.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength;
double taikoVelocity = taiko_base_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the taiko hit object
double taikoDuration = distance / taikoVelocity;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength;
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
@@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
StartTime = j,
Samples = currentSamples,
IsStrong = strong,
IsStrong = strong
};
}
@@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
Samples = obj.Samples,
IsStrong = strong,
Duration = taikoDuration,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4,
TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4
};
}
}
@@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
Samples = obj.Samples,
IsStrong = strong,
Duration = endTimeData.Duration,
RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier),
RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier)
};
}
else
@@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong,
IsStrong = strong
};
}
else
@@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{
StartTime = obj.StartTime,
Samples = obj.Samples,
IsStrong = strong,
IsStrong = strong
};
}
}
@@ -1,12 +1,14 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko
namespace osu.Game.Rulesets.Taiko.Difficulty
{
internal class TaikoDifficultyCalculator : DifficultyCalculator
{
@@ -34,6 +36,11 @@ namespace osu.Game.Rulesets.Taiko
{
}
public TaikoDifficultyCalculator(IBeatmap beatmap, Mod[] mods)
: base(beatmap, mods)
{
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
// Fill our custom DifficultyHitObject class, that carries additional information
@@ -50,10 +57,7 @@ namespace osu.Game.Rulesets.Taiko
double starRating = calculateDifficulty() * star_scaling_factor;
if (categoryDifficulty != null)
{
categoryDifficulty.Add("Strain", starRating);
categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate);
}
categoryDifficulty["Strain"] = starRating;
return starRating;
}
@@ -0,0 +1,112 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoPerformanceCalculator : PerformanceCalculator
{
private readonly int beatmapMaxCombo;
private Mod[] mods;
private int countGreat;
private int countGood;
private int countMeh;
private int countMiss;
public TaikoPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
beatmapMaxCombo = beatmap.HitObjects.Count(h => h is Hit);
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
mods = Score.Mods;
countGreat = Convert.ToInt32(Score.Statistics[HitResult.Great]);
countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]);
countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
// Don't count scores made with supposedly unranked mods
if (mods.Any(m => !m.Ranked))
return 0;
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is ModNoFail))
multiplier *= 0.90;
if (mods.Any(m => m is ModHidden))
multiplier *= 1.10;
double strainValue = computeStrainValue();
double accuracyValue = computeAccuracyValue();
double totalValue =
Math.Pow(
Math.Pow(strainValue, 1.1) +
Math.Pow(accuracyValue, 1.1), 1.0 / 1.1
) * multiplier;
if (categoryDifficulty != null)
{
categoryDifficulty["Strain"] = strainValue;
categoryDifficulty["Accuracy"] = accuracyValue;
}
return totalValue;
}
private double computeStrainValue()
{
double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes["Strain"] / 0.0075) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more
double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0);
strainValue *= lengthBonus;
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
strainValue *= Math.Pow(0.985, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(beatmapMaxCombo, 0.5), 1.0);
if (mods.Any(m => m is ModHidden))
strainValue *= 1.025;
if (mods.Any(m => m is ModFlashlight))
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
strainValue *= 1.05 * lengthBonus;
// Scale the speed value with accuracy _slightly_
return strainValue * Score.Accuracy;
}
private double computeAccuracyValue()
{
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be remoevd in the future
double hitWindowGreat = (int)(Beatmap.HitObjects.First().HitWindows.Great / 2) / TimeRate;
if (hitWindowGreat <= 0)
return 0;
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accValue = Math.Pow(150.0 / hitWindowGreat, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
}
private int totalHits => countGreat + countGood + countMeh + countMiss;
}
}
+6 -3
View File
@@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary>
private double tickSpacing = 100;
private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -47,9 +49,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
tickSpacing = timingPoint.BeatLength / TickRate;
RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * difficulty.OverallDifficulty);
RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * difficulty.OverallDifficulty);
overallDifficulty = difficulty.OverallDifficulty;
}
protected override void CreateNestedHitObjects()
@@ -57,6 +57,9 @@ namespace osu.Game.Rulesets.Taiko.Objects
base.CreateNestedHitObjects();
createTicks();
RequiredGoodHits = NestedHitObjects.Count * Math.Min(0.15, 0.05 + 0.10 / 6 * overallDifficulty);
RequiredGreatHits = NestedHitObjects.Count * Math.Min(0.30, 0.10 + 0.20 / 6 * overallDifficulty);
}
private void createTicks()
@@ -27,5 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// Strong hit objects give more points for hitting the hit object with both keys.
/// </summary>
public bool IsStrong;
protected override HitWindows CreateHitWindows() => new TaikoHitWindows();
}
}
@@ -0,0 +1,29 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class TaikoHitWindows : HitWindows
{
private static readonly IReadOnlyDictionary<HitResult, (double od0, double od5, double od10)> base_ranges = new Dictionary<HitResult, (double, double, double)>
{
{ HitResult.Great, (100, 70, 40) },
{ HitResult.Good, (240, 160, 100) },
{ HitResult.Meh, (270, 190, 140) },
{ HitResult.Miss, (400, 400, 400) },
};
public override void SetDifficulty(double difficulty)
{
Great = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Great]);
Good = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Good]);
Meh = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Meh]);
Miss = BeatmapDifficulty.DifficultyRange(difficulty, base_ranges[HitResult.Miss]);
}
}
}
+6 -1
View File
@@ -13,7 +13,10 @@ using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Difficulty;
namespace osu.Game.Rulesets.Taiko
{
@@ -142,7 +145,9 @@ namespace osu.Game.Rulesets.Taiko
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o };
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap);
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap, mods);
public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new TaikoPerformanceCalculator(this, beatmap, score);
public override int? LegacyID => 1;
@@ -9,6 +9,7 @@ using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Tests.Resources;
using OpenTK;
@@ -117,7 +118,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
public void TestParity(string beatmap)
{
var legacy = decode(beatmap, out Beatmap json);
json.ShouldDeepEqual(legacy);
json.WithDeepEqual(legacy).IgnoreProperty(r => r.DeclaringType == typeof(HitWindows)).Assert();
}
/// <summary>
+79 -21
View File
@@ -12,6 +12,7 @@ using osu.Framework.Platform;
using osu.Game.IPC;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using SharpCompress.Archives.Zip;
namespace osu.Game.Tests.Beatmaps.IO
{
@@ -77,8 +78,69 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>();
Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1);
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1);
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestRollbackOnFailure()
{
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestRollbackOnFailure"))
{
try
{
var osu = loadOsu(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
int fireCount = 0;
// ReSharper disable once AccessToModifiedClosure
manager.ItemAdded += _ => fireCount++;
manager.ItemRemoved += _ => fireCount++;
var imported = loadOszIntoOsu(osu);
Assert.AreEqual(0, fireCount -= 1);
imported.Hash += "-changed";
manager.Update(imported);
Assert.AreEqual(0, fireCount -= 2);
var breakTemp = createTemporaryBeatmap();
MemoryStream brokenOsu = new MemoryStream(new byte[] { 1, 3, 3, 7 });
MemoryStream brokenOsz = new MemoryStream(File.ReadAllBytes(breakTemp));
File.Delete(breakTemp);
using (var outStream = File.Open(breakTemp, FileMode.CreateNew))
using (var zip = ZipArchive.Open(brokenOsz))
{
zip.AddEntry("broken.osu", brokenOsu, false);
zip.SaveTo(outStream, SharpCompress.Common.CompressionType.Deflate);
}
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
Assert.AreEqual(12, manager.QueryBeatmaps(_ => true).ToList().Count);
// this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
manager.Import(breakTemp);
// no events should be fired in the case of a rollback.
Assert.AreEqual(0, fireCount);
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
Assert.AreEqual(12, manager.QueryBeatmaps(_ => true).ToList().Count);
}
finally
{
@@ -100,18 +162,17 @@ namespace osu.Game.Tests.Beatmaps.IO
var imported = loadOszIntoOsu(osu);
//var change = manager.QueryBeatmapSets(_ => true).First();
imported.Hash += "-changed";
manager.Update(imported);
var importedSecondTime = loadOszIntoOsu(osu);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID);
Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1);
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1);
// only one beatmap will exist as the online set ID matched, causing purging of the first import.
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
}
finally
{
@@ -162,8 +223,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var osu = loadOsu(host);
var temp = prepareTempCopy(osz_path);
Assert.IsTrue(File.Exists(temp));
var temp = createTemporaryBeatmap();
var importer = new ArchiveImportIPCChannel(client);
if (!importer.ImportAsync(temp).Wait(10000))
@@ -188,8 +248,7 @@ namespace osu.Game.Tests.Beatmaps.IO
try
{
var osu = loadOsu(host);
var temp = prepareTempCopy(osz_path);
Assert.IsTrue(File.Exists(temp), "Temporary file copy never substantiated");
var temp = createTemporaryBeatmap();
using (File.OpenRead(temp))
osu.Dependencies.Get<BeatmapManager>().Import(temp);
ensureLoaded(osu);
@@ -203,11 +262,16 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
private BeatmapSetInfo loadOszIntoOsu(OsuGameBase osu)
private string createTemporaryBeatmap()
{
var temp = prepareTempCopy(osz_path);
var temp = new FileInfo(osz_path).CopyTo(Path.GetTempFileName(), true).FullName;
Assert.IsTrue(File.Exists(temp));
return temp;
}
private BeatmapSetInfo loadOszIntoOsu(OsuGameBase osu, string path = null)
{
var temp = path ?? createTemporaryBeatmap();
var manager = osu.Dependencies.Get<BeatmapManager>();
@@ -219,7 +283,7 @@ namespace osu.Game.Tests.Beatmaps.IO
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
return imported.FirstOrDefault();
return imported.LastOrDefault();
}
private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu)
@@ -228,16 +292,10 @@ namespace osu.Game.Tests.Beatmaps.IO
manager.Delete(imported);
Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0);
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending);
}
private string prepareTempCopy(string path)
{
var temp = Path.GetTempFileName();
return new FileInfo(path).CopyTo(temp, true).FullName;
}
private OsuGameBase loadOsu(GameHost host)
{
var osu = new OsuGameBase();
+15 -1
View File
@@ -5,6 +5,7 @@ using System.ComponentModel;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual
@@ -15,7 +16,20 @@ namespace osu.Game.Tests.Visual
protected override Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset)
{
beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() });
return base.CreatePlayer(beatmap, ruleset);
return new ScoreAccessiblePlayer
{
InitialBeatmap = beatmap,
AllowPause = false,
AllowLeadIn = false,
AllowResults = false,
};
}
protected override bool ContinueCondition(Player player) => base.ContinueCondition(player) && ((ScoreAccessiblePlayer)player).ScoreProcessor.TotalScore > 0;
private class ScoreAccessiblePlayer : Player
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
}
}
}
@@ -65,11 +65,7 @@ namespace osu.Game.Tests.Visual
carousel.SelectionChanged = s => currentSelection = s;
AddStep("Load Beatmaps", () => { carousel.BeatmapSets = beatmapSets; });
bool changed = false;
carousel.BeatmapSetsChanged = () => changed = true;
AddUntilStep(() => changed, "Wait for load");
loadBeatmaps(beatmapSets);
testTraversal();
testFiltering();
@@ -84,6 +80,17 @@ namespace osu.Game.Tests.Visual
testCarouselRootIsRandom();
}
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets)
{
bool changed = false;
AddStep($"Load {beatmapSets.Count} Beatmaps", () =>
{
carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets;
});
AddUntilStep(() => changed, "Wait for load");
}
private void ensureRandomFetchSuccess() =>
AddAssert("ensure prev random fetch worked", () => selectedSets.Peek() == carousel.SelectedBeatmapSet);
@@ -423,7 +430,7 @@ namespace osu.Game.Tests.Visual
for (int i = 1; i <= 50; i++)
beatmapSets.Add(createTestBeatmapSet(i));
AddStep("Load 50 Beatmaps", () => { carousel.BeatmapSets = beatmapSets; });
loadBeatmaps(beatmapSets);
advanceSelection(direction: 1, diff: false);
checkNonmatchingFilter();
checkNonmatchingFilter();
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual
AddStep("show", () =>
{
infoWedge.State = Visibility.Visible;
infoWedge.UpdateBeatmap(beatmap);
infoWedge.Beatmap = beatmap;
});
// select part is redundant, but wait for load isn't
@@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual
AddStep($"select {b.Metadata.Title} beatmap", () =>
{
infoBefore = infoWedge.Info;
infoWedge.UpdateBeatmap(beatmap.Value = new TestWorkingBeatmap(b));
infoWedge.Beatmap = beatmap.Value = new TestWorkingBeatmap(b);
});
AddUntilStep(() => infoWedge.Info != infoBefore, "wait for async load");
@@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual
AddStep("select null beatmap", () =>
{
beatmap.Value = beatmap.Default;
infoWedge.UpdateBeatmap(beatmap);
infoWedge.Beatmap = beatmap;
});
}
+6 -6
View File
@@ -19,12 +19,12 @@ namespace osu.Game.Tests.Visual
[TestFixture]
public class TestCaseCursors : ManualInputManagerTestCase
{
private readonly CursorOverrideContainer cursorOverrideContainer;
private readonly MenuCursorContainer menuCursorContainer;
private readonly CustomCursorBox[] cursorBoxes = new CustomCursorBox[6];
public TestCaseCursors()
{
Child = cursorOverrideContainer = new CursorOverrideContainer
Child = menuCursorContainer = new MenuCursorContainer
{
RelativeSizeAxes = Axes.Both,
Children = new[]
@@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual
AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor));
AddStep("Move out", moveOut);
AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(cursorOverrideContainer.Cursor));
AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor));
}
/// <summary>
@@ -112,11 +112,11 @@ namespace osu.Game.Tests.Visual
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(cursorOverrideContainer.Cursor));
AddAssert("Check global cursor at mouse", () => checkAtMouse(cursorOverrideContainer.Cursor));
AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor));
AddAssert("Check global cursor at mouse", () => checkAtMouse(menuCursorContainer.Cursor));
AddStep("Move out", moveOut);
AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(cursorOverrideContainer.Cursor));
AddAssert("Check global cursor visible", () => checkVisible(menuCursorContainer.Cursor));
}
/// <summary>
@@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.Components;
@@ -111,6 +112,7 @@ namespace osu.Game.Tests.Visual
}
});
AddStep(@"select", () => first.State = SelectionState.Selected);
AddStep(@"change title", () => first.Room.Name.Value = @"I Changed Name");
AddStep(@"change host", () => first.Room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } });
AddStep(@"change status", () => first.Room.Status.Value = new RoomStatusPlaying());
@@ -121,6 +123,7 @@ namespace osu.Game.Tests.Visual
new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 1254 } } },
new User { Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 123189 } } },
});
AddStep(@"deselect", () => first.State = SelectionState.NotSelected);
}
[BackgroundDependencyLoader]
@@ -0,0 +1,23 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Game.Graphics.UserInterface;
using OpenTK;
namespace osu.Game.Tests.Visual
{
public class TestCaseExternalLinkButton : OsuTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(ExternalLinkButton) };
public TestCaseExternalLinkButton()
{
Child = new ExternalLinkButton("https://osu.ppy.sh/home")
{
Size = new Vector2(50)
};
}
}
}
@@ -0,0 +1,62 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Menu;
namespace osu.Game.Tests.Visual
{
public class TestCaseHoldToConfirmOverlay : OsuTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(ExitConfirmOverlay) };
public TestCaseHoldToConfirmOverlay()
{
bool fired = false;
var firedText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Fired!",
TextSize = 50,
Alpha = 0,
};
var overlay = new TestHoldToConfirmOverlay
{
Action = () =>
{
fired = true;
firedText.FadeTo(1).Then().FadeOut(1000);
}
};
Children = new Drawable[]
{
overlay,
firedText
};
AddStep("start confirming", () => overlay.Begin());
AddStep("abort confirming", () => overlay.Abort());
AddAssert("ensure aborted", () => !fired);
AddStep("start confirming", () => overlay.Begin());
AddUntilStep(() => fired, "wait until confirmed");
}
private class TestHoldToConfirmOverlay : ExitConfirmOverlay
{
protected override bool AllowMultipleFires => true;
public void Begin() => BeginConfirm();
public void Abort() => AbortConfirm();
}
}
}
@@ -0,0 +1,115 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using OpenTK.Graphics;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseLoaderAnimation : OsuTestCase
{
private TestLoader loader;
protected override void LoadComplete()
{
base.LoadComplete();
// required to preload the logo in a headless run (so it doesn't delay the loading itself).
Add(new OsuLogo());
bool logoVisible = false;
AddStep("almost instant display", () => Child = loader = new TestLoader(250));
AddUntilStep(() =>
{
logoVisible = loader.Logo?.Alpha > 0;
return loader.Logo != null && loader.ScreenLoaded;
}, "loaded");
AddAssert("logo not visible", () => !logoVisible);
AddStep("short load", () => Child = loader = new TestLoader(800));
AddUntilStep(() =>
{
logoVisible = loader.Logo?.Alpha > 0;
return loader.Logo != null && loader.ScreenLoaded;
}, "loaded");
AddAssert("logo visible", () => logoVisible);
AddUntilStep(() => loader.Logo?.Alpha == 0, "logo gone");
AddStep("longer load", () => Child = loader = new TestLoader(1400));
AddUntilStep(() =>
{
logoVisible = loader.Logo?.Alpha > 0;
return loader.Logo != null && loader.ScreenLoaded;
}, "loaded");
AddAssert("logo visible", () => logoVisible);
AddUntilStep(() => loader.Logo?.Alpha == 0, "logo gone");
}
private class TestLoader : Loader
{
private readonly double delay;
public OsuLogo Logo;
private TestScreen screen;
public bool ScreenLoaded => screen.IsCurrentScreen;
public TestLoader(double delay)
{
this.delay = delay;
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
Logo = logo;
base.LogoArriving(logo, resuming);
}
protected override OsuScreen CreateLoadableScreen() => screen = new TestScreen();
protected override ShaderPrecompiler CreateShaderPrecompiler() => new TestShaderPrecompiler(delay);
private class TestShaderPrecompiler : ShaderPrecompiler
{
private readonly double delay;
private double startTime;
public TestShaderPrecompiler(double delay)
{
this.delay = delay;
}
protected override void LoadComplete()
{
base.LoadComplete();
startTime = Time.Current;
}
protected override bool AllLoaded => Time.Current > startTime + delay;
}
private class TestScreen : OsuScreen
{
public TestScreen()
{
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.DarkSlateGray,
Alpha = 0,
};
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
Child.FadeInFromZero(200);
}
}
}
}
}
+211
View File
@@ -0,0 +1,211 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets;
using osu.Game.Screens.Multi.Components;
using osu.Game.Screens.Multi.Screens.Lounge;
using osu.Game.Users;
using OpenTK.Input;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseLounge : ManualInputManagerTestCase
{
private TestLounge lounge;
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
lounge = new TestLounge();
Room[] rooms =
{
new Room
{
Name = { Value = @"Just Another Room" },
Host = { Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } } },
Status = { Value = new RoomStatusPlaying() },
Availability = { Value = RoomAvailability.Public },
Type = { Value = new GameTypeTagTeam() },
Beatmap =
{
Value = new BeatmapInfo
{
StarDifficulty = 5.65,
Ruleset = rulesets.GetRuleset(0),
Metadata = new BeatmapMetadata
{
Title = @"Sidetracked Day (Short Ver.)",
Artist = @"VINXIS",
AuthorString = @"Hobbes2",
},
BeatmapSet = new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = @"https://assets.ppy.sh/beatmaps/767600/covers/cover.jpg?1526243446",
},
},
},
}
},
MaxParticipants = { Value = 10 },
Participants =
{
Value = new[]
{
new User { Username = @"flyte", Id = 3103765, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 142 } } },
new User { Username = @"Cookiezi", Id = 124493, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 546 } } },
new User { Username = @"Angelsim", Id = 1777162, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 287 } } },
new User { Username = @"Rafis", Id = 2558286, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 468 } } },
new User { Username = @"hvick225", Id = 50265, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 325 } } },
new User { Username = @"peppy", Id = 2, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 625 } } },
}
}
},
new Room
{
Name = { Value = @"Not Just Any Room" },
Host = { Value = new User { Username = @"Monstrata", Id = 2706438, Country = new Country { FlagName = @"CA" } } },
Status = { Value = new RoomStatusOpen() },
Availability = { Value = RoomAvailability.FriendsOnly },
Type = { Value = new GameTypeTeamVersus() },
Beatmap =
{
Value = new BeatmapInfo
{
StarDifficulty = 2.73,
Ruleset = rulesets.GetRuleset(0),
Metadata = new BeatmapMetadata
{
Title = @"lit(var)",
Artist = @"kensuke ushio",
AuthorString = @"Monstrata",
},
BeatmapSet = new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = @"https://assets.ppy.sh/beatmaps/623972/covers/cover.jpg?1521167183",
},
},
},
}
},
Participants =
{
Value = new[]
{
new User { Username = @"Jeby", Id = 3136279, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 3497 } } },
new User { Username = @"DualAkira", Id = 5220933, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 643 } } },
new User { Username = @"Datenshi Yohane", Id = 7171857, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 10555 } } },
}
}
},
new Room
{
Name = { Value = @"room THE FINAL" },
Host = { Value = new User { Username = @"Delis", Id = 1603923, Country = new Country { FlagName = @"JP" } } },
Status = { Value = new RoomStatusPlaying() },
Availability = { Value = RoomAvailability.Public },
Type = { Value = new GameTypeTagTeam() },
Beatmap =
{
Value = new BeatmapInfo
{
StarDifficulty = 4.48,
Ruleset = rulesets.GetRuleset(3),
Metadata = new BeatmapMetadata
{
Title = @"ONIGIRI FREEWAY",
Artist = @"OISHII",
AuthorString = @"Mentholzzz",
},
BeatmapSet = new BeatmapSetInfo
{
OnlineInfo = new BeatmapSetOnlineInfo
{
Covers = new BeatmapSetOnlineCovers
{
Cover = @"https://assets.ppy.sh/beatmaps/663098/covers/cover.jpg?1521898837",
},
},
},
}
},
MaxParticipants = { Value = 30 },
Participants =
{
Value = new[]
{
new User { Username = @"KizuA", Id = 6510442, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 5372 } } },
new User { Username = @"Colored", Id = 827563, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 810 } } },
new User { Username = @"Beryl", Id = 3817591, Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 10096 } } },
}
}
},
};
AddStep(@"show", () => Add(lounge));
AddStep(@"set rooms", () => lounge.Rooms = rooms);
selectAssert(0);
AddStep(@"clear rooms", () => lounge.Rooms = new Room[] {});
AddAssert(@"no room selected", () => lounge.SelectedRoom == null);
AddStep(@"set rooms", () => lounge.Rooms = rooms);
selectAssert(1);
AddStep(@"open room 1", () => clickRoom(1));
AddStep(@"make lounge current", lounge.MakeCurrent);
filterAssert(@"THE FINAL", LoungeTab.Public, 1);
filterAssert(string.Empty, LoungeTab.Public, 2);
filterAssert(string.Empty, LoungeTab.Private, 1);
filterAssert(string.Empty, LoungeTab.Public, 2);
filterAssert(@"no matches", LoungeTab.Public, 0);
AddStep(@"clear rooms", () => lounge.Rooms = new Room[] {});
AddStep(@"set rooms", () => lounge.Rooms = rooms);
AddAssert(@"no matches after clear", () => !lounge.ChildRooms.Any());
filterAssert(string.Empty, LoungeTab.Public, 2);
AddStep(@"exit", lounge.Exit);
}
private void clickRoom(int n)
{
InputManager.MoveMouseTo(lounge.ChildRooms.ElementAt(n));
InputManager.Click(MouseButton.Left);
}
private void selectAssert(int n)
{
AddStep($@"select room {n}", () => clickRoom(n));
AddAssert($@"room {n} selected", () => lounge.SelectedRoom == lounge.ChildRooms.ElementAt(n).Room);
}
private void filterAssert(string filter, LoungeTab tab, int endCount)
{
AddStep($@"filter '{filter}', {tab}", () => lounge.SetFilter(filter, tab));
AddAssert(@"filtered correctly", () => lounge.ChildRooms.Count() == endCount);
}
private class TestLounge : Lounge
{
public IEnumerable<DrawableRoom> ChildRooms => RoomsContainer.Children.Where(r => r.MatchingFilter);
public Room SelectedRoom => Inspector.Room;
public void SetFilter(string filter, LoungeTab tab)
{
Filter.Search.Current.Value = filter;
Filter.Tabs.Current.Value = tab;
}
}
}
}
+9 -7
View File
@@ -39,8 +39,6 @@ namespace osu.Game.Tests.Visual
typeof(SpecialSection),
};
private const string unranked_suffix = " (Unranked)";
private RulesetStore rulesets;
private ModDisplay modDisplay;
private TestModSelectOverlay modSelect;
@@ -121,7 +119,7 @@ namespace osu.Game.Tests.Visual
private void testManiaMods(ManiaRuleset ruleset)
{
testMultiplierTextUnranked(ruleset.GetModsFor(ModType.Special).First(m => m is ManiaModRandom));
testRankedText(ruleset.GetModsFor(ModType.Special).First(m => m is ManiaModRandom));
}
private void testSingleMod(Mod mod)
@@ -198,13 +196,16 @@ namespace osu.Game.Tests.Visual
checkLabelColor(Color4.White);
}
private void testMultiplierTextUnranked(Mod mod)
private void testRankedText(Mod mod)
{
AddAssert("check for ranked", () => !modSelect.MultiplierLabel.Text.EndsWith(unranked_suffix));
AddWaitStep(1, "wait for fade");
AddAssert("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0);
selectNext(mod);
AddAssert("check for unranked", () => modSelect.MultiplierLabel.Text.EndsWith(unranked_suffix));
AddWaitStep(1, "wait for fade");
AddAssert("check for unranked", () => modSelect.UnrankedLabel.Alpha != 0);
selectPrevious(mod);
AddAssert("check for ranked", () => !modSelect.MultiplierLabel.Text.EndsWith(unranked_suffix));
AddWaitStep(1, "wait for fade");
AddAssert("check for ranked", () => modSelect.UnrankedLabel.Alpha == 0);
}
private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1));
@@ -240,6 +241,7 @@ namespace osu.Game.Tests.Visual
}
public new OsuSpriteText MultiplierLabel => base.MultiplierLabel;
public new OsuSpriteText UnrankedLabel => base.UnrankedLabel;
public new TriangleButton DeselectAllButton => base.DeselectAllButton;
public new Color4 LowMultiplierColour => base.LowMultiplierColour;
@@ -0,0 +1,27 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Multi;
using osu.Game.Screens.Multi.Screens.Lounge;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseMultiHeader : OsuTestCase
{
public TestCaseMultiHeader()
{
Lounge lounge;
Children = new Drawable[]
{
lounge = new Lounge
{
Padding = new MarginPadding { Top = Header.HEIGHT },
},
new Header(lounge),
};
}
}
}
@@ -0,0 +1,21 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
using osu.Game.Screens.Multi;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseMultiScreen : OsuTestCase
{
public TestCaseMultiScreen()
{
Multiplayer multi = new Multiplayer();
AddStep(@"show", () => Add(multi));
AddWaitStep(5);
AddStep(@"exit", multi.Exit);
}
}
}
@@ -4,6 +4,8 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Graphics;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual
@@ -11,36 +13,82 @@ namespace osu.Game.Tests.Visual
[TestFixture]
public class TestCaseOnScreenDisplay : OsuTestCase
{
private FrameworkConfigManager config;
private Bindable<FrameSync> frameSyncMode;
protected override void LoadComplete()
{
base.LoadComplete();
Add(new OnScreenDisplay());
frameSyncMode = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync);
FrameSync initial = frameSyncMode.Value;
AddRepeatStep(@"Change frame limiter", setNextMode, 3);
AddStep(@"Restore frame limiter", () => frameSyncMode.Value = initial);
}
private void setNextMode()
{
var nextMode = frameSyncMode.Value + 1;
if (nextMode > FrameSync.Unlimited)
nextMode = FrameSync.VSync;
frameSyncMode.Value = nextMode;
}
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager config)
private void load()
{
this.config = config;
var config = new TestConfigManager();
var osd = new TestOnScreenDisplay();
osd.BeginTracking(this, config);
Add(osd);
AddRepeatStep("Change toggle (no bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingNoKeybind), 2);
AddRepeatStep("Change toggle (with bind)", () => config.ToggleSetting(TestConfigSetting.ToggleSettingWithKeybind), 2);
AddRepeatStep("Change enum (no bind)", () => config.IncrementEnumSetting(TestConfigSetting.EnumSettingNoKeybind), 3);
AddRepeatStep("Change enum (with bind)", () => config.IncrementEnumSetting(TestConfigSetting.EnumSettingWithKeybind), 3);
}
private class TestConfigManager : ConfigManager<TestConfigSetting>
{
public TestConfigManager()
{
InitialiseDefaults();
}
protected override void InitialiseDefaults()
{
Set(TestConfigSetting.ToggleSettingNoKeybind, false);
Set(TestConfigSetting.EnumSettingNoKeybind, EnumSetting.Setting1);
Set(TestConfigSetting.ToggleSettingWithKeybind, false);
Set(TestConfigSetting.EnumSettingWithKeybind, EnumSetting.Setting1);
base.InitialiseDefaults();
}
public void ToggleSetting(TestConfigSetting setting) => Set(setting, !Get<bool>(setting));
public void IncrementEnumSetting(TestConfigSetting setting)
{
var nextValue = Get<EnumSetting>(setting) + 1;
if (nextValue > EnumSetting.Setting4)
nextValue = EnumSetting.Setting1;
Set(setting, nextValue);
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
new TrackedSetting<bool>(TestConfigSetting.ToggleSettingNoKeybind, b => new SettingDescription(b, "toggle setting with no keybind", b ? "enabled" : "disabled")),
new TrackedSetting<EnumSetting>(TestConfigSetting.EnumSettingNoKeybind, v => new SettingDescription(v, "enum setting with no keybind", v.ToString())),
new TrackedSetting<bool>(TestConfigSetting.ToggleSettingWithKeybind, b => new SettingDescription(b, "toggle setting with keybind", b ? "enabled" : "disabled", "fake keybind")),
new TrackedSetting<EnumSetting>(TestConfigSetting.EnumSettingWithKeybind, v => new SettingDescription(v, "enum setting with keybind", v.ToString(), "fake keybind")),
};
protected override void PerformLoad()
{
}
protected override bool PerformSave() => false;
}
private enum TestConfigSetting
{
ToggleSettingNoKeybind,
EnumSettingNoKeybind,
ToggleSettingWithKeybind,
EnumSettingWithKeybind
}
private enum EnumSetting
{
Setting1,
Setting2,
Setting3,
Setting4
}
private class TestOnScreenDisplay : OnScreenDisplay
{
protected override void Display(Drawable toDisplay) => toDisplay.FadeIn().ResizeHeightTo(110);
}
}
}
@@ -0,0 +1,53 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Screens.Play.HUD;
using OpenTK;
using OpenTK.Input;
namespace osu.Game.Tests.Visual
{
[Description("'Hold to Quit' UI element")]
public class TestCaseQuitButton : ManualInputManagerTestCase
{
private bool exitAction;
[BackgroundDependencyLoader]
private void load()
{
QuitButton quitButton;
Add(quitButton = new QuitButton
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
Action = () => exitAction = true
});
var text = quitButton.Children.OfType<SpriteText>().First();
AddStep("Trigger text fade in", () => InputManager.MoveMouseTo(quitButton));
AddUntilStep(() => text.IsPresent && !exitAction, "Text visible");
AddStep("Trigger text fade out", () => InputManager.MoveMouseTo(Vector2.One));
AddUntilStep(() => !text.IsPresent && !exitAction, "Text is not visible");
AddStep("Trigger exit action", () =>
{
exitAction = false;
InputManager.MoveMouseTo(quitButton);
InputManager.PressButton(MouseButton.Left);
});
AddStep("Early release", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("action not triggered", () => !exitAction);
AddStep("Trigger exit action", () => InputManager.PressButton(MouseButton.Left));
AddUntilStep(() => exitAction, $"{nameof(quitButton.Action)} was triggered");
}
}
}
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual
{
base.LoadComplete();
var room = new Room
Room room = new Room
{
Name = { Value = @"My Awesome Room" },
Host = { Value = new User { Username = @"flyte", Id = 3103765, Country = new Country { FlagName = @"JP" } } },
@@ -71,9 +71,13 @@ namespace osu.Game.Tests.Visual
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Room = room,
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
});
AddStep(@"set room", () => inspector.Room = room);
AddStep(@"null room", () => inspector.Room = null);
AddStep(@"set room", () => inspector.Room = room);
AddStep(@"change title", () => room.Name.Value = @"A Better Room Than The Above");
AddStep(@"change host", () => room.Host.Value = new User { Username = @"DrabWeb", Id = 6946022, Country = new Country { FlagName = @"CA" } });
AddStep(@"change status", () => room.Status.Value = new RoomStatusPlaying());
@@ -88,7 +92,7 @@ namespace osu.Game.Tests.Visual
AddStep(@"change room", () =>
{
var newRoom = new Room
Room newRoom = new Room
{
Name = { Value = @"My New, Better Than Ever Room" },
Host = { Value = new User { Username = @"Angelsim", Id = 1777162, Country = new Country { FlagName = @"KR" } } },
@@ -0,0 +1,142 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens;
using OpenTK;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseScreenBreadcrumbControl : OsuTestCase
{
private readonly ScreenBreadcrumbControl breadcrumbs;
private Screen currentScreen, changedScreen;
public TestCaseScreenBreadcrumbControl()
{
TestScreen startScreen;
OsuSpriteText titleText;
Children = new Drawable[]
{
currentScreen = startScreen = new TestScreenOne(),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
breadcrumbs = new ScreenBreadcrumbControl(startScreen)
{
RelativeSizeAxes = Axes.X,
},
titleText = new OsuSpriteText(),
},
},
};
breadcrumbs.Current.ValueChanged += s =>
{
titleText.Text = $"Changed to {s.ToString()}";
changedScreen = s;
};
breadcrumbs.Current.TriggerChange();
assertCurrent();
pushNext();
assertCurrent();
pushNext();
assertCurrent();
AddStep(@"make start current", () =>
{
startScreen.MakeCurrent();
currentScreen = startScreen;
});
assertCurrent();
pushNext();
AddAssert(@"only 2 items", () => breadcrumbs.Items.Count() == 2);
AddStep(@"exit current", () => changedScreen.Exit());
AddAssert(@"current screen is first", () => startScreen == changedScreen);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
breadcrumbs.StripColour = colours.Blue;
}
private void pushNext() => AddStep(@"push next screen", () => currentScreen = ((TestScreen)currentScreen).PushNext());
private void assertCurrent() => AddAssert(@"changedScreen correct", () => currentScreen == changedScreen);
private abstract class TestScreen : OsuScreen
{
protected abstract string NextTitle { get; }
protected abstract TestScreen CreateNextScreen();
public TestScreen PushNext()
{
TestScreen screen = CreateNextScreen();
Push(screen);
return screen;
}
protected TestScreen()
{
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = Title,
},
new TriangleButton
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 100,
Text = $"Push {NextTitle}",
Action = () => PushNext(),
},
},
};
}
}
private class TestScreenOne : TestScreen
{
public override string Title => @"Screen One";
protected override string NextTitle => @"Two";
protected override TestScreen CreateNextScreen() => new TestScreenTwo();
}
private class TestScreenTwo : TestScreen
{
public override string Title => @"Screen Two";
protected override string NextTitle => @"One";
protected override TestScreen CreateNextScreen() => new TestScreenOne();
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<Import Project="..\osu.TestProject.props" />
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>
<TargetFrameworks>netcoreapp2.0;net471</TargetFrameworks>
<TargetFrameworks>netcoreapp2.1;net471</TargetFrameworks>
</PropertyGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" />
+2 -9
View File
@@ -6,7 +6,6 @@ using osu.Game.Rulesets.Objects;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.IO.Serialization;
using Newtonsoft.Json;
using osu.Game.IO.Serialization.Converters;
@@ -55,17 +54,11 @@ namespace osu.Game.Beatmaps
IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone()
{
var newInstance = (Beatmap<T>)MemberwiseClone();
newInstance.BeatmapInfo = BeatmapInfo.DeepClone();
return newInstance;
}
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();
}
public class Beatmap : Beatmap<HitObject>
{
public Beatmap Clone() => (Beatmap)base.Clone();
public new Beatmap Clone() => (Beatmap)base.Clone();
}
}
-2
View File
@@ -53,8 +53,6 @@ namespace osu.Game.Beatmaps
{
var beatmap = CreateBeatmap();
// todo: this *must* share logic (or directly use) Beatmap<T>'s constructor.
// right now this isn't easily possible due to generic entanglement.
beatmap.BeatmapInfo = original.BeatmapInfo;
beatmap.ControlPointInfo = original.ControlPointInfo;
beatmap.HitObjects = original.HitObjects.SelectMany(h => convert(h, original)).ToList();
+5
View File
@@ -32,6 +32,11 @@ namespace osu.Game.Beatmaps
public double SliderMultiplier { get; set; } = 1;
public double SliderTickRate { get; set; } = 1;
/// <summary>
/// Returns a shallow-clone of this <see cref="BeatmapDifficulty"/>.
/// </summary>
public BeatmapDifficulty Clone() => (BeatmapDifficulty)MemberwiseClone();
/// <summary>
/// Maps a difficulty value [0, 10] to a two-piece linear range of values.
/// </summary>
+5
View File
@@ -143,5 +143,10 @@ namespace osu.Game.Beatmaps
public bool BackgroundEquals(BeatmapInfo other) => other != null && BeatmapSet != null && other.BeatmapSet != null &&
BeatmapSet.Hash == other.BeatmapSet.Hash &&
(Metadata ?? BeatmapSet.Metadata).BackgroundFile == (other.Metadata ?? other.BeatmapSet.Metadata).BackgroundFile;
/// <summary>
/// Returns a shallow-clone of this <see cref="BeatmapInfo"/>.
/// </summary>
public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone();
}
}

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