1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +08:00

Merge branch 'master' into multiplayer-disable-adaptive-speed

This commit is contained in:
Salman Ahmed 2022-05-04 17:06:36 +03:00
commit e9ecf26b6a
640 changed files with 15562 additions and 4665 deletions

View File

@ -9,7 +9,7 @@
]
},
"jetbrains.resharper.globaltools": {
"version": "2020.3.2",
"version": "2022.1.0-eap10",
"commands": [
"jb"
]
@ -27,7 +27,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.1210.0",
"version": "2022.417.0",
"commands": [
"localisation"
]

View File

@ -191,4 +191,5 @@ dotnet_diagnostic.CA2225.severity = none
# Banned APIs
dotnet_diagnostic.RS0030.severity = error
dotnet_diagnostic.OLOC001.words_in_name = 5
dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text.

2
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,2 @@
# Normalize all the line endings
32a74f95a5c80a0ed18e693f13a47522099df5c3

80
.github/ISSUE_TEMPLATE/bug-issue.yml vendored Normal file
View File

@ -0,0 +1,80 @@
name: Bug report
description: Report a very clearly broken issue.
body:
- type: markdown
attributes:
value: |
# osu! bug report
Important to note that your issue may have already been reported before. Please check:
- Pinned issues, at the top of https://github.com/ppy/osu/issues.
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful.
- type: dropdown
attributes:
label: Type
options:
- Crash to desktop
- Game behaviour
- Performance
- Cosmetic
- Other
validations:
required: true
- type: textarea
attributes:
label: Bug description
description: How did you find the bug? Any additional details that might help?
validations:
required: true
- type: textarea
attributes:
label: Screenshots or videos
description: Add screenshots or videos that show the bug here.
placeholder: Drag and drop the screenshots/videos into this box.
validations:
required: false
- type: input
attributes:
label: Version
description: The version you encountered this bug on. This is shown at the bottom of the main menu and also at the end of the settings screen.
validations:
required: true
- type: markdown
attributes:
value: |
## Logs
Attaching log files is required for every reported bug. See instructions below on how to find them.
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
### Desktop platforms
If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there
The default places to find the logs on desktop platforms are as follows:
- `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux & macOS*
If you have selected a custom location for the game files, you can find the `logs` folder there.
### Mobile platforms
The places to find the logs on mobile platforms are as follows:
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
---
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea
attributes:
label: Logs
placeholder: Drag and drop the log files into this box.
validations:
required: true

View File

@ -2,6 +2,60 @@ on: [push, pull_request]
name: Continuous Integration
jobs:
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: Restore inspectcode cache
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/inspectcode
key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }}
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
exit_code=0
while read -r line; do
if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
test:
name: Test
runs-on: ${{matrix.os.fullname}}
@ -93,52 +147,4 @@ jobs:
# cannot accept .sln(f) files as arguments.
# Build just the main game for now.
- name: Build
run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug
inspect-code:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# FIXME: Tools won't run in .NET 6.0 unless you install 3.1.x LTS side by side.
# https://itnext.io/how-to-support-multiple-net-sdks-in-github-actions-workflows-b988daa884e
- name: Install .NET 3.1.x LTS
uses: actions/setup-dotnet@v1
with:
dotnet-version: "3.1.x"
- name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1
with:
dotnet-version: "6.0.x"
- name: Restore Tools
run: dotnet tool restore
- name: Restore Packages
run: dotnet restore
- name: CodeFileSanity
run: |
# TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround.
# FIXME: Suppress warnings from templates project
exit_code=0
while read -r line; do
if [[ ! -z "$line" ]]; then
echo "::error::$line"
exit_code=1
fi
done <<< $(dotnet codefilesanity)
exit $exit_code
# Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded.
# - name: .NET Format (Dry Run)
# run: dotnet format --dry-run --check
- name: InspectCode
run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --output=$(pwd)/inspectcodereport.xml --cachesDir=$(pwd)/inspectcode --verbosity=WARN
- name: NVika
run: dotnet nvika parsereport "${{github.workspace}}/inspectcodereport.xml" --treatwarningsaserrors
run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug

View File

@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component>

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"ms-dotnettools.csharp"
]
}

View File

@ -26,14 +26,6 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<PropertyGroup Label="Project">
<!--
NU1701:
DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway.
This is required due to https://github.com/NuGet/Home/issues/5740
-->
<NoWarn>$(NoWarn);NU1701</NoWarn>
</PropertyGroup>
<PropertyGroup Label="Nuget">
<IsPackable>false</IsPackable>
<Authors>ppy Pty Ltd</Authors>
@ -42,7 +34,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2021 ppy Pty Ltd</Copyright>
<Copyright>Copyright (c) 2022 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags>
</PropertyGroup>
</Project>

View File

@ -8,17 +8,17 @@ GEM
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.553.0)
aws-sdk-core (3.126.0)
aws-partitions (1.570.0)
aws-sdk-core (3.130.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.54.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-kms (1.55.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.112.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-s3 (1.113.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
@ -36,8 +36,8 @@ GEM
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.3)
excon (0.91.0)
faraday (1.9.3)
excon (0.92.1)
faraday (1.10.0)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.204.2)
fastlane (2.205.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@ -130,10 +130,10 @@ GEM
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.0)
google-cloud-storage (1.36.1)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
@ -141,8 +141,8 @@ GEM
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.0)
faraday (>= 0.17.3, < 2.0)
googleauth (1.1.2)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
@ -152,7 +152,7 @@ GEM
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.5.0)
jmespath (1.6.1)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
@ -182,9 +182,9 @@ GEM
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.16.0)
signet (0.16.1)
addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0)
faraday (>= 0.17.5, < 3.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
@ -205,7 +205,7 @@ GEM
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unf_ext (0.0.8.1)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)

View File

@ -5,7 +5,7 @@ dotnet tool restore
# - cmd: dotnet format --dry-run --check
dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors
exit $LASTEXITCODE

View File

@ -2,5 +2,5 @@
dotnet tool restore
dotnet CodeFileSanity
dotnet jb inspectcode "osu.Desktop.slnf" --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet jb inspectcode "osu.Desktop.slnf" --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN
dotnet nvika parsereport "inspectcodereport.xml" --treatwarningsaserrors

View File

@ -1,4 +1,4 @@
Copyright (c) 2021 ppy Pty Ltd <contact@ppy.sh>.
Copyright (c) 2022 ppy Pty Ltd <contact@ppy.sh>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -31,7 +31,7 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:**
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.15+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
}
}

View File

@ -3,22 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.EmptyFreeform.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.EmptyFreeform.Mods
{
public class EmptyFreeformModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo
{
User = new APIUser { Username = "sample" },
},
Replay = new EmptyFreeformAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new EmptyFreeformAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}

View File

@ -3,22 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo
{
User = new APIUser { Username = "sample" },
},
Replay = new PippidonAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
}
}

View File

@ -1,24 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Scoring;
using System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.EmptyScrolling.Mods
{
public class EmptyScrollingModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo
{
User = new APIUser { Username = "sample" },
},
Replay = new EmptyScrollingAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new EmptyScrollingAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}

View File

@ -3,22 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo
{
User = new APIUser { Username = "sample" },
},
Replay = new PippidonAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new PippidonAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "sample" });
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@ -25,6 +26,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => Array.Empty<Skill>();
}
}

View File

@ -8,7 +8,7 @@
<PackageProjectUrl>https://github.com/ppy/osu/blob/master/Templates</PackageProjectUrl>
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<copyright>Copyright (c) 2021 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2022 ppy Pty Ltd</copyright>
<Description>Templates to use when creating a ruleset for consumption in osu!.</Description>
<PackageTags>dotnet-new;templates;osu</PackageTags>
<TargetFramework>netstandard2.1</TargetFramework>

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.314.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.430.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -123,7 +123,12 @@ namespace osu.Desktop
tools.RemoveUninstallerRegistryEntry();
}, onEveryRun: (version, tools, firstRun) =>
{
tools.SetProcessAppUserModelId();
// While setting the `ProcessAppUserModelId` fixes duplicate icons/shortcuts on the taskbar, it currently
// causes the right-click context menu to function incorrectly.
//
// This may turn out to be non-required after an alternative solution is implemented.
// see https://github.com/clowd/Clowd.Squirrel/issues/24
// tools.SetProcessAppUserModelId();
});
}

View File

@ -19,7 +19,7 @@ namespace osu.Desktop.Security
public class ElevatedPrivilegesChecker : Component
{
[Resolved]
private NotificationOverlay notifications { get; set; }
private INotificationOverlay notifications { get; set; }
private bool elevated;

View File

@ -25,7 +25,7 @@ namespace osu.Desktop.Updater
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{
private UpdateManager updateManager;
private NotificationOverlay notificationOverlay;
private INotificationOverlay notificationOverlay;
public Task PrepareUpdateAsync() => UpdateManager.RestartAppWhenExited();
@ -39,9 +39,9 @@ namespace osu.Desktop.Updater
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification)
private void load(INotificationOverlay notifications)
{
notificationOverlay = notification;
notificationOverlay = notifications;
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
}

View File

@ -24,8 +24,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.8.28-pre" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
<PackageReference Include="Clowd.Squirrel" Version="2.9.23-gc8da1a" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
@ -33,7 +32,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.0.175" />
</ItemGroup>
<ItemGroup Label="Resources">

View File

@ -11,7 +11,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2021 ppy Pty Ltd</copyright>
<copyright>Copyright (c) 2022 ppy Pty Ltd</copyright>
<language>en-AU</language>
</metadata>
<files>

View File

@ -27,7 +27,7 @@ namespace osu.Game.Benchmarks
storage = new TemporaryNativeStorage("realm-benchmark");
storage.DeleteDirectory(string.Empty);
realm = new RealmAccess(storage, "client");
realm = new RealmAccess(storage, OsuGameBase.CLIENT_DATABASE_FILENAME);
realm.Run(r =>
{

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.0505463516206195d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(4.0505463516206195d, 127, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(5.1696411260785498d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new CatchModDoubleTime());
[TestCase(5.1696411260785498d, 127, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap);

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
: base(skin, storage, null, "skin.ini")
: base(skin, null, storage)
{
}
}

View File

@ -5,10 +5,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.MathUtils;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Catch.Beatmaps
{
@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
public void ApplyPositionOffsets(IBeatmap beatmap)
{
var rng = new FastRandom(RNG_SEED);
var rng = new LegacyRandom(RNG_SEED);
float? lastPosition = null;
double lastStartTime = 0;
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
initialiseHyperDash(beatmap);
}
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, FastRandom rng)
private static void applyHardRockOffset(CatchHitObject hitObject, ref float? lastPosition, ref double lastStartTime, LegacyRandom rng)
{
float offsetPosition = hitObject.OriginalX;
double startTime = hitObject.StartTime;
@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
/// <param name="position">The position which the offset should be applied to.</param>
/// <param name="maxOffset">The maximum offset, cannot exceed 20px.</param>
/// <param name="rng">The random number generator.</param>
private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng)
private static void applyRandomOffset(ref float position, double maxOffset, LegacyRandom rng)
{
bool right = rng.NextBool();
float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset)));

View File

@ -24,7 +24,7 @@ using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
public class CatchHitObjectComposer : HitObjectComposer<CatchHitObject>
public class CatchHitObjectComposer : DistancedHitObjectComposer<CatchHitObject>
{
private const float distance_snap_radius = 50;
@ -42,6 +42,10 @@ namespace osu.Game.Rulesets.Catch.Edit
[BackgroundDependencyLoader]
private void load()
{
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
RightSideToolboxContainer.Alpha = 0;
DistanceSpacingMultiplier.Disabled = true;
LayerBelowRuleset.Add(new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,

View File

@ -3,19 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
}

View File

@ -3,20 +3,15 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModCinema : ModCinema<CatchHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!salad" } },
Replay = new CatchAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new CatchAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "osu!salad" });
}
}

View File

@ -90,6 +90,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new LegacyHitExplosion();
return null;
default:
throw new UnsupportedSkinComponentException(component);
}
}

View File

@ -13,7 +13,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
@ -107,30 +106,5 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
{
throw new System.NotImplementedException();
}
public override float GetBeatSnapDistanceAt(HitObject referenceObject)
{
throw new System.NotImplementedException();
}
public override float DurationToDistance(HitObject referenceObject, double duration)
{
throw new System.NotImplementedException();
}
public override double DistanceToDuration(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance)
{
throw new System.NotImplementedException();
}
}
}

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3449735700206298d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(2.3449735700206298d, 151, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(2.7879104989252959d, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new ManiaModDoubleTime());
[TestCase(2.7879104989252959d, 151, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap);

View File

@ -11,8 +11,8 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using osu.Game.Utils;
using osuTK;
namespace osu.Game.Rulesets.Mania.Beatmaps
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
private readonly int originalTargetColumns;
// Internal for testing purposes
internal FastRandom Random { get; private set; }
internal LegacyRandom Random { get; private set; }
private Pattern lastPattern = new Pattern();
@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
IBeatmapDifficultyInfo difficulty = original.Difficulty;
int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate);
Random = new FastRandom(seed);
Random = new LegacyRandom(seed);
return base.ConvertBeatmap(original, cancellationToken);
}
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary>
private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator
{
public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
public SpecificBeatmapPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
}

View File

@ -8,12 +8,12 @@ using System.Linq;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private PatternType convertType;
public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
public DistanceObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
convertType = PatternType.None;

View File

@ -2,13 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private readonly int endTime;
private readonly PatternType convertType;
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
public EndTimeObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);

View File

@ -9,11 +9,11 @@ using osuTK;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
private readonly PatternType convertType;
public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density,
public HitObjectPatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density,
PatternType lastStair, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{

View File

@ -5,8 +5,8 @@ using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@ -23,14 +23,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <summary>
/// The random number generator to use.
/// </summary>
protected readonly FastRandom Random;
protected readonly LegacyRandom Random;
/// <summary>
/// The beatmap which <see cref="HitObject"/> is being converted from.
/// </summary>
protected readonly IBeatmap OriginalBeatmap;
protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
protected PatternGenerator(LegacyRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(hitObject, beatmap, previousPattern)
{
if (random == null) throw new ArgumentNullException(nameof(random));

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
private const double release_threshold = 24;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
@ -37,31 +38,43 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
var maniaCurrent = (ManiaDifficultyHitObject)current;
double endTime = maniaCurrent.EndTime;
int column = maniaCurrent.BaseObject.Column;
double closestEndTime = Math.Abs(endTime - maniaCurrent.LastObject.StartTime); // Lowest value we can assume with the current information
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
bool isOverlapping = false;
// Fill up the holdEndTimes array
for (int i = 0; i < holdEndTimes.Length; ++i)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
holdAddition = 0;
// The current note is overlapped if a previous note or end is overlapping the current note body
isOverlapping |= Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1);
// We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
holdFactor = 1.25;
closestEndTime = Math.Min(closestEndTime, Math.Abs(endTime - holdEndTimes[i]));
// Decay individual strains
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
}
holdEndTimes[column] = endTime;
// The hold addition is given if there was an overlap, however it is only valid if there are no other note with a similar ending.
// Releasing multiple notes is just as easy as releasing 1. Nerfs the hold addition by half if the closest release is release_threshold away.
// holdAddition
// ^
// 1.0 + - - - - - -+-----------
// | /
// 0.5 + - - - - -/ Sigmoid Curve
// | /|
// 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold
if (isOverlapping)
holdAddition = 1 / (1 + Math.Exp(0.5 * (release_threshold - closestEndTime)));
// Increase individual strain in own column
individualStrains[column] += 2.0 * holdFactor;
individualStrain = individualStrains[column];

View File

@ -1,95 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Game.Rulesets.Mania.MathUtils
{
/// <summary>
/// A PRNG specified in http://heliosphan.org/fastrandom.html.
/// </summary>
internal class FastRandom
{
private const double int_to_real = 1.0 / (int.MaxValue + 1.0);
private const uint int_mask = 0x7FFFFFFF;
private const uint y = 842502087;
private const uint z = 3579807591;
private const uint w = 273326509;
internal uint X { get; private set; }
internal uint Y { get; private set; } = y;
internal uint Z { get; private set; } = z;
internal uint W { get; private set; } = w;
public FastRandom(int seed)
{
X = (uint)seed;
}
public FastRandom()
: this(Environment.TickCount)
{
}
/// <summary>
/// Generates a random unsigned integer within the range [<see cref="uint.MinValue"/>, <see cref="uint.MaxValue"/>).
/// </summary>
/// <returns>The random value.</returns>
public uint NextUInt()
{
uint t = X ^ (X << 11);
X = Y;
Y = Z;
Z = W;
return W = W ^ (W >> 19) ^ t ^ (t >> 8);
}
/// <summary>
/// Generates a random integer value within the range [0, <see cref="int.MaxValue"/>).
/// </summary>
/// <returns>The random value.</returns>
public int Next() => (int)(int_mask & NextUInt());
/// <summary>
/// Generates a random integer value within the range [0, <paramref name="upperBound"/>).
/// </summary>
/// <param name="upperBound">The upper bound.</param>
/// <returns>The random value.</returns>
public int Next(int upperBound) => (int)(NextDouble() * upperBound);
/// <summary>
/// Generates a random integer value within the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The lower bound of the range.</param>
/// <param name="upperBound">The upper bound of the range.</param>
/// <returns>The random value.</returns>
public int Next(int lowerBound, int upperBound) => (int)(lowerBound + NextDouble() * (upperBound - lowerBound));
/// <summary>
/// Generates a random double value within the range [0, 1).
/// </summary>
/// <returns>The random value.</returns>
public double NextDouble() => int_to_real * Next();
private uint bitBuffer;
private int bitIndex = 32;
/// <summary>
/// Generates a reandom boolean value. Cached such that a random value is only generated once in every 32 calls.
/// </summary>
/// <returns>The random value.</returns>
public bool NextBool()
{
if (bitIndex == 32)
{
bitBuffer = NextUInt();
bitIndex = 1;
return (bitBuffer & 1) == 1;
}
bitIndex++;
return ((bitBuffer >>= 1) & 1) == 1;
}
}
}

View File

@ -3,20 +3,15 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedUser { Username = "osu!topus" });
}
}

View File

@ -3,21 +3,16 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModCinema : ModCinema<ManiaHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "osu!topus" } },
Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), new ModCreatedUser { Username = "osu!topus" });
}
}

View File

@ -116,9 +116,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
case ManiaSkinComponents.StageForeground:
return new LegacyStageForeground();
}
break;
default:
throw new UnsupportedSkinComponentException(component);
}
}
return base.GetDrawableComponent(component);

View File

@ -4,6 +4,7 @@
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Cached]
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
[Cached(typeof(IPositionSnapProvider))]
[Cached(typeof(IDistanceSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider();
private TestOsuDistanceSnapGrid grid;
@ -179,13 +180,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}
}
private class SnapProvider : IPositionSnapProvider
private class SnapProvider : IDistanceSnapProvider
{
public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) =>
new SnapResult(screenSpacePosition, null);
public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public IBindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length;
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;

View File

@ -0,0 +1,108 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Diagnostics;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests
{
[HeadlessTest]
public class LegacyMainCirclePieceTest : OsuTestScene
{
private static readonly object?[][] texture_priority_cases =
{
// default priority lookup
new object?[]
{
// available textures
new[] { @"hitcircle", @"hitcircleoverlay" },
// priority lookup prefix
null,
// expected circle and overlay
@"hitcircle", @"hitcircleoverlay",
},
// custom priority lookup
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle", @"sliderstartcircleoverlay" },
@"sliderstartcircle",
@"sliderstartcircle", @"sliderstartcircleoverlay",
},
// when no sprites are available for the specified prefix, fall back to "hitcircle"/"hitcircleoverlay".
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay" },
@"sliderstartcircle",
@"hitcircle", @"hitcircleoverlay",
},
// when a circle is available for the specified prefix but no overlay exists, no overlay is displayed.
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircle" },
@"sliderstartcircle",
@"sliderstartcircle", null
},
// when no circle is available for the specified prefix but an overlay exists, the overlay is ignored.
new object?[]
{
new[] { @"hitcircle", @"hitcircleoverlay", @"sliderstartcircleoverlay" },
@"sliderstartcircle",
@"hitcircle", @"hitcircleoverlay",
}
};
[TestCaseSource(nameof(texture_priority_cases))]
public void TestTexturePriorities(string[] textureFilenames, string priorityLookup, string? expectedCircle, string? expectedOverlay)
{
TestLegacyMainCirclePiece piece = null!;
AddStep("load circle piece", () =>
{
var skin = new Mock<ISkinSource>();
// shouldn't be required as GetTexture(string) calls GetTexture(string, WrapMode, WrapMode) by default,
// but moq doesn't handle that well, therefore explicitly requiring to use `CallBase`:
// https://github.com/moq/moq4/issues/972
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
.Returns((string componentName, WrapMode _, WrapMode __) => new Texture(1, 1) { AssetName = componentName });
Child = new DependencyProvidingContainer
{
CachedDependencies = new (Type, object)[] { (typeof(ISkinSource), skin.Object) },
Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
};
var sprites = this.ChildrenOfType<Sprite>().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray();
Debug.Assert(sprites.Length <= 2);
});
AddAssert("check circle sprite", () => piece.CircleSprite?.Texture?.AssetName == expectedCircle);
AddAssert("check overlay sprite", () => piece.OverlaySprite?.Texture?.AssetName == expectedOverlay);
}
private class TestLegacyMainCirclePiece : LegacyMainCirclePiece
{
public new Sprite? CircleSprite => base.CircleSprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public new Sprite? OverlaySprite => base.OverlaySprite.ChildrenOfType<Sprite>().DistinctBy(s => s.Texture.AssetName).SingleOrDefault();
public TestLegacyMainCirclePiece(string? priorityLookupPrefix)
: base(priorityLookupPrefix, false)
{
}
}
}
}

View File

@ -16,31 +16,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAlternate : OsuModTestScene
{
[Test]
public void TestInputAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
}
});
[Test]
public void TestInputAlternating() => CreateModTest(new ModTestData
{
@ -116,17 +91,50 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
}
});
/// <summary>
/// Ensures alternation is reset before the first hitobject after intro.
/// </summary>
[Test]
public void TestInputSingularAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
// first press during intro.
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
// press same key at hitobject and ensure it has been hit.
new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
}
});
/// <summary>
/// Ensures alternation is reset before the first hitobject after a break.
/// </summary>
[Test]
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
{
new BreakPeriod(500, 2250),
new BreakPeriod(500, 2000),
},
HitObjects = new List<HitObject>
{
@ -138,16 +146,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
new HitCircle
{
StartTime = 2500,
Position = new Vector2(100),
}
Position = new Vector2(500, 100),
},
new HitCircle
{
StartTime = 3000,
Position = new Vector2(500, 100),
},
}
},
ReplayFrames = new List<ReplayFrame>
{
// first press to start alternate lock.
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(100)),
// press same key after break but before hit object.
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
new OsuReplayFrame(2251, new Vector2(300, 100)),
// press same key at second hitobject and ensure it has been hit.
new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(500, 100)),
// press same key at third hitobject and ensure it has been missed.
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton),
new OsuReplayFrame(3001, new Vector2(500, 100)),
}
});
}

View File

@ -6,18 +6,18 @@ using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAimAssist : OsuModTestScene
public class TestSceneOsuModMagnetised : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
public void TestAimAssist(float strength)
public void TestMagnetised(float strength)
{
CreateModTest(new ModTestData
{
Mod = new OsuModAimAssist
Mod = new OsuModMagnetised
{
AssistStrength = { Value = strength },
AttractionStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,

View File

@ -15,15 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6972307565739273d, "diffcalc-test")]
[TestCase(1.4484754139145539d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(6.6972307565739273d, 206, "diffcalc-test")]
[TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9382559208689809d, "diffcalc-test")]
[TestCase(1.7548875851757628d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
[TestCase(8.9382559208689809d, 206, "diffcalc-test")]
[TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6972307218715166d, 239, "diffcalc-test")]
[TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
@ -13,7 +12,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Visual;
using osuTK;
@ -67,11 +65,8 @@ namespace osu.Game.Rulesets.Osu.Tests
private class TestAutoMod : OsuModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
Replay = new MissingAutoGenerator(beatmap, mods).Generate()
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new MissingAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
private class MissingAutoGenerator : OsuAutoGeneratorBase

View File

@ -70,7 +70,9 @@ namespace osu.Game.Rulesets.Osu.Tests
var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
Child = new SkinProvidingContainer(tintingSkin)
var provider = Ruleset.Value.CreateInstance().CreateLegacySkinProvider(tintingSkin, Beatmap.Value.Beatmap);
Child = new SkinProvidingContainer(provider)
{
RelativeSizeAxes = Axes.Both,
Child = dho = new DrawableSlider(prepareObject(new Slider

View File

@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps
@ -20,13 +21,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
new BeatmapStatistic
{
Name = @"Circle Count",
Name = BeatmapsetsStrings.ShowStatsCountCircles,
Content = circles.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
},
new BeatmapStatistic
{
Name = @"Slider Count",
Name = BeatmapsetsStrings.ShowStatsCountSliders,
Content = sliders.ToString(),
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
},

View File

@ -61,10 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
int maxCombo = beatmap.GetMaxCombo();
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);

View File

@ -38,7 +38,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
double difficulty = 0;
double weight = 1;
List<double> strains = GetCurrentStrainPeaks().OrderByDescending(d => d).ToList();
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
var peaks = GetCurrentStrainPeaks().Where(p => p > 0);
List<double> strains = peaks.OrderByDescending(d => d).ToList();
// We are reducing the highest strains first to account for extreme difficulty spikes
for (int i = 0; i < Math.Min(strains.Count, ReducedSectionCount); i++)

View File

@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<List<PathControlPoint>> RemoveControlPointsRequested;
[Resolved(CanBeNull = true)]
private IPositionSnapProvider snapProvider { get; set; }
private IDistanceSnapProvider snapProvider { get; set; }
public PathControlPointVisualiser(Slider slider, bool allowSelection)
{

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private int currentSegmentLength;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
private IDistanceSnapProvider snapProvider { get; set; }
public SliderPlacementBlueprint()
: base(new Objects.Slider())
@ -220,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
private IDistanceSnapProvider snapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, pathControlPoint);
HitObject.SnapTo(composer);
HitObject.SnapTo(snapProvider);
return pathControlPoint;
}
@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Snap the slider to the current beat divisor before checking length validity.
HitObject.SnapTo(composer);
HitObject.SnapTo(snapProvider);
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)

View File

@ -24,7 +24,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuHitObjectComposer : HitObjectComposer<OsuHitObject>
public class OsuHitObjectComposer : DistancedHitObjectComposer<OsuHitObject>
{
public OsuHitObjectComposer(Ruleset ruleset)
: base(ruleset)
@ -59,11 +59,6 @@ namespace osu.Game.Rulesets.Osu.Edit
{
LayerBelowRuleset.AddRange(new Drawable[]
{
new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
},
distanceSnapGridContainer = new Container
{
RelativeSizeAxes = Axes.Both

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Edit
public class OsuSelectionHandler : EditorSelectionHandler
{
[Resolved(CanBeNull = true)]
private IPositionSnapProvider? positionSnapProvider { get; set; }
private IDistanceSnapProvider? snapProvider { get; set; }
/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// Snap the slider's length to the current beat divisor
// to calculate the final resulting duration / bounding box before the final checks.
slider.SnapTo(positionSnapProvider);
slider.SnapTo(snapProvider);
//if sliderhead or sliderend end up outside playfield, revert scaling.
Quad scaledQuad = getSurroundingQuad(new OsuHitObject[] { slider });
@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Osu.Edit
point.Position = oldControlPoints.Dequeue();
// Snap the slider's length again to undo the potentially-invalid length applied by the previous snap.
slider.SnapTo(positionSnapProvider);
slider.SnapTo(snapProvider);
}
private void scaleHitObjects(OsuHitObject[] hitObjects, Anchor reference, Vector2 scale)

View File

@ -2,21 +2,24 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
@ -26,9 +29,16 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
private double firstObjectValidJudgementTime;
private IBindable<bool> isBreakTime;
private const double flash_duration = 1000;
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods;
private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset;
@ -39,29 +49,30 @@ namespace osu.Game.Rulesets.Osu.Mods
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var firstHitObject = ruleset.Objects.FirstOrDefault();
firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0);
var periods = new List<Period>();
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
public void ApplyToPlayer(Player player)
{
isBreakTime = player.IsBreakTime.GetBoundCopy();
isBreakTime.ValueChanged += e =>
{
if (e.NewValue)
lastActionPressed = null;
};
}
private bool checkCorrectAction(OsuAction action)
{
if (isBreakTime.Value)
return true;
if (gameplayClock.CurrentTime < firstObjectValidJudgementTime)
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
{
lastActionPressed = null;
return true;
}
switch (action)
{

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
public bool PerformFail() => false;

View File

@ -5,21 +5,16 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
}

View File

@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1.12;
public override Type[] IncompatibleMods => new[] { typeof(OsuModFlashlight) };
private DrawableOsuBlinds blinds;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)

View File

@ -5,22 +5,17 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema<OsuHitObject>
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "Autoplay" } },
Replay = new OsuAutoGenerator(beatmap, mods).Generate()
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Configuration;
@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModStrictTracking)).ToArray();
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModFlashlight : ModFlashlight<OsuHitObject>, IApplicableToDrawableHitObject
{
public override double ScoreMultiplier => 1.12;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModBlinds)).ToArray();
private const double default_follow_delay = 120;

View File

@ -16,20 +16,20 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Aim Assist";
public override string Acronym => "AA";
public override IconUsage? Icon => FontAwesome.Solid.MousePointer;
public override string Name => "Magnetised";
public override string Acronym => "MG";
public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circle the circle chases you!";
public override string Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
private IFrameStableClock gameplayClock;
[SettingSource("Assist strength", "How much this mod will assist you.", 0)]
public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f)
[SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
{
Precision = 0.05f,
MinValue = 0.05f,
@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void easeTo(DrawableHitObject hitObject, Vector2 destination)
{
double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value);
double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
protected virtual float EndScale => 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) };
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
{

View File

@ -4,19 +4,14 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -27,13 +22,9 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override string Description => "It never gets boring!";
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
/// <summary>
/// Number of previous hitobjects to be shifted together when another object is being moved.
/// </summary>
private const int preceding_hitobjects_to_shift = 10;
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random? rng;
@ -42,330 +33,33 @@ namespace osu.Game.Rulesets.Osu.Mods
if (!(beatmap is OsuBeatmap osuBeatmap))
return;
var hitObjects = osuBeatmap.HitObjects;
Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value);
var randomObjects = randomiseObjects(hitObjects);
var positionInfos = OsuHitObjectGenerationUtils.GeneratePositionInfos(osuBeatmap.HitObjects);
applyRandomisation(hitObjects, randomObjects);
}
/// <summary>
/// Randomise the position of each hit object and return a list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.
/// </summary>
/// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to have their positions randomised.</param>
/// <returns>A list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.</returns>
private List<RandomObjectInfo> randomiseObjects(IEnumerable<OsuHitObject> hitObjects)
{
Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects");
var randomObjects = new List<RandomObjectInfo>();
RandomObjectInfo? previous = null;
float rateOfChangeMultiplier = 0;
foreach (OsuHitObject hitObject in hitObjects)
foreach (var positionInfo in positionInfos)
{
var current = new RandomObjectInfo(hitObject);
randomObjects.Add(current);
// rateOfChangeMultiplier only changes every 5 iterations in a combo
// to prevent shaky-line-shaped streams
if (hitObject.IndexInCurrentCombo % 5 == 0)
if (positionInfo.HitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
if (previous == null)
if (positionInfo == positionInfos.First())
{
current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
positionInfo.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
positionInfo.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
}
else
{
current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
// is proportional to the distance between the last and the current hit object
// to allow jumps and prevent too sharp turns during streams.
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f));
positionInfo.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, positionInfo.DistanceFromPrevious / (playfield_diagonal * 0.5f));
}
previous = current;
}
return randomObjects;
}
/// <summary>
/// Reposition the hit objects according to the information in <paramref name="randomObjects"/>.
/// </summary>
/// <param name="hitObjects">The hit objects to be repositioned.</param>
/// <param name="randomObjects">A list of <see cref="RandomObjectInfo"/> describing how each hit object should be placed.</param>
private void applyRandomisation(IReadOnlyList<OsuHitObject> hitObjects, IReadOnlyList<RandomObjectInfo> randomObjects)
{
RandomObjectInfo? previous = null;
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
var current = randomObjects[i];
if (hitObject is Spinner)
{
previous = null;
continue;
}
computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null);
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2.Zero;
switch (hitObject)
{
case HitCircle circle:
shift = clampHitCircleToPlayfield(circle, current);
break;
case Slider slider:
shift = clampSliderToPlayfield(slider, current);
break;
}
if (shift != Vector2.Zero)
{
var toBeShifted = new List<OsuHitObject>();
for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
{
// only shift hit circles
if (!(hitObjects[j] is HitCircle)) break;
toBeShifted.Add(hitObjects[j]);
}
if (toBeShifted.Count > 0)
applyDecreasingShift(toBeShifted, shift);
}
previous = current;
}
}
/// <summary>
/// Compute the randomised position of a hit object while attempting to keep it inside the playfield.
/// </summary>
/// <param name="current">The <see cref="RandomObjectInfo"/> representing the hit object to have the randomised position computed for.</param>
/// <param name="previous">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the current one.</param>
/// <param name="beforePrevious">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param>
private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious)
{
float previousAbsoluteAngle = 0f;
if (previous != null)
{
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
}
float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle;
var posRelativeToPrev = new Vector2(
current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
);
Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre;
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
current.PositionRandomised = lastEndPosition + posRelativeToPrev;
}
/// <summary>
/// Move the randomised position of a hit circle so that it fits inside the playfield.
/// </summary>
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo)
{
var previousPosition = objectInfo.PositionRandomised;
objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding(
objectInfo.PositionRandomised,
(float)circle.Radius
);
circle.Position = objectInfo.PositionRandomised;
return objectInfo.PositionRandomised - previousPosition;
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo)
{
var possibleMovementBounds = calculatePossibleMovementBounds(slider);
var previousPosition = objectInfo.PositionRandomised;
// Clamp slider position to the placement area
// If the slider is larger than the playfield, force it to stay at the original position
float newX = possibleMovementBounds.Width < 0
? objectInfo.PositionOriginal.X
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
float newY = possibleMovementBounds.Height < 0
? objectInfo.PositionOriginal.Y
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY);
objectInfo.EndPositionRandomised = slider.EndPosition;
shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal);
return objectInfo.PositionRandomised - previousPosition;
}
/// <summary>
/// Decreasingly shift a list of <see cref="OsuHitObject"/>s by a specified amount.
/// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
/// </summary>
/// <param name="hitObjects">The list of hit objects to be shifted.</param>
/// <param name="shift">The amount to be shifted.</param>
private void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2 shift)
{
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
// The first object is shifted by a vector slightly smaller than shift
// The last object is shifted by a vector slightly larger than zero
Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
}
}
/// <summary>
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
private RectangleF calculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float minY = float.PositiveInfinity;
float maxY = float.NegativeInfinity;
// Compute the bounding box of the slider.
foreach (var pos in pathPositions)
{
minX = MathF.Min(minX, pos.X);
maxX = MathF.Max(maxX, pos.X);
minY = MathF.Min(minY, pos.Y);
maxY = MathF.Max(maxY, pos.Y);
}
// Take the circle radius into account.
float radius = (float)slider.Radius;
minX -= radius;
minY -= radius;
maxX += radius;
maxY += radius;
// Given the bounding box of the slider (via min/max X/Y),
// the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
// and the amount that it can move to the right is WIDTH - maxX.
// Same calculation applies for the Y axis.
float left = -minX;
float right = OsuPlayfield.BASE_SIZE.X - maxX;
float top = -minY;
float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
return new RectangleF(left, top, right - left, bottom - top);
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private void shiftNestedObjects(Slider slider, Vector2 shift)
{
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
osuHitObject.Position += shift;
}
}
/// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary>
/// <param name="position">The position to be clamped.</param>
/// <param name="padding">The minimum distance allowed from playfield edges.</param>
/// <returns>The clamped position.</returns>
private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
{
return new Vector2(
Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
);
}
private class RandomObjectInfo
{
/// <summary>
/// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
/// </summary>
/// <remarks>
/// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
/// </remarks>
/// <example>
/// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing
/// the previous object to reach this one.
/// </example>
public float RelativeAngle { get; set; }
/// <summary>
/// The jump distance from the previous hit object to this one.
/// </summary>
/// <remarks>
/// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center.
/// </remarks>
public float DistanceFromPrevious { get; set; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionRandomised { get; set; }
public Vector2 EndPositionOriginal { get; }
public Vector2 EndPositionRandomised { get; set; }
public OsuHitObject HitObject { get; }
public RandomObjectInfo(OsuHitObject hitObject)
{
PositionRandomised = PositionOriginal = hitObject.Position;
EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
HitObject = hitObject;
}
osuBeatmap.HitObjects = OsuHitObjectGenerationUtils.RepositionHitObjects(positionInfos);
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray();
/// <summary>
/// How early before a hitobject's start time to trigger a hit.

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) };
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) };
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
{

View File

@ -0,0 +1,148 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModStrictTracking : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => @"Strict Tracking";
public override string Acronym => @"ST";
public override IconUsage? Icon => FontAwesome.Solid.PenFancy;
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Follow circles just got serious...";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableSlider slider)
{
slider.Tracking.ValueChanged += e =>
{
if (e.NewValue || slider.Judged) return;
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
if (!tail.Judged)
tail.MissForcefully();
};
}
}
public void ApplyToBeatmap(IBeatmap beatmap)
{
var osuBeatmap = (OsuBeatmap)beatmap;
if (osuBeatmap.HitObjects.Count == 0) return;
var hitObjects = osuBeatmap.HitObjects.Select(ho =>
{
if (ho is Slider slider)
{
var newSlider = new StrictTrackingSlider(slider);
return newSlider;
}
return ho;
}).ToList();
osuBeatmap.HitObjects = hitObjects;
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
drawableRuleset.Playfield.RegisterPool<StrictTrackingSliderTailCircle, StrictTrackingDrawableSliderTail>(10, 100);
}
private class StrictTrackingSliderTailCircle : SliderTailCircle
{
public StrictTrackingSliderTailCircle(Slider slider)
: base(slider)
{
}
public override Judgement CreateJudgement() => new OsuJudgement();
}
private class StrictTrackingDrawableSliderTail : DrawableSliderTail
{
public override bool DisplayResult => true;
}
private class StrictTrackingSlider : Slider
{
public StrictTrackingSlider(Slider original)
{
StartTime = original.StartTime;
Samples = original.Samples;
Path = original.Path;
NodeSamples = original.NodeSamples;
RepeatCount = original.RepeatCount;
Position = original.Position;
NewCombo = original.NewCombo;
ComboOffset = original.ComboOffset;
LegacyLastTickOffset = original.LegacyLastTickOffset;
TickDistanceMultiplier = original.TickDistanceMultiplier;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
foreach (var e in sliderEvents)
{
switch (e.Type)
{
case SliderEventType.Head:
AddNested(HeadCircle = new SliderHeadCircle
{
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
});
break;
case SliderEventType.LegacyLastTick:
AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
{
RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
});
break;
case SliderEventType.Repeat:
AddNested(new SliderRepeat(this)
{
RepeatIndex = e.SpanIndex,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
});
break;
}
}
UpdateNestedSamples();
}
}
}
}

View File

@ -9,6 +9,10 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSuddenDeath : ModSuddenDeath
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
{
typeof(OsuModAutopilot),
typeof(OsuModTarget),
}).ToArray();
}
}

View File

@ -42,7 +42,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => @"Practice keeping up with the beat of the song.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles) };
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
{
typeof(IRequiresApproachCircles),
typeof(OsuModRandom),
typeof(OsuModSpunOut),
typeof(OsuModStrictTracking),
typeof(OsuModSuddenDeath)
}).ToArray();
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
public Bindable<int?> Seed { get; } = new Bindable<int?>

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) };
private float theta;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles

View File

@ -6,10 +6,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -21,10 +21,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public readonly IBindable<float> ScaleBindable = new BindableFloat();
public readonly IBindable<int> IndexInCurrentComboBindable = new Bindable<int>();
// Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects.
// Must be set to update IsHovered as it's used in relax mod to detect osu hit objects.
public override bool HandlePositionalInput => true;
protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X;
protected override float SamplePlaybackPosition => CalculateDrawableRelativePosition(this);
/// <summary>
/// Whether this <see cref="DrawableOsuHitObject"/> can be hit, given a time value.
@ -89,6 +89,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent.ScreenSpaceDrawQuad.AABBFloat;
/// <summary>
/// Calculates the position of the given <paramref name="drawable"/> relative to the playfield area.
/// </summary>
/// <param name="drawable">The drawable to calculate its relative position.</param>
protected float CalculateDrawableRelativePosition(Drawable drawable) => (drawable.ScreenSpaceDrawQuad.Centre.X - parentScreenSpaceRectangle.X) / parentScreenSpaceRectangle.Width;
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement);
}
}

View File

@ -13,7 +13,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
@ -208,7 +207,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X);
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);

View File

@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider()
{
SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples();
SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions();
}
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects
}
}
updateNestedSamples();
UpdateNestedSamples();
}
private void updateNestedPositions()
@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects
TailCircle.Position = EndPosition;
}
private void updateNestedSamples()
protected void UpdateNestedSamples()
{
var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)

View File

@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
new OsuModStrictTracking()
};
case ModType.Conversion:
@ -194,7 +195,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
new OsuModAimAssist(),
new OsuModMagnetised(),
new ModAdaptiveSpeed()
};

View File

@ -3,10 +3,10 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
@ -16,63 +16,61 @@ using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
#nullable enable
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class LegacyMainCirclePiece : CompositeDrawable
{
public override bool RemoveCompletedTransforms => false;
private readonly string priorityLookup;
/// <summary>
/// A prioritised prefix to perform texture lookups with.
/// </summary>
private readonly string? priorityLookupPrefix;
private readonly bool hasNumber;
public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
protected Drawable CircleSprite = null!;
protected Drawable OverlaySprite = null!;
protected Container OverlayLayer { get; private set; } = null!;
private SkinnableSpriteText hitCircleText = null!;
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[Resolved(canBeNull: true)]
private DrawableHitObject? drawableObject { get; set; }
[Resolved]
private ISkinSource skin { get; set; } = null!;
public LegacyMainCirclePiece(string? priorityLookupPrefix = null, bool hasNumber = true)
{
this.priorityLookup = priorityLookup;
this.priorityLookupPrefix = priorityLookupPrefix;
this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
private Drawable hitCircleSprite;
protected Container OverlayLayer { get; private set; }
private Drawable hitCircleOverlay;
private SkinnableSpriteText hitCircleText;
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
[Resolved]
private DrawableHitObject drawableObject { get; set; }
[Resolved]
private ISkinSource skin { get; set; }
[BackgroundDependencyLoader]
private void load()
{
var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
bool allowFallback = false;
// attempt lookup using priority specification
Texture baseTexture = getTextureWithFallback(string.Empty);
// if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
if (baseTexture == null)
{
allowFallback = true;
baseTexture = getTextureWithFallback(string.Empty);
}
// if a base texture for the specified prefix exists, continue using it for subsequent lookups.
// otherwise fall back to the default prefix "hitcircle".
string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle";
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
// the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
InternalChildren = new[]
{
hitCircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = baseTexture })
CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -81,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = hitCircleOverlay = new KiaiFlashingDrawable(() => getAnimationWithFallback(@"overlay", 1000 / 2d))
Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -105,39 +103,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
if (overlayAboveNumber)
OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue);
OverlayLayer.ChangeChildDepth(OverlaySprite, float.MinValue);
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
Texture getTextureWithFallback(string name)
if (drawableOsuObject != null)
{
Texture tex = null;
if (!string.IsNullOrEmpty(priorityLookup))
{
tex = skin.GetTexture($"{priorityLookup}{name}");
if (!allowFallback)
return tex;
}
return tex ?? skin.GetTexture($"hitcircle{name}");
}
Drawable getAnimationWithFallback(string name, double frameLength)
{
Drawable animation = null;
if (!string.IsNullOrEmpty(priorityLookup))
{
animation = skin.GetAnimation($"{priorityLookup}{name}", true, true, frameLength: frameLength);
if (!allowFallback)
return animation;
}
return animation ?? skin.GetAnimation($"hitcircle{name}", true, true, frameLength: frameLength);
accentColour.BindTo(drawableOsuObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
}
}
@ -145,28 +116,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
base.LoadComplete();
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
accentColour.BindValueChanged(colour => CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
if (drawableObject != null)
{
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
}
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
const double legacy_fade_duration = 240;
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
using (BeginAbsoluteSequence(drawableObject.AsNonNull().HitStateUpdateTime))
{
switch (state)
{
case ArmedState.Hit:
hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
CircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
OverlaySprite.FadeOut(legacy_fade_duration, Easing.Out);
OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber)
{

View File

@ -33,7 +33,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (osuComponent.Component)
{
case OsuSkinComponents.FollowPoint:
return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false);
return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false);
case OsuSkinComponents.SliderScorePoint:
return this.GetAnimation(component.LookupName, false, false);
case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
@ -123,6 +126,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
case OsuSkinComponents.ApproachCircle:
return new LegacyApproachCircle();
default:
throw new UnsupportedSkinComponentException(component);
}
}

View File

@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Osu.UI
},
new SettingsCheckbox
{
ClassicDefault = false,
LabelText = "Snaking out sliders",
Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders)
},

View File

@ -11,7 +11,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Utils
{
public static class OsuHitObjectGenerationUtils
public static partial class OsuHitObjectGenerationUtils
{
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn

View File

@ -0,0 +1,340 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
#nullable enable
namespace osu.Game.Rulesets.Osu.Utils
{
public static partial class OsuHitObjectGenerationUtils
{
/// <summary>
/// Number of previous hitobjects to be shifted together when an object is being moved.
/// </summary>
private const int preceding_hitobjects_to_shift = 10;
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
/// <summary>
/// Generate a list of <see cref="ObjectPositionInfo"/>s containing information for how the given list of
/// <see cref="OsuHitObject"/>s are positioned.
/// </summary>
/// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to process.</param>
/// <returns>A list of <see cref="ObjectPositionInfo"/>s describing how each hit object is positioned relative to the previous one.</returns>
public static List<ObjectPositionInfo> GeneratePositionInfos(IEnumerable<OsuHitObject> hitObjects)
{
var positionInfos = new List<ObjectPositionInfo>();
Vector2 previousPosition = playfield_centre;
float previousAngle = 0;
foreach (OsuHitObject hitObject in hitObjects)
{
Vector2 relativePosition = hitObject.Position - previousPosition;
float absoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
float relativeAngle = absoluteAngle - previousAngle;
positionInfos.Add(new ObjectPositionInfo(hitObject)
{
RelativeAngle = relativeAngle,
DistanceFromPrevious = relativePosition.Length
});
previousPosition = hitObject.EndPosition;
previousAngle = absoluteAngle;
}
return positionInfos;
}
/// <summary>
/// Reposition the hit objects according to the information in <paramref name="objectPositionInfos"/>.
/// </summary>
/// <param name="objectPositionInfos">Position information for each hit object.</param>
/// <returns>The repositioned hit objects.</returns>
public static List<OsuHitObject> RepositionHitObjects(IEnumerable<ObjectPositionInfo> objectPositionInfos)
{
List<WorkingObject> workingObjects = objectPositionInfos.Select(o => new WorkingObject(o)).ToList();
WorkingObject? previous = null;
for (int i = 0; i < workingObjects.Count; i++)
{
var current = workingObjects[i];
var hitObject = current.HitObject;
if (hitObject is Spinner)
{
previous = null;
continue;
}
computeModifiedPosition(current, previous, i > 1 ? workingObjects[i - 2] : null);
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2.Zero;
switch (hitObject)
{
case HitCircle _:
shift = clampHitCircleToPlayfield(current);
break;
case Slider _:
shift = clampSliderToPlayfield(current);
break;
}
if (shift != Vector2.Zero)
{
var toBeShifted = new List<OsuHitObject>();
for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
{
// only shift hit circles
if (!(workingObjects[j].HitObject is HitCircle)) break;
toBeShifted.Add(workingObjects[j].HitObject);
}
if (toBeShifted.Count > 0)
applyDecreasingShift(toBeShifted, shift);
}
previous = current;
}
return workingObjects.Select(p => p.HitObject).ToList();
}
/// <summary>
/// Compute the modified position of a hit object while attempting to keep it inside the playfield.
/// </summary>
/// <param name="current">The <see cref="WorkingObject"/> representing the hit object to have the modified position computed for.</param>
/// <param name="previous">The <see cref="WorkingObject"/> representing the hit object immediately preceding the current one.</param>
/// <param name="beforePrevious">The <see cref="WorkingObject"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param>
private static void computeModifiedPosition(WorkingObject current, WorkingObject? previous, WorkingObject? beforePrevious)
{
float previousAbsoluteAngle = 0f;
if (previous != null)
{
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
}
float absoluteAngle = previousAbsoluteAngle + current.PositionInfo.RelativeAngle;
var posRelativeToPrev = new Vector2(
current.PositionInfo.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
current.PositionInfo.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
);
Vector2 lastEndPosition = previous?.EndPositionModified ?? playfield_centre;
posRelativeToPrev = RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
current.PositionModified = lastEndPosition + posRelativeToPrev;
}
/// <summary>
/// Move the modified position of a <see cref="HitCircle"/> so that it fits inside the playfield.
/// </summary>
/// <returns>The deviation from the original modified position in order to fit within the playfield.</returns>
private static Vector2 clampHitCircleToPlayfield(WorkingObject workingObject)
{
var previousPosition = workingObject.PositionModified;
workingObject.EndPositionModified = workingObject.PositionModified = clampToPlayfieldWithPadding(
workingObject.PositionModified,
(float)workingObject.HitObject.Radius
);
workingObject.HitObject.Position = workingObject.PositionModified;
return workingObject.PositionModified - previousPosition;
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
/// <returns>The deviation from the original modified position in order to fit within the playfield.</returns>
private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
{
var slider = (Slider)workingObject.HitObject;
var possibleMovementBounds = calculatePossibleMovementBounds(slider);
var previousPosition = workingObject.PositionModified;
// Clamp slider position to the placement area
// If the slider is larger than the playfield, force it to stay at the original position
float newX = possibleMovementBounds.Width < 0
? workingObject.PositionOriginal.X
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
float newY = possibleMovementBounds.Height < 0
? workingObject.PositionOriginal.Y
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
workingObject.EndPositionModified = slider.EndPosition;
shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
return workingObject.PositionModified - previousPosition;
}
/// <summary>
/// Decreasingly shift a list of <see cref="OsuHitObject"/>s by a specified amount.
/// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
/// </summary>
/// <param name="hitObjects">The list of hit objects to be shifted.</param>
/// <param name="shift">The amount to be shifted.</param>
private static void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2 shift)
{
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
// The first object is shifted by a vector slightly smaller than shift
// The last object is shifted by a vector slightly larger than zero
Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
}
}
/// <summary>
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
private static RectangleF calculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float minY = float.PositiveInfinity;
float maxY = float.NegativeInfinity;
// Compute the bounding box of the slider.
foreach (var pos in pathPositions)
{
minX = MathF.Min(minX, pos.X);
maxX = MathF.Max(maxX, pos.X);
minY = MathF.Min(minY, pos.Y);
maxY = MathF.Max(maxY, pos.Y);
}
// Take the circle radius into account.
float radius = (float)slider.Radius;
minX -= radius;
minY -= radius;
maxX += radius;
maxY += radius;
// Given the bounding box of the slider (via min/max X/Y),
// the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
// and the amount that it can move to the right is WIDTH - maxX.
// Same calculation applies for the Y axis.
float left = -minX;
float right = OsuPlayfield.BASE_SIZE.X - maxX;
float top = -minY;
float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
return new RectangleF(left, top, right - left, bottom - top);
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private static void shiftNestedObjects(Slider slider, Vector2 shift)
{
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
osuHitObject.Position += shift;
}
}
/// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary>
/// <param name="position">The position to be clamped.</param>
/// <param name="padding">The minimum distance allowed from playfield edges.</param>
/// <returns>The clamped position.</returns>
private static Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
{
return new Vector2(
Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
);
}
public class ObjectPositionInfo
{
/// <summary>
/// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
/// </summary>
/// <remarks>
/// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
/// </remarks>
/// <example>
/// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing
/// the previous object to reach this one.
/// </example>
public float RelativeAngle { get; set; }
/// <summary>
/// The jump distance from the previous hit object to this one.
/// </summary>
/// <remarks>
/// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center.
/// </remarks>
public float DistanceFromPrevious { get; set; }
/// <summary>
/// The hit object associated with this <see cref="ObjectPositionInfo"/>.
/// </summary>
public OsuHitObject HitObject { get; }
public ObjectPositionInfo(OsuHitObject hitObject)
{
HitObject = hitObject;
}
}
private class WorkingObject
{
public Vector2 PositionOriginal { get; }
public Vector2 PositionModified { get; set; }
public Vector2 EndPositionModified { get; set; }
public ObjectPositionInfo PositionInfo { get; }
public OsuHitObject HitObject => PositionInfo.HitObject;
public WorkingObject(ObjectPositionInfo positionInfo)
{
PositionInfo = positionInfo;
PositionModified = PositionOriginal = HitObject.Position;
EndPositionModified = HitObject.EndPosition;
}
}
}
}

View File

@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[TestCase(2.2420075288523802d, "diffcalc-test")]
[TestCase(2.2420075288523802d, "diffcalc-test-strong")]
public void Test(double expected, string name)
=> base.Test(expected, name);
[TestCase(2.2420075288523802d, 200, "diffcalc-test")]
[TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(3.134084469440479d, "diffcalc-test")]
[TestCase(3.134084469440479d, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new TaikoModDoubleTime());
[TestCase(3.134084469440479d, 200, "diffcalc-test")]
[TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap);

View File

@ -28,9 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
// flying hits all land in one common scrolling container (and stay there for rewind purposes),
// so we need to manually get the latest one.
flyingHit = this.ChildrenOfType<DrawableFlyingHit>()
.OrderByDescending(h => h.HitObject.StartTime)
.FirstOrDefault();
flyingHit = this.ChildrenOfType<DrawableFlyingHit>().MaxBy(h => h.HitObject.StartTime);
});
AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType);

View File

@ -141,7 +141,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
double staminaPeak = (staminaRightPeaks[i] + staminaLeftPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak));
double peak = norm(2, colourPeak, rhythmPeak, staminaPeak);
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty.
if (peak > 0)
peaks.Add(peak);
}
double difficulty = 0;

View File

@ -3,19 +3,14 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
}
}

View File

@ -3,20 +3,15 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModCinema : ModCinema<TaikoHitObject>
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{
ScoreInfo = new ScoreInfo { User = new APIUser { Username = "mekkadosu!" } },
Replay = new TaikoAutoGenerator(beatmap).Generate(),
};
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new TaikoAutoGenerator(beatmap).Generate(), new ModCreatedUser { Username = "mekkadosu!" });
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// <summary>
/// Default size of a drawable taiko hit object.
/// </summary>
public const float DEFAULT_SIZE = 0.45f;
public const float DEFAULT_SIZE = 0.475f;
public override Judgement CreateJudgement() => new TaikoJudgement();

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// <summary>
/// Scale multiplier for a strong drawable taiko hit object.
/// </summary>
public const float STRONG_SCALE = 1.4f;
public const float STRONG_SCALE = 1 / 0.65f;
/// <summary>
/// Default size of a strong drawable taiko hit object.

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Default
@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
/// </summary>
public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
{
public const float SYMBOL_SIZE = 0.45f;
public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_SIZE;
public const float SYMBOL_BORDER = 8;
private const double pre_beat_transition_time = 80;
private Color4 accentColour;

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("approachcircle"),
Scale = new Vector2(0.73f),
Scale = new Vector2(0.83f),
Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite
{
Texture = skin.GetTexture("taikobigcircle"),
Scale = new Vector2(0.7f),
Scale = new Vector2(0.8f),
Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -57,6 +57,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.DrumRollTick:
return this.GetAnimation("sliderscorepoint", false, false);
case TaikoSkinComponents.Swell:
// todo: support taiko legacy swell (https://github.com/ppy/osu/issues/13601).
return null;
case TaikoSkinComponents.HitTarget:
if (GetTexture("taikobigcircle") != null)
return new TaikoLegacyHitTarget();
@ -119,6 +123,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
case TaikoSkinComponents.Mascot:
return new DrawableTaikoMascot();
default:
throw new UnsupportedSkinComponentException(component);
}
}

View File

@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName)
{
}
}

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