mirror of
https://github.com/ppy/osu.git
synced 2025-01-14 18:32:56 +08:00
Merge branch 'master' into fix-taiko-hd
This commit is contained in:
commit
737e622199
30
.github/ISSUE_TEMPLATE/01-bug-issues.md
vendored
30
.github/ISSUE_TEMPLATE/01-bug-issues.md
vendored
@ -1,30 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug Report
|
|
||||||
about: Report a bug or crash to desktop
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
IMPORTANT: Your issue may already be reported.
|
|
||||||
|
|
||||||
Please check:
|
|
||||||
- Pinned issues, at the top of https://github.com/ppy/osu/issues
|
|
||||||
- Current priority 0 issues at https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0
|
|
||||||
- Search for your issue. If you find that it already exists, please respond with a reaction or add any further information that may be helpful.
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
**Describe the bug:**
|
|
||||||
|
|
||||||
**Screenshots or videos showing encountered issue:**
|
|
||||||
|
|
||||||
**osu!lazer version:**
|
|
||||||
|
|
||||||
**Logs:**
|
|
||||||
|
|
||||||
<!--
|
|
||||||
*please attach logs here, which are located at:*
|
|
||||||
- `%AppData%/osu/logs` *(on Windows),*
|
|
||||||
- `~/.local/share/osu/logs` *(on Linux & macOS).*
|
|
||||||
- `Android/data/sh.ppy.osulazer/files/logs` *(on Android)*,
|
|
||||||
- on iOS they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
|
|
||||||
-->
|
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,12 +1,12 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Suggestions or feature request
|
|
||||||
url: https://github.com/ppy/osu/discussions/categories/ideas
|
|
||||||
about: Got something you think should change or be added? Search for or start a new discussion!
|
|
||||||
- name: Help
|
- name: Help
|
||||||
url: https://github.com/ppy/osu/discussions/categories/q-a
|
url: https://github.com/ppy/osu/discussions/categories/q-a
|
||||||
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
|
about: osu! not working as you'd expect? Not sure it's a bug? Check the Q&A section!
|
||||||
|
- name: Suggestions or feature request
|
||||||
|
url: https://github.com/ppy/osu/discussions/categories/ideas
|
||||||
|
about: Got something you think should change or be added? Search for or start a new discussion!
|
||||||
- name: osu!stable issues
|
- name: osu!stable issues
|
||||||
url: https://github.com/ppy/osu-stable-issues
|
url: https://github.com/ppy/osu-stable-issues
|
||||||
about: For osu!stable bugs (not osu!lazer), check out the dedicated repository. Note that we only accept serious bug reports.
|
about: For osu!(stable) - ie. the current "live" game version, check out the dedicated repository. Note that this is for serious bug reports only, not tech support.
|
||||||
|
|
||||||
|
199
.github/workflows/diffcalc.yml
vendored
Normal file
199
.github/workflows/diffcalc.yml
vendored
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
|
||||||
|
# Usage:
|
||||||
|
# !pp check 0 | Runs only the osu! ruleset.
|
||||||
|
# !pp check 0 2 | Runs only the osu! and catch rulesets.
|
||||||
|
#
|
||||||
|
|
||||||
|
name: Difficulty Calculation
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [ created ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CONCURRENCY: 4
|
||||||
|
ALLOW_DOWNLOAD: 1
|
||||||
|
SAVE_DOWNLOADED: 1
|
||||||
|
SKIP_INSERT_ATTRIBUTES: 1
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
metadata:
|
||||||
|
name: Check for requests
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: github.event.issue.pull_request && contains(github.event.comment.body, '!pp check') && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
|
||||||
|
outputs:
|
||||||
|
matrix: ${{ steps.generate-matrix.outputs.matrix }}
|
||||||
|
continue: ${{ steps.generate-matrix.outputs.continue }}
|
||||||
|
steps:
|
||||||
|
- name: Construct build matrix
|
||||||
|
id: generate-matrix
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.comment.body }}" =~ "osu" ]] ; then
|
||||||
|
MATRIX_PROJECTS_JSON+='{ "name": "osu", "id": 0 },'
|
||||||
|
fi
|
||||||
|
if [[ "${{ github.event.comment.body }}" =~ "taiko" ]] ; then
|
||||||
|
MATRIX_PROJECTS_JSON+='{ "name": "taiko", "id": 1 },'
|
||||||
|
fi
|
||||||
|
if [[ "${{ github.event.comment.body }}" =~ "catch" ]] ; then
|
||||||
|
MATRIX_PROJECTS_JSON+='{ "name": "catch", "id": 2 },'
|
||||||
|
fi
|
||||||
|
if [[ "${{ github.event.comment.body }}" =~ "mania" ]] ; then
|
||||||
|
MATRIX_PROJECTS_JSON+='{ "name": "mania", "id": 3 },'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${MATRIX_PROJECTS_JSON}" != "" ]]; then
|
||||||
|
MATRIX_JSON="{ \"ruleset\": [ ${MATRIX_PROJECTS_JSON} ] }"
|
||||||
|
echo "${MATRIX_JSON}"
|
||||||
|
CONTINUE="yes"
|
||||||
|
else
|
||||||
|
CONTINUE="no"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "::set-output name=continue::${CONTINUE}"
|
||||||
|
echo "::set-output name=matrix::${MATRIX_JSON}"
|
||||||
|
diffcalc:
|
||||||
|
name: Run
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: needs.metadata.outputs.continue == 'yes'
|
||||||
|
needs: metadata
|
||||||
|
strategy:
|
||||||
|
matrix: ${{ fromJson(needs.metadata.outputs.matrix) }}
|
||||||
|
steps:
|
||||||
|
- name: Verify MySQL connection from host
|
||||||
|
run: |
|
||||||
|
mysql -e "SHOW DATABASES"
|
||||||
|
|
||||||
|
- name: Drop previous databases
|
||||||
|
run: |
|
||||||
|
for db in osu_master osu_pr
|
||||||
|
do
|
||||||
|
mysql -e "DROP DATABASE IF EXISTS $db"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Create directory structure
|
||||||
|
run: |
|
||||||
|
mkdir -p $GITHUB_WORKSPACE/master/
|
||||||
|
mkdir -p $GITHUB_WORKSPACE/pr/
|
||||||
|
|
||||||
|
# Checkout osu
|
||||||
|
- name: Checkout osu (master)
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: peppy/osu
|
||||||
|
ref: 'diffcalc-optimisations'
|
||||||
|
path: 'master/osu'
|
||||||
|
- name: Checkout osu (pr)
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
path: 'pr/osu'
|
||||||
|
|
||||||
|
- name: Checkout osu-difficulty-calculator (master)
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: peppy/osu-difficulty-calculator
|
||||||
|
ref: 'bypass-attrib-row-insert'
|
||||||
|
path: 'master/osu-difficulty-calculator'
|
||||||
|
- name: Checkout osu-difficulty-calculator (pr)
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: peppy/osu-difficulty-calculator
|
||||||
|
ref: 'bypass-attrib-row-insert'
|
||||||
|
path: 'pr/osu-difficulty-calculator'
|
||||||
|
|
||||||
|
- name: Install .NET 5.0.x
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: "5.0.x"
|
||||||
|
|
||||||
|
# Sanity checks to make sure diffcalc is not run when incompatible.
|
||||||
|
- name: Build diffcalc (master)
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator
|
||||||
|
./UseLocalOsu.sh
|
||||||
|
dotnet build
|
||||||
|
- name: Build diffcalc (pr)
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator
|
||||||
|
./UseLocalOsu.sh
|
||||||
|
dotnet build
|
||||||
|
|
||||||
|
- name: Download + import data
|
||||||
|
run: |
|
||||||
|
PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top_1000 | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||||
|
BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||||
|
|
||||||
|
# Set env variable for further steps.
|
||||||
|
echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
|
||||||
|
echo "Downloading database dump $PERFORMANCE_DATA_NAME.."
|
||||||
|
wget -q -nc https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2
|
||||||
|
echo "Extracting.."
|
||||||
|
tar -xf $PERFORMANCE_DATA_NAME.tar.bz2
|
||||||
|
|
||||||
|
echo "Downloading beatmap dump $BEATMAPS_DATA_NAME.."
|
||||||
|
wget -q -nc https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2
|
||||||
|
echo "Extracting.."
|
||||||
|
tar -xf $BEATMAPS_DATA_NAME.tar.bz2
|
||||||
|
|
||||||
|
cd $PERFORMANCE_DATA_NAME
|
||||||
|
|
||||||
|
for db in osu_master osu_pr
|
||||||
|
do
|
||||||
|
echo "Setting up database $db.."
|
||||||
|
|
||||||
|
mysql -e "CREATE DATABASE $db"
|
||||||
|
|
||||||
|
echo "Importing beatmaps.."
|
||||||
|
cat osu_beatmaps.sql | mysql $db
|
||||||
|
echo "Importing beatmapsets.."
|
||||||
|
cat osu_beatmapsets.sql | mysql $db
|
||||||
|
|
||||||
|
echo "Creating table structure.."
|
||||||
|
mysql $db -e 'CREATE TABLE `osu_beatmap_difficulty` (
|
||||||
|
`beatmap_id` int unsigned NOT NULL,
|
||||||
|
`mode` tinyint NOT NULL DEFAULT 0,
|
||||||
|
`mods` int unsigned NOT NULL,
|
||||||
|
`diff_unified` float NOT NULL,
|
||||||
|
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`beatmap_id`,`mode`,`mods`),
|
||||||
|
KEY `diff_sort` (`mode`,`mods`,`diff_unified`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;'
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Run diffcalc (master)
|
||||||
|
env:
|
||||||
|
DB_NAME: osu_master
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
|
||||||
|
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
|
||||||
|
- name: Run diffcalc (pr)
|
||||||
|
env:
|
||||||
|
DB_NAME: osu_pr
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator
|
||||||
|
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
|
||||||
|
|
||||||
|
- name: Print diffs
|
||||||
|
run: |
|
||||||
|
mysql -e "
|
||||||
|
SELECT
|
||||||
|
m.beatmap_id,
|
||||||
|
m.mods,
|
||||||
|
b.filename,
|
||||||
|
m.diff_unified as 'sr_master',
|
||||||
|
p.diff_unified as 'sr_pr',
|
||||||
|
(p.diff_unified - m.diff_unified) as 'diff'
|
||||||
|
FROM osu_master.osu_beatmap_difficulty m
|
||||||
|
JOIN osu_pr.osu_beatmap_difficulty p
|
||||||
|
ON m.beatmap_id = p.beatmap_id
|
||||||
|
AND m.mode = p.mode
|
||||||
|
AND m.mods = p.mods
|
||||||
|
JOIN osu_pr.osu_beatmaps b
|
||||||
|
ON b.beatmap_id = p.beatmap_id
|
||||||
|
WHERE abs(m.diff_unified - p.diff_unified) > 0.1
|
||||||
|
ORDER BY abs(m.diff_unified - p.diff_unified)
|
||||||
|
DESC
|
||||||
|
LIMIT 10000;"
|
||||||
|
|
||||||
|
# Todo: Run ppcalc
|
163
.github/workflows/test-diffcalc.yml
vendored
163
.github/workflows/test-diffcalc.yml
vendored
@ -1,163 +0,0 @@
|
|||||||
# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
|
|
||||||
# Usage:
|
|
||||||
# !pp check 0 | Runs only the osu! ruleset.
|
|
||||||
# !pp check 0 2 | Runs only the osu! and catch rulesets.
|
|
||||||
#
|
|
||||||
|
|
||||||
name: Diffcalc Consistency Checks
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [ created ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
DB_USER: root
|
|
||||||
DB_HOST: 127.0.0.1
|
|
||||||
CONCURRENCY: 4
|
|
||||||
ALLOW_DOWNLOAD: 1
|
|
||||||
SAVE_DOWNLOADED: 1
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
diffcalc:
|
|
||||||
name: Diffcalc
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
if: |
|
|
||||||
github.event.issue.pull_request &&
|
|
||||||
contains(github.event.comment.body, '!pp check') &&
|
|
||||||
(github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
ruleset:
|
|
||||||
- { name: osu, id: 0 }
|
|
||||||
- { name: taiko, id: 1 }
|
|
||||||
- { name: catch, id: 2 }
|
|
||||||
- { name: mania, id: 3 }
|
|
||||||
|
|
||||||
services:
|
|
||||||
mysql:
|
|
||||||
image: mysql:8.0
|
|
||||||
env:
|
|
||||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
|
||||||
ports:
|
|
||||||
- 3306:3306
|
|
||||||
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Verify ruleset
|
|
||||||
if: contains(github.event.comment.body, matrix.ruleset.id) == false
|
|
||||||
run: |
|
|
||||||
echo "${{ github.event.comment.body }} doesn't contain ${{ matrix.ruleset.id }}"
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
- name: Verify MySQL connection from host
|
|
||||||
run: |
|
|
||||||
sudo apt-get install -y mysql-client
|
|
||||||
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "SHOW DATABASES"
|
|
||||||
|
|
||||||
- name: Create directory structure
|
|
||||||
run: |
|
|
||||||
mkdir -p $GITHUB_WORKSPACE/master/
|
|
||||||
mkdir -p $GITHUB_WORKSPACE/pr/
|
|
||||||
|
|
||||||
# Checkout osu
|
|
||||||
- name: Checkout osu (master)
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
repository: ppy/osu
|
|
||||||
path: 'master/osu'
|
|
||||||
- name: Checkout osu (pr)
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
path: 'pr/osu'
|
|
||||||
|
|
||||||
# Checkout osu-difficulty-calculator
|
|
||||||
- name: Checkout osu-difficulty-calculator (master)
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
repository: ppy/osu-difficulty-calculator
|
|
||||||
path: 'master/osu-difficulty-calculator'
|
|
||||||
- name: Checkout osu-difficulty-calculator (pr)
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
with:
|
|
||||||
repository: ppy/osu-difficulty-calculator
|
|
||||||
path: 'pr/osu-difficulty-calculator'
|
|
||||||
|
|
||||||
- name: Install .NET 5.0.x
|
|
||||||
uses: actions/setup-dotnet@v1
|
|
||||||
with:
|
|
||||||
dotnet-version: "5.0.x"
|
|
||||||
|
|
||||||
# Sanity checks to make sure diffcalc is not run when incompatible.
|
|
||||||
- name: Build diffcalc (master)
|
|
||||||
run: |
|
|
||||||
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator
|
|
||||||
./UseLocalOsu.sh
|
|
||||||
dotnet build
|
|
||||||
- name: Build diffcalc (pr)
|
|
||||||
run: |
|
|
||||||
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator
|
|
||||||
./UseLocalOsu.sh
|
|
||||||
dotnet build
|
|
||||||
|
|
||||||
# Initial data imports
|
|
||||||
- name: Download + import data
|
|
||||||
run: |
|
|
||||||
PERFORMANCE_DATA_NAME=$(curl https://data.ppy.sh/ | grep performance_${{ matrix.ruleset.name }}_top | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
|
|
||||||
BEATMAPS_DATA_NAME=$(curl https://data.ppy.sh/ | grep osu_files | tail -1 | awk -F "\"" '{print $2}' | sed 's/\.tar\.bz2//g')
|
|
||||||
|
|
||||||
# Set env variable for further steps.
|
|
||||||
echo "BEATMAPS_PATH=$GITHUB_WORKSPACE/$BEATMAPS_DATA_NAME" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
cd $GITHUB_WORKSPACE
|
|
||||||
|
|
||||||
wget https://data.ppy.sh/$PERFORMANCE_DATA_NAME.tar.bz2
|
|
||||||
wget https://data.ppy.sh/$BEATMAPS_DATA_NAME.tar.bz2
|
|
||||||
tar -xf $PERFORMANCE_DATA_NAME.tar.bz2
|
|
||||||
tar -xf $BEATMAPS_DATA_NAME.tar.bz2
|
|
||||||
|
|
||||||
cd $GITHUB_WORKSPACE/$PERFORMANCE_DATA_NAME
|
|
||||||
|
|
||||||
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_master"
|
|
||||||
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "CREATE DATABASE osu_pr"
|
|
||||||
|
|
||||||
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_master
|
|
||||||
cat *.sql | mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} --database=osu_pr
|
|
||||||
|
|
||||||
# Run diffcalc
|
|
||||||
- name: Run diffcalc (master)
|
|
||||||
env:
|
|
||||||
DB_NAME: osu_master
|
|
||||||
run: |
|
|
||||||
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
|
|
||||||
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
|
|
||||||
- name: Run diffcalc (pr)
|
|
||||||
env:
|
|
||||||
DB_NAME: osu_pr
|
|
||||||
run: |
|
|
||||||
cd $GITHUB_WORKSPACE/pr/osu-difficulty-calculator/osu.Server.DifficultyCalculator
|
|
||||||
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
|
|
||||||
|
|
||||||
# Print diffs
|
|
||||||
- name: Print diffs
|
|
||||||
run: |
|
|
||||||
mysql --host ${{ env.DB_HOST }} -u${{ env.DB_USER }} -e "
|
|
||||||
SELECT
|
|
||||||
m.beatmap_id,
|
|
||||||
m.mods,
|
|
||||||
m.diff_unified as 'sr_master',
|
|
||||||
p.diff_unified as 'sr_pr',
|
|
||||||
(p.diff_unified - m.diff_unified) as 'diff'
|
|
||||||
FROM osu_master.osu_beatmap_difficulty m
|
|
||||||
JOIN osu_pr.osu_beatmap_difficulty p
|
|
||||||
ON m.beatmap_id = p.beatmap_id
|
|
||||||
AND m.mode = p.mode
|
|
||||||
AND m.mods = p.mods
|
|
||||||
WHERE abs(m.diff_unified - p.diff_unified) > 0.1
|
|
||||||
ORDER BY abs(m.diff_unified - p.diff_unified)
|
|
||||||
DESC
|
|
||||||
LIMIT 10000;"
|
|
||||||
|
|
||||||
# Todo: Run ppcalc
|
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ContentModelUserStore">
|
<component name="UserContentModel">
|
||||||
<attachedFolders />
|
<attachedFolders />
|
||||||
<explicitIncludes />
|
<explicitIncludes />
|
||||||
<explicitExcludes />
|
<explicitExcludes />
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.913.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.916.1" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.916.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
|
174
osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
Normal file
174
osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Tests
|
||||||
|
{
|
||||||
|
public class TestSceneCursorParticles : TestSceneOsuPlayer
|
||||||
|
{
|
||||||
|
protected override bool Autoplay => autoplay;
|
||||||
|
protected override bool HasCustomSteps => true;
|
||||||
|
|
||||||
|
private bool autoplay;
|
||||||
|
private IBeatmap currentBeatmap;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SkinManager skinManager { get; set; }
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentBeatmap ?? base.CreateBeatmap(ruleset);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLegacyBreakParticles()
|
||||||
|
{
|
||||||
|
LegacyCursorParticles cursorParticles = null;
|
||||||
|
|
||||||
|
createLegacyTest(false, () => new Beatmap
|
||||||
|
{
|
||||||
|
Breaks =
|
||||||
|
{
|
||||||
|
new BreakPeriod(8500, 10000),
|
||||||
|
},
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = 8000,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = 11000,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("fetch cursor particles", () =>
|
||||||
|
{
|
||||||
|
cursorParticles = this.ChildrenOfType<LegacyCursorParticles>().SingleOrDefault();
|
||||||
|
return cursorParticles != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("move mouse to centre", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
|
||||||
|
|
||||||
|
AddAssert("particles are being spawned", () => cursorParticles.Active);
|
||||||
|
|
||||||
|
AddStep("press left mouse button", () => InputManager.PressButton(MouseButton.Left));
|
||||||
|
AddWaitStep("wait a bit", 5);
|
||||||
|
AddStep("press right mouse button", () => InputManager.PressButton(MouseButton.Right));
|
||||||
|
AddWaitStep("wait a bit", 5);
|
||||||
|
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||||
|
AddWaitStep("wait a bit", 5);
|
||||||
|
AddStep("release right mouse button", () => InputManager.ReleaseButton(MouseButton.Right));
|
||||||
|
|
||||||
|
AddUntilStep("wait for beatmap start", () => !Player.IsBreakTime.Value);
|
||||||
|
AddAssert("particle spawning stopped", () => !cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("wait for break", () => Player.IsBreakTime.Value);
|
||||||
|
AddAssert("particles are being spawned", () => cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("wait for break end", () => !Player.IsBreakTime.Value);
|
||||||
|
AddAssert("particle spawning stopped", () => !cursorParticles.Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLegacyKiaiParticles()
|
||||||
|
{
|
||||||
|
LegacyCursorParticles cursorParticles = null;
|
||||||
|
DrawableSpinner spinner = null;
|
||||||
|
DrawableSlider slider = null;
|
||||||
|
|
||||||
|
createLegacyTest(true, () =>
|
||||||
|
{
|
||||||
|
var controlPointInfo = new ControlPointInfo();
|
||||||
|
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
|
||||||
|
|
||||||
|
return new Beatmap
|
||||||
|
{
|
||||||
|
ControlPointInfo = controlPointInfo,
|
||||||
|
HitObjects =
|
||||||
|
{
|
||||||
|
new Spinner
|
||||||
|
{
|
||||||
|
StartTime = 0,
|
||||||
|
Duration = 1000,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
},
|
||||||
|
new Slider
|
||||||
|
{
|
||||||
|
StartTime = 2500,
|
||||||
|
RepeatCount = 0,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
Path = new SliderPath(new[]
|
||||||
|
{
|
||||||
|
new PathControlPoint(Vector2.Zero),
|
||||||
|
new PathControlPoint(new Vector2(100, 0)),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
new HitCircle
|
||||||
|
{
|
||||||
|
StartTime = 4500,
|
||||||
|
Position = OsuPlayfield.BASE_SIZE / 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
AddUntilStep("fetch cursor particles", () =>
|
||||||
|
{
|
||||||
|
cursorParticles = this.ChildrenOfType<LegacyCursorParticles>().SingleOrDefault();
|
||||||
|
return cursorParticles != null;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for spinner tracking", () =>
|
||||||
|
{
|
||||||
|
spinner = this.ChildrenOfType<DrawableSpinner>().SingleOrDefault();
|
||||||
|
return spinner?.RotationTracker.Tracking == true;
|
||||||
|
});
|
||||||
|
AddAssert("particles are being spawned", () => cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("spinner tracking stopped", () => !spinner.RotationTracker.Tracking);
|
||||||
|
AddAssert("particle spawning stopped", () => !cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("wait for slider tracking", () =>
|
||||||
|
{
|
||||||
|
slider = this.ChildrenOfType<DrawableSlider>().SingleOrDefault();
|
||||||
|
return slider?.Tracking.Value == true;
|
||||||
|
});
|
||||||
|
AddAssert("particles are being spawned", () => cursorParticles.Active);
|
||||||
|
|
||||||
|
AddUntilStep("slider tracking stopped", () => !slider.Tracking.Value);
|
||||||
|
AddAssert("particle spawning stopped", () => !cursorParticles.Active);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createLegacyTest(bool autoplay, Func<IBeatmap> beatmap) => CreateTest(() =>
|
||||||
|
{
|
||||||
|
AddStep("set beatmap", () =>
|
||||||
|
{
|
||||||
|
this.autoplay = autoplay;
|
||||||
|
currentBeatmap = beatmap();
|
||||||
|
});
|
||||||
|
AddStep("setup default legacy skin", () =>
|
||||||
|
{
|
||||||
|
skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
public double AimStrain { get; set; }
|
public double AimStrain { get; set; }
|
||||||
public double SpeedStrain { get; set; }
|
public double SpeedStrain { get; set; }
|
||||||
|
public double FlashlightRating { get; set; }
|
||||||
public double ApproachRate { get; set; }
|
public double ApproachRate { get; set; }
|
||||||
public double OverallDifficulty { get; set; }
|
public double OverallDifficulty { get; set; }
|
||||||
public int HitCircleCount { get; set; }
|
public int HitCircleCount { get; set; }
|
||||||
|
@ -34,10 +34,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
||||||
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||||
|
double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||||
|
|
||||||
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
|
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
|
||||||
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
|
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
|
||||||
double basePerformance = Math.Pow(Math.Pow(baseAimPerformance, 1.1) + Math.Pow(baseSpeedPerformance, 1.1), 1 / 1.1);
|
double baseFlashlightPerformance = 0.0;
|
||||||
|
|
||||||
|
if (mods.Any(h => h is OsuModFlashlight))
|
||||||
|
baseFlashlightPerformance = Math.Pow(flashlightRating, 2.0) * 25.0;
|
||||||
|
|
||||||
|
double basePerformance =
|
||||||
|
Math.Pow(
|
||||||
|
Math.Pow(baseAimPerformance, 1.1) +
|
||||||
|
Math.Pow(baseSpeedPerformance, 1.1) +
|
||||||
|
Math.Pow(baseFlashlightPerformance, 1.1), 1.0 / 1.1
|
||||||
|
);
|
||||||
|
|
||||||
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
|
||||||
|
|
||||||
HitWindows hitWindows = new OsuHitWindows();
|
HitWindows hitWindows = new OsuHitWindows();
|
||||||
@ -60,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
Mods = mods,
|
Mods = mods,
|
||||||
AimStrain = aimRating,
|
AimStrain = aimRating,
|
||||||
SpeedStrain = speedRating,
|
SpeedStrain = speedRating,
|
||||||
|
FlashlightRating = flashlightRating,
|
||||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||||
MaxCombo = maxCombo,
|
MaxCombo = maxCombo,
|
||||||
@ -86,7 +99,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
|
||||||
{
|
{
|
||||||
new Aim(mods),
|
new Aim(mods),
|
||||||
new Speed(mods)
|
new Speed(mods),
|
||||||
|
new Flashlight(mods)
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||||
@ -95,6 +109,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
new OsuModHalfTime(),
|
new OsuModHalfTime(),
|
||||||
new OsuModEasy(),
|
new OsuModEasy(),
|
||||||
new OsuModHardRock(),
|
new OsuModHardRock(),
|
||||||
|
new OsuModFlashlight(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||||
|
|
||||||
// Custom multipliers for NoFail and SpunOut.
|
// Custom multipliers for NoFail and SpunOut.
|
||||||
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
|
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
|
||||||
|
|
||||||
if (mods.Any(m => m is OsuModNoFail))
|
if (mods.Any(m => m is OsuModNoFail))
|
||||||
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
|
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
|
||||||
@ -52,11 +52,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
double aimValue = computeAimValue();
|
double aimValue = computeAimValue();
|
||||||
double speedValue = computeSpeedValue();
|
double speedValue = computeSpeedValue();
|
||||||
double accuracyValue = computeAccuracyValue();
|
double accuracyValue = computeAccuracyValue();
|
||||||
|
double flashlightValue = computeFlashlightValue();
|
||||||
double totalValue =
|
double totalValue =
|
||||||
Math.Pow(
|
Math.Pow(
|
||||||
Math.Pow(aimValue, 1.1) +
|
Math.Pow(aimValue, 1.1) +
|
||||||
Math.Pow(speedValue, 1.1) +
|
Math.Pow(speedValue, 1.1) +
|
||||||
Math.Pow(accuracyValue, 1.1), 1.0 / 1.1
|
Math.Pow(accuracyValue, 1.1) +
|
||||||
|
Math.Pow(flashlightValue, 1.1), 1.0 / 1.1
|
||||||
) * multiplier;
|
) * multiplier;
|
||||||
|
|
||||||
if (categoryRatings != null)
|
if (categoryRatings != null)
|
||||||
@ -64,6 +66,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
categoryRatings.Add("Aim", aimValue);
|
categoryRatings.Add("Aim", aimValue);
|
||||||
categoryRatings.Add("Speed", speedValue);
|
categoryRatings.Add("Speed", speedValue);
|
||||||
categoryRatings.Add("Accuracy", accuracyValue);
|
categoryRatings.Add("Accuracy", accuracyValue);
|
||||||
|
categoryRatings.Add("Flashlight", flashlightValue);
|
||||||
categoryRatings.Add("OD", Attributes.OverallDifficulty);
|
categoryRatings.Add("OD", Attributes.OverallDifficulty);
|
||||||
categoryRatings.Add("AR", Attributes.ApproachRate);
|
categoryRatings.Add("AR", Attributes.ApproachRate);
|
||||||
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
|
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
|
||||||
@ -81,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
|
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||||
|
|
||||||
// Longer maps are worth more
|
// Longer maps are worth more.
|
||||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
|
|
||||||
@ -91,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (countMiss > 0)
|
if (countMiss > 0)
|
||||||
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
|
aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss);
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling.
|
||||||
if (Attributes.MaxCombo > 0)
|
if (Attributes.MaxCombo > 0)
|
||||||
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
||||||
|
|
||||||
@ -109,23 +112,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (mods.Any(h => h is OsuModHidden))
|
if (mods.Any(h => h is OsuModHidden))
|
||||||
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||||
|
|
||||||
double flashlightBonus = 1.0;
|
aimValue *= approachRateBonus;
|
||||||
|
|
||||||
if (mods.Any(h => h is OsuModFlashlight))
|
// Scale the aim value with accuracy _slightly_.
|
||||||
{
|
|
||||||
// Apply object-based bonus for flashlight.
|
|
||||||
flashlightBonus = 1.0 + 0.35 * Math.Min(1.0, totalHits / 200.0) +
|
|
||||||
(totalHits > 200
|
|
||||||
? 0.3 * Math.Min(1.0, (totalHits - 200) / 300.0) +
|
|
||||||
(totalHits > 500 ? (totalHits - 500) / 1200.0 : 0.0)
|
|
||||||
: 0.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
aimValue *= Math.Max(flashlightBonus, approachRateBonus);
|
|
||||||
|
|
||||||
// Scale the aim value with accuracy _slightly_
|
|
||||||
aimValue *= 0.5 + accuracy / 2.0;
|
aimValue *= 0.5 + accuracy / 2.0;
|
||||||
// It is important to also consider accuracy difficulty when doing that
|
// It is important to also consider accuracy difficulty when doing that.
|
||||||
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
|
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
|
||||||
|
|
||||||
return aimValue;
|
return aimValue;
|
||||||
@ -135,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
{
|
{
|
||||||
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
|
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||||
|
|
||||||
// Longer maps are worth more
|
// Longer maps are worth more.
|
||||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||||
speedValue *= lengthBonus;
|
speedValue *= lengthBonus;
|
||||||
@ -144,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (countMiss > 0)
|
if (countMiss > 0)
|
||||||
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
|
speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
|
||||||
|
|
||||||
// Combo scaling
|
// Combo scaling.
|
||||||
if (Attributes.MaxCombo > 0)
|
if (Attributes.MaxCombo > 0)
|
||||||
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
||||||
|
|
||||||
@ -159,7 +150,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
if (mods.Any(m => m is OsuModHidden))
|
if (mods.Any(m => m is OsuModHidden))
|
||||||
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
|
||||||
|
|
||||||
// Scale the speed value with accuracy and OD
|
// Scale the speed value with accuracy and OD.
|
||||||
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
|
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
|
||||||
// Scale the speed value with # of 50s to punish doubletapping.
|
// Scale the speed value with # of 50s to punish doubletapping.
|
||||||
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||||
@ -169,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
private double computeAccuracyValue()
|
private double computeAccuracyValue()
|
||||||
{
|
{
|
||||||
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
|
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
|
||||||
double betterAccuracyPercentage;
|
double betterAccuracyPercentage;
|
||||||
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
|
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
|
||||||
|
|
||||||
@ -178,15 +169,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
else
|
else
|
||||||
betterAccuracyPercentage = 0;
|
betterAccuracyPercentage = 0;
|
||||||
|
|
||||||
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points
|
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.
|
||||||
if (betterAccuracyPercentage < 0)
|
if (betterAccuracyPercentage < 0)
|
||||||
betterAccuracyPercentage = 0;
|
betterAccuracyPercentage = 0;
|
||||||
|
|
||||||
// Lots of arbitrary values from testing.
|
// Lots of arbitrary values from testing.
|
||||||
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
|
||||||
double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||||
|
|
||||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
||||||
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
||||||
|
|
||||||
if (mods.Any(m => m is OsuModHidden))
|
if (mods.Any(m => m is OsuModHidden))
|
||||||
@ -197,6 +188,42 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
return accuracyValue;
|
return accuracyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private double computeFlashlightValue()
|
||||||
|
{
|
||||||
|
if (!mods.Any(h => h is OsuModFlashlight))
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
double rawFlashlight = Attributes.FlashlightRating;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModTouchDevice))
|
||||||
|
rawFlashlight = Math.Pow(rawFlashlight, 0.8);
|
||||||
|
|
||||||
|
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
|
||||||
|
|
||||||
|
// Add an additional bonus for HDFL.
|
||||||
|
if (mods.Any(h => h is OsuModHidden))
|
||||||
|
flashlightValue *= 1.3;
|
||||||
|
|
||||||
|
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||||
|
if (countMiss > 0)
|
||||||
|
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875));
|
||||||
|
|
||||||
|
// Combo scaling.
|
||||||
|
if (Attributes.MaxCombo > 0)
|
||||||
|
flashlightValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
|
||||||
|
|
||||||
|
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
|
||||||
|
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
|
||||||
|
(totalHits > 200 ? 0.2 * Math.Min(1.0, (totalHits - 200) / 200.0) : 0.0);
|
||||||
|
|
||||||
|
// Scale the flashlight value with accuracy _slightly_.
|
||||||
|
flashlightValue *= 0.5 + accuracy / 2.0;
|
||||||
|
// It is important to also consider accuracy difficulty when doing that.
|
||||||
|
flashlightValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
|
||||||
|
|
||||||
|
return flashlightValue;
|
||||||
|
}
|
||||||
|
|
||||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||||
}
|
}
|
||||||
|
66
osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
Normal file
66
osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// 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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the skill required to memorise and hit every object in a map with the Flashlight mod enabled.
|
||||||
|
/// </summary>
|
||||||
|
public class Flashlight : OsuStrainSkill
|
||||||
|
{
|
||||||
|
public Flashlight(Mod[] mods)
|
||||||
|
: base(mods)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double SkillMultiplier => 0.15;
|
||||||
|
protected override double StrainDecayBase => 0.15;
|
||||||
|
protected override double DecayWeight => 1.0;
|
||||||
|
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
if (current.BaseObject is Spinner)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var osuCurrent = (OsuDifficultyHitObject)current;
|
||||||
|
var osuHitObject = (OsuHitObject)(osuCurrent.BaseObject);
|
||||||
|
|
||||||
|
double scalingFactor = 52.0 / osuHitObject.Radius;
|
||||||
|
double smallDistNerf = 1.0;
|
||||||
|
double cumulativeStrainTime = 0.0;
|
||||||
|
|
||||||
|
double result = 0.0;
|
||||||
|
|
||||||
|
for (int i = 0; i < Previous.Count; i++)
|
||||||
|
{
|
||||||
|
var osuPrevious = (OsuDifficultyHitObject)Previous[i];
|
||||||
|
var osuPreviousHitObject = (OsuHitObject)(osuPrevious.BaseObject);
|
||||||
|
|
||||||
|
if (!(osuPrevious.BaseObject is Spinner))
|
||||||
|
{
|
||||||
|
double jumpDistance = (osuHitObject.StackedPosition - osuPreviousHitObject.EndPosition).Length;
|
||||||
|
|
||||||
|
cumulativeStrainTime += osuPrevious.StrainTime;
|
||||||
|
|
||||||
|
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
||||||
|
if (i == 0)
|
||||||
|
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
|
||||||
|
|
||||||
|
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
||||||
|
double stackNerf = Math.Min(1.0, (osuPrevious.JumpDistance / scalingFactor) / 25.0);
|
||||||
|
|
||||||
|
result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Pow(smallDistNerf * result, 2.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
public SliderBall Ball { get; private set; }
|
public SliderBall Ball { get; private set; }
|
||||||
public SkinnableDrawable Body { get; private set; }
|
public SkinnableDrawable Body { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A target container which can be used to add top level elements to the slider's display.
|
||||||
|
/// Intended to be used for proxy purposes only.
|
||||||
|
/// </summary>
|
||||||
|
public Container OverlayElementContainer { get; private set; }
|
||||||
|
|
||||||
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
|
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
|
||||||
|
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
@ -65,6 +71,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
|
||||||
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
|
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
|
||||||
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
|
||||||
|
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
||||||
|
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
|
||||||
Ball = new SliderBall(this)
|
Ball = new SliderBall(this)
|
||||||
{
|
{
|
||||||
GetInitialHitAction = () => HeadCircle.HitAction,
|
GetInitialHitAction = () => HeadCircle.HitAction,
|
||||||
@ -72,7 +80,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
AlwaysPresent = true,
|
AlwaysPresent = true,
|
||||||
Alpha = 0
|
Alpha = 0
|
||||||
},
|
},
|
||||||
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
|
|
||||||
slidingSample = new PausableSkinnableSound { Looping = true }
|
slidingSample = new PausableSkinnableSound { Looping = true }
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -179,6 +186,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
tailContainer.Clear(false);
|
tailContainer.Clear(false);
|
||||||
repeatContainer.Clear(false);
|
repeatContainer.Clear(false);
|
||||||
tickContainer.Clear(false);
|
tickContainer.Clear(false);
|
||||||
|
|
||||||
|
OverlayElementContainer.Clear(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
public Slider Slider => DrawableSlider?.HitObject;
|
public Slider Slider => DrawableSlider?.HitObject;
|
||||||
|
|
||||||
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||||
|
|
||||||
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
public Slider Slider => DrawableSlider?.HitObject;
|
public Slider Slider => DrawableSlider?.HitObject;
|
||||||
|
|
||||||
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
public DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
|
||||||
|
|
||||||
private double animDuration;
|
private double animDuration;
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
FollowPoint,
|
FollowPoint,
|
||||||
Cursor,
|
Cursor,
|
||||||
CursorTrail,
|
CursorTrail,
|
||||||
|
CursorParticles,
|
||||||
SliderScorePoint,
|
SliderScorePoint,
|
||||||
ReverseArrow,
|
ReverseArrow,
|
||||||
HitCircleText,
|
HitCircleText,
|
||||||
|
256
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs
Normal file
256
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
// 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.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.UI;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
|
{
|
||||||
|
public class LegacyCursorParticles : CompositeDrawable, IKeyBindingHandler<OsuAction>
|
||||||
|
{
|
||||||
|
public bool Active => breakSpewer?.Active.Value == true || kiaiSpewer?.Active.Value == true;
|
||||||
|
|
||||||
|
private LegacyCursorParticleSpewer breakSpewer;
|
||||||
|
private LegacyCursorParticleSpewer kiaiSpewer;
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private Player player { get; set; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private OsuPlayfield playfield { get; set; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private GameplayBeatmap gameplayBeatmap { get; set; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private GameplayClock gameplayClock { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(ISkinSource skin, OsuColour colours)
|
||||||
|
{
|
||||||
|
var texture = skin.GetTexture("star2");
|
||||||
|
var starBreakAdditive = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.StarBreakAdditive)?.Value ?? new Color4(255, 182, 193, 255);
|
||||||
|
|
||||||
|
if (texture != null)
|
||||||
|
{
|
||||||
|
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
|
||||||
|
texture.ScaleAdjust *= 1.6f;
|
||||||
|
}
|
||||||
|
|
||||||
|
InternalChildren = new[]
|
||||||
|
{
|
||||||
|
breakSpewer = new LegacyCursorParticleSpewer(texture, 20)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = starBreakAdditive,
|
||||||
|
Direction = SpewDirection.None,
|
||||||
|
},
|
||||||
|
kiaiSpewer = new LegacyCursorParticleSpewer(texture, 60)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = starBreakAdditive,
|
||||||
|
Direction = SpewDirection.None,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (player != null)
|
||||||
|
((IBindable<bool>)breakSpewer.Active).BindTo(player.IsBreakTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
if (playfield == null || gameplayBeatmap == null) return;
|
||||||
|
|
||||||
|
DrawableHitObject kiaiHitObject = null;
|
||||||
|
|
||||||
|
// Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary.
|
||||||
|
if (gameplayBeatmap.ControlPointInfo.EffectPointAt(gameplayBeatmap.Time.Current).KiaiMode)
|
||||||
|
kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking);
|
||||||
|
|
||||||
|
kiaiSpewer.Active.Value = kiaiHitObject != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool isTracking(DrawableHitObject h)
|
||||||
|
{
|
||||||
|
if (!h.HitObject.Kiai)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
switch (h)
|
||||||
|
{
|
||||||
|
case DrawableSlider slider:
|
||||||
|
return slider.Tracking.Value;
|
||||||
|
|
||||||
|
case DrawableSpinner spinner:
|
||||||
|
return spinner.RotationTracker.Tracking;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||||
|
{
|
||||||
|
handleInput(e.Action, true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
|
||||||
|
{
|
||||||
|
handleInput(e.Action, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool leftPressed;
|
||||||
|
private bool rightPressed;
|
||||||
|
|
||||||
|
private void handleInput(OsuAction action, bool pressed)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case OsuAction.LeftButton:
|
||||||
|
leftPressed = pressed;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OsuAction.RightButton:
|
||||||
|
rightPressed = pressed;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftPressed && rightPressed)
|
||||||
|
breakSpewer.Direction = SpewDirection.Omni;
|
||||||
|
else if (leftPressed)
|
||||||
|
breakSpewer.Direction = SpewDirection.Left;
|
||||||
|
else if (rightPressed)
|
||||||
|
breakSpewer.Direction = SpewDirection.Right;
|
||||||
|
else
|
||||||
|
breakSpewer.Direction = SpewDirection.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LegacyCursorParticleSpewer : ParticleSpewer, IRequireHighFrequencyMousePosition
|
||||||
|
{
|
||||||
|
private const int particle_duration_min = 300;
|
||||||
|
private const int particle_duration_max = 1000;
|
||||||
|
|
||||||
|
public SpewDirection Direction { get; set; }
|
||||||
|
|
||||||
|
protected override bool CanSpawnParticles => base.CanSpawnParticles && cursorScreenPosition.HasValue;
|
||||||
|
protected override float ParticleGravity => 240;
|
||||||
|
|
||||||
|
public LegacyCursorParticleSpewer(Texture texture, int perSecond)
|
||||||
|
: base(texture, perSecond, particle_duration_max)
|
||||||
|
{
|
||||||
|
Active.BindValueChanged(_ => resetVelocityCalculation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2? cursorScreenPosition;
|
||||||
|
private Vector2 cursorVelocity;
|
||||||
|
|
||||||
|
private const double max_velocity_frame_length = 15;
|
||||||
|
private double velocityFrameLength;
|
||||||
|
private Vector2 totalPosDifference;
|
||||||
|
|
||||||
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
|
||||||
|
|
||||||
|
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||||
|
{
|
||||||
|
if (cursorScreenPosition == null)
|
||||||
|
{
|
||||||
|
cursorScreenPosition = e.ScreenSpaceMousePosition;
|
||||||
|
return base.OnMouseMove(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate cursor velocity.
|
||||||
|
totalPosDifference += e.ScreenSpaceMousePosition - cursorScreenPosition.Value;
|
||||||
|
cursorScreenPosition = e.ScreenSpaceMousePosition;
|
||||||
|
|
||||||
|
velocityFrameLength += Math.Abs(Clock.ElapsedFrameTime);
|
||||||
|
|
||||||
|
if (velocityFrameLength > max_velocity_frame_length)
|
||||||
|
{
|
||||||
|
cursorVelocity = totalPosDifference / (float)velocityFrameLength;
|
||||||
|
|
||||||
|
totalPosDifference = Vector2.Zero;
|
||||||
|
velocityFrameLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnMouseMove(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetVelocityCalculation()
|
||||||
|
{
|
||||||
|
cursorScreenPosition = null;
|
||||||
|
totalPosDifference = Vector2.Zero;
|
||||||
|
velocityFrameLength = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override FallingParticle CreateParticle() =>
|
||||||
|
new FallingParticle
|
||||||
|
{
|
||||||
|
StartPosition = ToLocalSpace(cursorScreenPosition ?? Vector2.Zero),
|
||||||
|
Duration = RNG.NextSingle(particle_duration_min, particle_duration_max),
|
||||||
|
StartAngle = (float)(RNG.NextDouble() * 4 - 2),
|
||||||
|
EndAngle = RNG.NextSingle(-2f, 2f),
|
||||||
|
EndScale = RNG.NextSingle(2f),
|
||||||
|
Velocity = getVelocity(),
|
||||||
|
};
|
||||||
|
|
||||||
|
private Vector2 getVelocity()
|
||||||
|
{
|
||||||
|
Vector2 velocity = Vector2.Zero;
|
||||||
|
|
||||||
|
switch (Direction)
|
||||||
|
{
|
||||||
|
case SpewDirection.Left:
|
||||||
|
velocity = new Vector2(
|
||||||
|
RNG.NextSingle(-460f, 0),
|
||||||
|
RNG.NextSingle(-40f, 40f)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpewDirection.Right:
|
||||||
|
velocity = new Vector2(
|
||||||
|
RNG.NextSingle(0, 460f),
|
||||||
|
RNG.NextSingle(-40f, 40f)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SpewDirection.Omni:
|
||||||
|
velocity = new Vector2(
|
||||||
|
RNG.NextSingle(-460f, 460f),
|
||||||
|
RNG.NextSingle(-160f, 160f)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
velocity += cursorVelocity * 40;
|
||||||
|
|
||||||
|
return velocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SpewDirection
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Omni,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Container circleSprites;
|
|
||||||
private Drawable hitCircleSprite;
|
private Drawable hitCircleSprite;
|
||||||
private Drawable hitCircleOverlay;
|
|
||||||
|
protected Drawable HitCircleOverlay { get; private set; }
|
||||||
|
|
||||||
private SkinnableSpriteText hitCircleText;
|
private SkinnableSpriteText hitCircleText;
|
||||||
|
|
||||||
@ -70,14 +70,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
|
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
|
||||||
Texture overlayTexture = getTextureWithFallback("overlay");
|
Texture overlayTexture = getTextureWithFallback("overlay");
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new[]
|
||||||
{
|
|
||||||
circleSprites = new Container
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Children = new[]
|
|
||||||
{
|
{
|
||||||
hitCircleSprite = new KiaiFlashingSprite
|
hitCircleSprite = new KiaiFlashingSprite
|
||||||
{
|
{
|
||||||
@ -85,13 +78,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
},
|
},
|
||||||
hitCircleOverlay = new KiaiFlashingSprite
|
HitCircleOverlay = new KiaiFlashingSprite
|
||||||
{
|
{
|
||||||
Texture = overlayTexture,
|
Texture = overlayTexture,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
||||||
|
|
||||||
if (overlayAboveNumber)
|
if (overlayAboveNumber)
|
||||||
AddInternal(hitCircleOverlay.CreateProxy());
|
ChangeInternalChildDepth(HitCircleOverlay, float.MinValue);
|
||||||
|
|
||||||
accentColour.BindTo(drawableObject.AccentColour);
|
accentColour.BindTo(drawableObject.AccentColour);
|
||||||
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
|
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
|
||||||
@ -153,8 +144,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
switch (state)
|
switch (state)
|
||||||
{
|
{
|
||||||
case ArmedState.Hit:
|
case ArmedState.Hit:
|
||||||
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
|
hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
|
||||||
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||||
|
|
||||||
|
HitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out);
|
||||||
|
HitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
|
||||||
|
|
||||||
if (hasNumber)
|
if (hasNumber)
|
||||||
{
|
{
|
||||||
|
67
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
Normal file
67
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
|
{
|
||||||
|
public class LegacyReverseArrow : CompositeDrawable
|
||||||
|
{
|
||||||
|
private ISkin skin { get; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private DrawableHitObject drawableHitObject { get; set; }
|
||||||
|
|
||||||
|
private Drawable proxy;
|
||||||
|
|
||||||
|
public LegacyReverseArrow(ISkin skin)
|
||||||
|
{
|
||||||
|
this.skin = skin;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
|
||||||
|
string lookupName = new OsuSkinComponent(OsuSkinComponents.ReverseArrow).LookupName;
|
||||||
|
|
||||||
|
InternalChild = skin.GetAnimation(lookupName, true, true) ?? Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
proxy = CreateProxy();
|
||||||
|
|
||||||
|
if (drawableHitObject != null)
|
||||||
|
{
|
||||||
|
drawableHitObject.HitObjectApplied += onHitObjectApplied;
|
||||||
|
onHitObjectApplied(drawableHitObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
||||||
|
{
|
||||||
|
Debug.Assert(proxy.Parent == null);
|
||||||
|
|
||||||
|
// see logic in LegacySliderHeadHitCircle.
|
||||||
|
(drawableObject as DrawableSliderRepeat)?.DrawableSlider
|
||||||
|
.OverlayElementContainer.Add(proxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
if (drawableHitObject != null)
|
||||||
|
drawableHitObject.HitObjectApplied -= onHitObjectApplied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
// 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.Diagnostics;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||||
|
{
|
||||||
|
public class LegacySliderHeadHitCircle : LegacyMainCirclePiece
|
||||||
|
{
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private DrawableHitObject drawableHitObject { get; set; }
|
||||||
|
|
||||||
|
private Drawable proxiedHitCircleOverlay;
|
||||||
|
|
||||||
|
public LegacySliderHeadHitCircle()
|
||||||
|
: base("sliderstartcircle")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
proxiedHitCircleOverlay = HitCircleOverlay.CreateProxy();
|
||||||
|
|
||||||
|
if (drawableHitObject != null)
|
||||||
|
{
|
||||||
|
drawableHitObject.HitObjectApplied += onHitObjectApplied;
|
||||||
|
onHitObjectApplied(drawableHitObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onHitObjectApplied(DrawableHitObject drawableObject)
|
||||||
|
{
|
||||||
|
Debug.Assert(proxiedHitCircleOverlay.Parent == null);
|
||||||
|
|
||||||
|
// see logic in LegacyReverseArrow.
|
||||||
|
(drawableObject as DrawableSliderHead)?.DrawableSlider
|
||||||
|
.OverlayElementContainer.Add(proxiedHitCircleOverlay.With(d => d.Depth = float.MinValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (drawableHitObject != null)
|
||||||
|
drawableHitObject.HitObjectApplied -= onHitObjectApplied;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -67,7 +67,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
case OsuSkinComponents.SliderHeadHitCircle:
|
case OsuSkinComponents.SliderHeadHitCircle:
|
||||||
if (hasHitCircle.Value)
|
if (hasHitCircle.Value)
|
||||||
return new LegacyMainCirclePiece("sliderstartcircle");
|
return new LegacySliderHeadHitCircle();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.ReverseArrow:
|
||||||
|
if (hasHitCircle.Value)
|
||||||
|
return new LegacyReverseArrow(this);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@ -89,6 +95,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
case OsuSkinComponents.CursorParticles:
|
||||||
|
if (GetTexture("star2") != null)
|
||||||
|
return new LegacyCursorParticles();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
|
||||||
case OsuSkinComponents.HitCircleText:
|
case OsuSkinComponents.HitCircleText:
|
||||||
if (!this.HasFont(LegacyFont.HitCircle))
|
if (!this.HasFont(LegacyFont.HitCircle))
|
||||||
return null;
|
return null;
|
||||||
|
@ -9,5 +9,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
SliderBorder,
|
SliderBorder,
|
||||||
SliderBall,
|
SliderBall,
|
||||||
SpinnerBackground,
|
SpinnerBackground,
|
||||||
|
StarBreakAdditive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
InternalChild = fadeContainer = new Container
|
InternalChild = fadeContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = cursorTrail = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling)
|
Children = new[]
|
||||||
|
{
|
||||||
|
cursorTrail = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorTrail), _ => new DefaultCursorTrail(), confineMode: ConfineMode.NoScaling),
|
||||||
|
new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorParticles), confineMode: ConfineMode.NoScaling),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.UI
|
namespace osu.Game.Rulesets.Osu.UI
|
||||||
{
|
{
|
||||||
|
[Cached]
|
||||||
public class OsuPlayfield : Playfield
|
public class OsuPlayfield : Playfield
|
||||||
{
|
{
|
||||||
private readonly PlayfieldBorder playfieldBorder;
|
private readonly PlayfieldBorder playfieldBorder;
|
||||||
|
@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
@ -137,6 +138,23 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
|
AddAssert("no circle added", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClear()
|
||||||
|
{
|
||||||
|
AddStep("OD 1", () => recreateDisplay(new OsuHitWindows(), 1));
|
||||||
|
|
||||||
|
AddStep("hit", () => newJudgement(0.2D));
|
||||||
|
AddAssert("bar added", () => this.ChildrenOfType<BarHitErrorMeter>().All(
|
||||||
|
meter => meter.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Count() == 1));
|
||||||
|
AddAssert("circle added", () => this.ChildrenOfType<ColourHitErrorMeter>().All(
|
||||||
|
meter => meter.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Count() == 1));
|
||||||
|
|
||||||
|
AddStep("clear", () => this.ChildrenOfType<HitErrorMeter>().ForEach(meter => meter.Clear()));
|
||||||
|
|
||||||
|
AddAssert("bar cleared", () => !this.ChildrenOfType<BarHitErrorMeter.JudgementLine>().Any());
|
||||||
|
AddAssert("colour cleared", () => !this.ChildrenOfType<ColourHitErrorMeter.HitErrorCircle>().Any());
|
||||||
|
}
|
||||||
|
|
||||||
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
|
private void recreateDisplay(HitWindows hitWindows, float overallDifficulty)
|
||||||
{
|
{
|
||||||
hitWindows?.SetDifficulty(overallDifficulty);
|
hitWindows?.SetDifficulty(overallDifficulty);
|
||||||
|
128
osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs
Normal file
128
osu.Game.Tests/Visual/Gameplay/TestSceneParticleSpewer.cs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class TestSceneParticleSpewer : OsuTestScene
|
||||||
|
{
|
||||||
|
private TestParticleSpewer spewer;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private SkinManager skinManager { get; set; }
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Child = spewer = createSpewer();
|
||||||
|
|
||||||
|
AddToggleStep("toggle spawning", value => spewer.Active.Value = value);
|
||||||
|
AddSliderStep("particle gravity", 0f, 1f, 0f, value => spewer.Gravity = value);
|
||||||
|
AddSliderStep("particle velocity", 0f, 1f, 0.5f, value => spewer.MaxVelocity = value);
|
||||||
|
AddStep("move to new location", () =>
|
||||||
|
{
|
||||||
|
spewer.TransformTo(nameof(spewer.SpawnPosition), new Vector2(RNG.NextSingle(), RNG.NextSingle()), 1000, Easing.Out);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUpSteps]
|
||||||
|
public void SetUpSteps()
|
||||||
|
{
|
||||||
|
AddStep("create spewer", () => Child = spewer = createSpewer());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPresence()
|
||||||
|
{
|
||||||
|
AddStep("start spewer", () => spewer.Active.Value = true);
|
||||||
|
AddAssert("is present", () => spewer.IsPresent);
|
||||||
|
|
||||||
|
AddWaitStep("wait for some particles", 3);
|
||||||
|
AddStep("stop spewer", () => spewer.Active.Value = false);
|
||||||
|
|
||||||
|
AddWaitStep("wait for clean screen", 8);
|
||||||
|
AddAssert("is not present", () => !spewer.IsPresent);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTimeJumps()
|
||||||
|
{
|
||||||
|
ManualClock testClock = new ManualClock();
|
||||||
|
|
||||||
|
AddStep("prepare clock", () =>
|
||||||
|
{
|
||||||
|
testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -3;
|
||||||
|
spewer.Clock = new FramedClock(testClock);
|
||||||
|
});
|
||||||
|
AddStep("start spewer", () => spewer.Active.Value = true);
|
||||||
|
AddAssert("spawned first particle", () => spewer.TotalCreatedParticles == 1);
|
||||||
|
|
||||||
|
AddStep("move clock forward", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * 3);
|
||||||
|
AddAssert("spawned second particle", () => spewer.TotalCreatedParticles == 2);
|
||||||
|
|
||||||
|
AddStep("move clock backwards", () => testClock.CurrentTime = TestParticleSpewer.MAX_DURATION * -1);
|
||||||
|
AddAssert("spawned third particle", () => spewer.TotalCreatedParticles == 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TestParticleSpewer createSpewer() =>
|
||||||
|
new TestParticleSpewer(skinManager.DefaultLegacySkin.GetTexture("star2"))
|
||||||
|
{
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
RelativePositionAxes = Axes.Both,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Position = new Vector2(0.5f),
|
||||||
|
Size = new Vector2(0.5f),
|
||||||
|
};
|
||||||
|
|
||||||
|
private class TestParticleSpewer : ParticleSpewer
|
||||||
|
{
|
||||||
|
public const int MAX_DURATION = 1500;
|
||||||
|
private const int rate = 250;
|
||||||
|
|
||||||
|
public int TotalCreatedParticles { get; private set; }
|
||||||
|
|
||||||
|
public float Gravity;
|
||||||
|
|
||||||
|
public float MaxVelocity = 0.25f;
|
||||||
|
|
||||||
|
public Vector2 SpawnPosition { get; set; } = new Vector2(0.5f);
|
||||||
|
|
||||||
|
protected override float ParticleGravity => Gravity;
|
||||||
|
|
||||||
|
public TestParticleSpewer(Texture texture)
|
||||||
|
: base(texture, rate, MAX_DURATION)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override FallingParticle CreateParticle()
|
||||||
|
{
|
||||||
|
TotalCreatedParticles++;
|
||||||
|
|
||||||
|
return new FallingParticle
|
||||||
|
{
|
||||||
|
Velocity = new Vector2(
|
||||||
|
RNG.NextSingle(-MaxVelocity, MaxVelocity),
|
||||||
|
RNG.NextSingle(-MaxVelocity, MaxVelocity)
|
||||||
|
),
|
||||||
|
StartPosition = SpawnPosition,
|
||||||
|
Duration = RNG.NextSingle(MAX_DURATION),
|
||||||
|
StartAngle = RNG.NextSingle(MathF.PI * 2),
|
||||||
|
EndAngle = RNG.NextSingle(MathF.PI * 2),
|
||||||
|
EndScale = RNG.NextSingle(0.5f, 1.5f)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -115,6 +115,8 @@ namespace osu.Game.Graphics
|
|||||||
null, TextureCoords);
|
null, TextureCoords);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool CanDrawOpaqueInterior => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly struct ParticlePart
|
private readonly struct ParticlePart
|
||||||
|
200
osu.Game/Graphics/ParticleSpewer.cs
Normal file
200
osu.Game/Graphics/ParticleSpewer.cs
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
// 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 osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.EnumExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.OpenGL.Vertices;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Graphics
|
||||||
|
{
|
||||||
|
public abstract class ParticleSpewer : Sprite
|
||||||
|
{
|
||||||
|
private readonly FallingParticle[] particles;
|
||||||
|
private int currentIndex;
|
||||||
|
private double lastParticleAdded;
|
||||||
|
|
||||||
|
private readonly double cooldown;
|
||||||
|
private readonly double maxDuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether particles are being spawned.
|
||||||
|
/// </summary>
|
||||||
|
public readonly BindableBool Active = new BindableBool();
|
||||||
|
|
||||||
|
public override bool IsPresent => base.IsPresent && hasActiveParticles;
|
||||||
|
|
||||||
|
protected virtual bool CanSpawnParticles => true;
|
||||||
|
protected virtual float ParticleGravity => 0;
|
||||||
|
|
||||||
|
private bool hasActiveParticles => Active.Value || (lastParticleAdded + maxDuration) > Time.Current;
|
||||||
|
|
||||||
|
protected ParticleSpewer(Texture texture, int perSecond, double maxDuration)
|
||||||
|
{
|
||||||
|
Texture = texture;
|
||||||
|
Blending = BlendingParameters.Additive;
|
||||||
|
|
||||||
|
particles = new FallingParticle[perSecond * (int)Math.Ceiling(maxDuration / 1000)];
|
||||||
|
|
||||||
|
cooldown = 1000f / perSecond;
|
||||||
|
this.maxDuration = maxDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (Active.Value && CanSpawnParticles && Math.Abs(Time.Current - lastParticleAdded) > cooldown)
|
||||||
|
{
|
||||||
|
var newParticle = CreateParticle();
|
||||||
|
newParticle.StartTime = (float)Time.Current;
|
||||||
|
|
||||||
|
particles[currentIndex] = newParticle;
|
||||||
|
|
||||||
|
currentIndex = (currentIndex + 1) % particles.Length;
|
||||||
|
lastParticleAdded = Time.Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
Invalidate(Invalidation.DrawNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called each time a new particle should be spawned.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual FallingParticle CreateParticle() => new FallingParticle();
|
||||||
|
|
||||||
|
protected override DrawNode CreateDrawNode() => new ParticleSpewerDrawNode(this);
|
||||||
|
|
||||||
|
# region DrawNode
|
||||||
|
|
||||||
|
private class ParticleSpewerDrawNode : SpriteDrawNode
|
||||||
|
{
|
||||||
|
private readonly FallingParticle[] particles;
|
||||||
|
|
||||||
|
protected new ParticleSpewer Source => (ParticleSpewer)base.Source;
|
||||||
|
|
||||||
|
private readonly float maxDuration;
|
||||||
|
|
||||||
|
private float currentTime;
|
||||||
|
private float gravity;
|
||||||
|
private Axes relativePositionAxes;
|
||||||
|
private Vector2 sourceSize;
|
||||||
|
|
||||||
|
public ParticleSpewerDrawNode(ParticleSpewer source)
|
||||||
|
: base(source)
|
||||||
|
{
|
||||||
|
particles = new FallingParticle[Source.particles.Length];
|
||||||
|
maxDuration = (float)Source.maxDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ApplyState()
|
||||||
|
{
|
||||||
|
base.ApplyState();
|
||||||
|
|
||||||
|
Source.particles.CopyTo(particles, 0);
|
||||||
|
|
||||||
|
currentTime = (float)Source.Time.Current;
|
||||||
|
gravity = Source.ParticleGravity;
|
||||||
|
relativePositionAxes = Source.RelativePositionAxes;
|
||||||
|
sourceSize = Source.DrawSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Blit(Action<TexturedVertex2D> vertexAction)
|
||||||
|
{
|
||||||
|
foreach (var p in particles)
|
||||||
|
{
|
||||||
|
var timeSinceStart = currentTime - p.StartTime;
|
||||||
|
|
||||||
|
// ignore particles from the future.
|
||||||
|
// these can appear when seeking in replays.
|
||||||
|
if (timeSinceStart < 0) continue;
|
||||||
|
|
||||||
|
var alpha = p.AlphaAtTime(timeSinceStart);
|
||||||
|
if (alpha <= 0) continue;
|
||||||
|
|
||||||
|
var pos = p.PositionAtTime(timeSinceStart, gravity, maxDuration);
|
||||||
|
var scale = p.ScaleAtTime(timeSinceStart);
|
||||||
|
var angle = p.AngleAtTime(timeSinceStart);
|
||||||
|
|
||||||
|
var rect = createDrawRect(pos, scale);
|
||||||
|
|
||||||
|
var quad = new Quad(
|
||||||
|
transformPosition(rect.TopLeft, rect.Centre, angle),
|
||||||
|
transformPosition(rect.TopRight, rect.Centre, angle),
|
||||||
|
transformPosition(rect.BottomLeft, rect.Centre, angle),
|
||||||
|
transformPosition(rect.BottomRight, rect.Centre, angle)
|
||||||
|
);
|
||||||
|
|
||||||
|
DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction,
|
||||||
|
new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
|
||||||
|
null, TextureCoords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RectangleF createDrawRect(Vector2 position, float scale)
|
||||||
|
{
|
||||||
|
var width = Texture.DisplayWidth * scale;
|
||||||
|
var height = Texture.DisplayHeight * scale;
|
||||||
|
|
||||||
|
if (relativePositionAxes.HasFlagFast(Axes.X))
|
||||||
|
position.X *= sourceSize.X;
|
||||||
|
if (relativePositionAxes.HasFlagFast(Axes.Y))
|
||||||
|
position.Y *= sourceSize.Y;
|
||||||
|
|
||||||
|
return new RectangleF(
|
||||||
|
position.X - width / 2,
|
||||||
|
position.Y - height / 2,
|
||||||
|
width,
|
||||||
|
height);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector2 transformPosition(Vector2 pos, Vector2 centre, float angle)
|
||||||
|
{
|
||||||
|
float cos = MathF.Cos(angle);
|
||||||
|
float sin = MathF.Sin(angle);
|
||||||
|
|
||||||
|
float x = centre.X + (pos.X - centre.X) * cos + (pos.Y - centre.Y) * sin;
|
||||||
|
float y = centre.Y + (pos.Y - centre.Y) * cos - (pos.X - centre.X) * sin;
|
||||||
|
|
||||||
|
return Vector2Extensions.Transform(new Vector2(x, y), DrawInfo.Matrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool CanDrawOpaqueInterior => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
protected struct FallingParticle
|
||||||
|
{
|
||||||
|
public float StartTime;
|
||||||
|
public Vector2 StartPosition;
|
||||||
|
public Vector2 Velocity;
|
||||||
|
public float Duration;
|
||||||
|
public float StartAngle;
|
||||||
|
public float EndAngle;
|
||||||
|
public float EndScale;
|
||||||
|
|
||||||
|
public float AlphaAtTime(float timeSinceStart) => 1 - progressAtTime(timeSinceStart);
|
||||||
|
|
||||||
|
public float ScaleAtTime(float timeSinceStart) => (float)Interpolation.Lerp(1, EndScale, progressAtTime(timeSinceStart));
|
||||||
|
|
||||||
|
public float AngleAtTime(float timeSinceStart) => (float)Interpolation.Lerp(StartAngle, EndAngle, progressAtTime(timeSinceStart));
|
||||||
|
|
||||||
|
public Vector2 PositionAtTime(float timeSinceStart, float gravity, float maxDuration)
|
||||||
|
{
|
||||||
|
var progress = progressAtTime(timeSinceStart);
|
||||||
|
var currentGravity = new Vector2(0, gravity * Duration / maxDuration * progress);
|
||||||
|
|
||||||
|
return StartPosition + (Velocity + currentGravity) * timeSinceStart / maxDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float progressAtTime(float timeSinceStart) => Math.Clamp(timeSinceStart / Duration, 0, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
// 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;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// 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.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Login
|
namespace osu.Game.Overlays.Login
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
// 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.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
@ -189,9 +189,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
private OsuPasswordTextBox passwordTextbox;
|
private OsuPasswordTextBox passwordTextbox;
|
||||||
private TriangleButton joinButton;
|
private TriangleButton joinButton;
|
||||||
private OsuSpriteText errorText;
|
private OsuSpriteText errorText;
|
||||||
|
private Sample sampleJoinFail;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours)
|
private void load(OsuColour colours, AudioManager audio)
|
||||||
{
|
{
|
||||||
Child = new FillFlowContainer
|
Child = new FillFlowContainer
|
||||||
{
|
{
|
||||||
@ -227,6 +228,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sampleJoinFail = audio.Samples.Get(@"UI/password-fail");
|
||||||
|
|
||||||
joinButton.Action = () => lounge?.Join(room, passwordTextbox.Text, null, joinFailed);
|
joinButton.Action = () => lounge?.Join(room, passwordTextbox.Text, null, joinFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,6 +247,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
|
|||||||
.FadeOutFromOne(1000, Easing.In);
|
.FadeOutFromOne(1000, Easing.In);
|
||||||
|
|
||||||
Body.Shake();
|
Body.Shake();
|
||||||
|
|
||||||
|
sampleJoinFail?.Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Screens.Play
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Encapsulates gameplay timing logic and provides a <see cref="GameplayClock"/> via DI for gameplay components to use.
|
/// Encapsulates gameplay timing logic and provides a <see cref="GameplayClock"/> via DI for gameplay components to use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Cached]
|
||||||
public abstract class GameplayClockContainer : Container, IAdjustableClock
|
public abstract class GameplayClockContainer : Container, IAdjustableClock
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -35,6 +36,11 @@ namespace osu.Game.Screens.Play
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected IClock SourceClock { get; private set; }
|
protected IClock SourceClock { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when a seek has been performed via <see cref="Seek"/>
|
||||||
|
/// </summary>
|
||||||
|
public event Action OnSeek;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="GameplayClockContainer"/>.
|
/// Creates a new <see cref="GameplayClockContainer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -88,6 +94,8 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
// Manually process to make sure the gameplay clock is correctly updated after a seek.
|
// Manually process to make sure the gameplay clock is correctly updated after a seek.
|
||||||
GameplayClock.UnderlyingClock.ProcessFrame();
|
GameplayClock.UnderlyingClock.ProcessFrame();
|
||||||
|
|
||||||
|
OnSeek?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -286,5 +286,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
this.FadeTo(0.8f, 150).Then().FadeOut(judgement_fade_duration).Expire();
|
this.FadeTo(0.8f, 150).Then().FadeOut(judgement_fade_duration).Expire();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Clear() => judgementsContainer.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
judgementsFlow.Push(GetColourForHitResult(judgement.Type));
|
judgementsFlow.Push(GetColourForHitResult(judgement.Type));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void Clear() => judgementsFlow.Clear();
|
||||||
|
|
||||||
private class JudgementFlow : FillFlowContainer<HitErrorCircle>
|
private class JudgementFlow : FillFlowContainer<HitErrorCircle>
|
||||||
{
|
{
|
||||||
private const int max_available_judgements = 20;
|
private const int max_available_judgements = 20;
|
||||||
|
@ -22,6 +22,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private GameplayClockContainer gameplayClockContainer { get; set; }
|
||||||
|
|
||||||
public bool UsesFixedAnchor { get; set; }
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
@ -34,6 +37,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
if (gameplayClockContainer != null)
|
||||||
|
gameplayClockContainer.OnSeek += Clear;
|
||||||
|
|
||||||
processor.NewJudgement += OnNewJudgement;
|
processor.NewJudgement += OnNewJudgement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,12 +73,21 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked by <see cref="GameplayClockContainer.OnSeek"/>.
|
||||||
|
/// Any inheritors of <see cref="HitErrorMeter"/> should have this method clear their container that displays the hit error results.
|
||||||
|
/// </summary>
|
||||||
|
public abstract void Clear();
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
if (processor != null)
|
if (processor != null)
|
||||||
processor.NewJudgement -= OnNewJudgement;
|
processor.NewJudgement -= OnNewJudgement;
|
||||||
|
|
||||||
|
if (gameplayClockContainer != null)
|
||||||
|
gameplayClockContainer.OnSeek -= Clear;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,9 +37,9 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.5.0" />
|
<PackageReference Include="Realm" Version="10.5.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2021.916.1" />
|
<PackageReference Include="ppy.osu.Framework" Version="2021.916.1" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.913.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.9.0" />
|
<PackageReference Include="Sentry" Version="3.9.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.28.3" />
|
<PackageReference Include="SharpCompress" Version="0.29.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -71,7 +71,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.916.1" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.916.1" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.913.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.918.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
Loading…
Reference in New Issue
Block a user