mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:17:51 +08:00
Merge branch 'master' into fix-legacy-score-multipliers-2
This commit is contained in:
commit
71c4b138fb
515
.github/workflows/diffcalc.yml
vendored
515
.github/workflows/diffcalc.yml
vendored
@ -1,206 +1,365 @@
|
|||||||
# Listens for new PR comments containing !pp check [id], and runs a diffcalc comparison against master.
|
# ## Description
|
||||||
# Usage:
|
|
||||||
# !pp check 0 | Runs only the osu! ruleset.
|
|
||||||
# !pp check 0 2 | Runs only the osu! and catch rulesets.
|
|
||||||
#
|
#
|
||||||
|
# Uses [diffcalc-sheet-generator](https://github.com/smoogipoo/diffcalc-sheet-generator) to run two builds of osu and generate an SR/PP/Score comparison spreadsheet.
|
||||||
|
#
|
||||||
|
# ## Requirements
|
||||||
|
#
|
||||||
|
# Self-hosted runner with installed:
|
||||||
|
# - `docker >= 20.10.16`
|
||||||
|
# - `docker-compose >= 2.5.1`
|
||||||
|
# - `lbzip2`
|
||||||
|
# - `jq`
|
||||||
|
#
|
||||||
|
# ## Usage
|
||||||
|
#
|
||||||
|
# The workflow can be run in two ways:
|
||||||
|
# 1. Via workflow dispatch.
|
||||||
|
# 2. By an owner of the repository posting a pull request or issue comment containing `!diffcalc`.
|
||||||
|
# For pull requests, the workflow will assume the pull request as the target to compare against (i.e. the `OSU_B` variable).
|
||||||
|
# Any lines in the comment of the form `KEY=VALUE` are treated as variables for the generator.
|
||||||
|
#
|
||||||
|
# ## Google Service Account
|
||||||
|
#
|
||||||
|
# Spreadsheets are uploaded to a Google Service Account, and exposed with read-only permissions to the wider audience.
|
||||||
|
#
|
||||||
|
# 1. Create a project at https://console.cloud.google.com
|
||||||
|
# 2. Enable the `Google Sheets` and `Google Drive` APIs.
|
||||||
|
# 3. Create a Service Account
|
||||||
|
# 4. Generate a key in the JSON format.
|
||||||
|
# 5. Encode the key as base64 and store as an **actions secret** with name **`DIFFCALC_GOOGLE_CREDENTIALS`**
|
||||||
|
#
|
||||||
|
# ## Environment variables
|
||||||
|
#
|
||||||
|
# The default environment may be configured via **actions variables**.
|
||||||
|
#
|
||||||
|
# Refer to [the sample environment](https://github.com/smoogipoo/diffcalc-sheet-generator/blob/master/.env.sample), and prefix each variable with `DIFFCALC_` (e.g. `DIFFCALC_THREADS`, `DIFFCALC_INNODB_BUFFER_SIZE`, etc...).
|
||||||
|
|
||||||
|
name: Run difficulty calculation comparison
|
||||||
|
|
||||||
|
run-name: "${{ github.event_name == 'workflow_dispatch' && format('Manual run: {0}', inputs.osu-b) || 'Automatic comment trigger' }}"
|
||||||
|
|
||||||
name: Difficulty Calculation
|
|
||||||
on:
|
on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [ created ]
|
types: [ created ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
osu-b:
|
||||||
|
description: "The target build of ppy/osu"
|
||||||
|
type: string
|
||||||
|
required: true
|
||||||
|
ruleset:
|
||||||
|
description: "The ruleset to process"
|
||||||
|
type: choice
|
||||||
|
required: true
|
||||||
|
options:
|
||||||
|
- osu
|
||||||
|
- taiko
|
||||||
|
- catch
|
||||||
|
- mania
|
||||||
|
converts:
|
||||||
|
description: "Include converted beatmaps"
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
ranked-only:
|
||||||
|
description: "Only ranked beatmaps"
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
generators:
|
||||||
|
description: "Comma-separated list of generators (available: [sr, pp, score])"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'pp,sr'
|
||||||
|
osu-a:
|
||||||
|
description: "The source build of ppy/osu"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
difficulty-calculator-a:
|
||||||
|
description: "The source build of ppy/osu-difficulty-calculator"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
difficulty-calculator-b:
|
||||||
|
description: "The target build of ppy/osu-difficulty-calculator"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
score-processor-a:
|
||||||
|
description: "The source build of ppy/osu-queue-score-statistics"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
score-processor-b:
|
||||||
|
description: "The target build of ppy/osu-queue-score-statistics"
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: 'latest'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CONCURRENCY: 4
|
COMMENT_TAG: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||||
ALLOW_DOWNLOAD: 1
|
|
||||||
SAVE_DOWNLOADED: 1
|
|
||||||
SKIP_INSERT_ATTRIBUTES: 1
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
metadata:
|
wait-for-queue:
|
||||||
name: Check for requests
|
name: "Wait for previous workflows"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') && github.event.comment.author_association == 'OWNER') }}
|
||||||
|
timeout-minutes: 50400 # 35 days, the maximum for jobs.
|
||||||
|
steps:
|
||||||
|
- uses: ahmadnassri/action-workflow-queue@v1
|
||||||
|
with:
|
||||||
|
timeout: 2147483647 # Around 24 days, maximum supported.
|
||||||
|
delay: 120000 # Poll every 2 minutes. API seems fairly low on this one.
|
||||||
|
|
||||||
|
create-comment:
|
||||||
|
name: Create PR comment
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '!diffcalc') && github.event.comment.author_association == 'OWNER' }}
|
||||||
|
steps:
|
||||||
|
- name: Create comment
|
||||||
|
uses: thollander/actions-comment-pull-request@v2
|
||||||
|
with:
|
||||||
|
comment_tag: ${{ env.COMMENT_TAG }}
|
||||||
|
message: |
|
||||||
|
Difficulty calculation queued -- please wait! (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
|
||||||
|
|
||||||
|
*This comment will update on completion*
|
||||||
|
|
||||||
|
directory:
|
||||||
|
name: Prepare directory
|
||||||
|
needs: wait-for-queue
|
||||||
runs-on: self-hosted
|
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')
|
if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') && github.event.comment.author_association == 'OWNER') }}
|
||||||
outputs:
|
outputs:
|
||||||
matrix: ${{ steps.generate-matrix.outputs.matrix }}
|
GENERATOR_DIR: ${{ steps.set-outputs.outputs.GENERATOR_DIR }}
|
||||||
continue: ${{ steps.generate-matrix.outputs.continue }}
|
GENERATOR_ENV: ${{ steps.set-outputs.outputs.GENERATOR_ENV }}
|
||||||
|
GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }}
|
||||||
steps:
|
steps:
|
||||||
- name: Construct build matrix
|
- name: Checkout
|
||||||
id: generate-matrix
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Checkout diffcalc-sheet-generator
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
path: 'diffcalc-sheet-generator'
|
||||||
|
repository: 'smoogipoo/diffcalc-sheet-generator'
|
||||||
|
|
||||||
|
- name: Set outputs
|
||||||
|
id: set-outputs
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event.comment.body }}" =~ "osu" ]] ; then
|
echo "GENERATOR_DIR=${{ github.workspace }}/diffcalc-sheet-generator" >> "${GITHUB_OUTPUT}"
|
||||||
MATRIX_PROJECTS_JSON+='{ "name": "osu", "id": 0 },'
|
echo "GENERATOR_ENV=${{ github.workspace }}/diffcalc-sheet-generator/.env" >> "${GITHUB_OUTPUT}"
|
||||||
fi
|
echo "GOOGLE_CREDS_FILE=${{ github.workspace }}/diffcalc-sheet-generator/google-credentials.json" >> "${GITHUB_OUTPUT}"
|
||||||
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
|
environment:
|
||||||
MATRIX_JSON="{ \"ruleset\": [ ${MATRIX_PROJECTS_JSON} ] }"
|
name: Setup environment
|
||||||
echo "${MATRIX_JSON}"
|
needs: directory
|
||||||
CONTINUE="yes"
|
|
||||||
else
|
|
||||||
CONTINUE="no"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "continue=${CONTINUE}" >> $GITHUB_OUTPUT
|
|
||||||
echo "matrix=${MATRIX_JSON}" >> $GITHUB_OUTPUT
|
|
||||||
diffcalc:
|
|
||||||
name: Run
|
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
timeout-minutes: 1440
|
if: ${{ !cancelled() && needs.directory.result == 'success' }}
|
||||||
if: needs.metadata.outputs.continue == 'yes'
|
env:
|
||||||
needs: metadata
|
VARS_JSON: ${{ toJSON(vars) }}
|
||||||
strategy:
|
|
||||||
matrix: ${{ fromJson(needs.metadata.outputs.matrix) }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Verify MySQL connection from host
|
- name: Add base environment
|
||||||
run: |
|
run: |
|
||||||
mysql -e "SHOW DATABASES"
|
# Required by diffcalc-sheet-generator
|
||||||
|
cp '${{ github.workspace }}/diffcalc-sheet-generator/.env.sample' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
|
||||||
- name: Drop previous databases
|
# Add Google credentials
|
||||||
run: |
|
echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ needs.directory.outputs.GOOGLE_CREDS_FILE }}"
|
||||||
for db in osu_master osu_pr
|
|
||||||
do
|
# Add repository variables
|
||||||
mysql -e "DROP DATABASE IF EXISTS $db"
|
echo "${VARS_JSON}" | jq -c '. | to_entries | .[]' | while read -r line; do
|
||||||
|
opt=$(jq -r '.key' <<< ${line})
|
||||||
|
val=$(jq -r '.value' <<< ${line})
|
||||||
|
|
||||||
|
if [[ "${opt}" =~ ^DIFFCALC_ ]]; then
|
||||||
|
optNoPrefix=$(echo "${opt}" | cut -d '_' -f2-)
|
||||||
|
sed -i "s;^${optNoPrefix}=.*$;${optNoPrefix}=${val};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Create directory structure
|
- name: Add pull-request environment
|
||||||
|
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
|
||||||
run: |
|
run: |
|
||||||
mkdir -p $GITHUB_WORKSPACE/master/
|
sed -i "s;^OSU_B=.*$;OSU_B=${{ github.event.issue.pull_request.url }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
mkdir -p $GITHUB_WORKSPACE/pr/
|
|
||||||
|
|
||||||
- name: Get upstream branch # https://akaimo.hatenablog.jp/entry/2020/05/16/101251
|
- name: Add comment environment
|
||||||
id: upstreambranch
|
if: ${{ github.event_name == 'issue_comment' }}
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
echo "branchname=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')" >> $GITHUB_OUTPUT
|
# Add comment environment
|
||||||
echo "repo=$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')" >> $GITHUB_OUTPUT
|
echo '${{ github.event.comment.body }}' | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
|
||||||
|
opt=$(echo ${line} | cut -d '=' -f1)
|
||||||
# Checkout osu
|
sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
- name: Checkout osu (master)
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
path: 'master/osu'
|
|
||||||
- name: Checkout osu (pr)
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
path: 'pr/osu'
|
|
||||||
repository: ${{ steps.upstreambranch.outputs.repo }}
|
|
||||||
ref: ${{ steps.upstreambranch.outputs.branchname }}
|
|
||||||
|
|
||||||
- name: Checkout osu-difficulty-calculator (master)
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: ppy/osu-difficulty-calculator
|
|
||||||
path: 'master/osu-difficulty-calculator'
|
|
||||||
- name: Checkout osu-difficulty-calculator (pr)
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: ppy/osu-difficulty-calculator
|
|
||||||
path: 'pr/osu-difficulty-calculator'
|
|
||||||
|
|
||||||
- name: Install .NET 5.0.x
|
|
||||||
uses: actions/setup-dotnet@v3
|
|
||||||
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
|
done
|
||||||
|
|
||||||
- name: Run diffcalc (master)
|
- name: Add dispatch environment
|
||||||
env:
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
DB_NAME: osu_master
|
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE/master/osu-difficulty-calculator/osu.Server.DifficultyCalculator
|
sed -i 's;^OSU_B=.*$;OSU_B=${{ inputs.osu-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
dotnet run -c:Release -- all -m ${{ matrix.ruleset.id }} -ac -c ${{ env.CONCURRENCY }}
|
sed -i 's/^RULESET=.*$/RULESET=${{ inputs.ruleset }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
- name: Run diffcalc (pr)
|
sed -i 's/^GENERATORS=.*$/GENERATORS=${{ inputs.generators }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
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
|
if [[ '${{ inputs.osu-a }}' != 'latest' ]]; then
|
||||||
run: |
|
sed -i 's;^OSU_A=.*$;OSU_A=${{ inputs.osu-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
mysql -e "
|
fi
|
||||||
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
|
if [[ '${{ inputs.difficulty-calculator-a }}' != 'latest' ]]; then
|
||||||
|
sed -i 's;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${{ inputs.difficulty-calculator-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ '${{ inputs.difficulty-calculator-b }}' != 'latest' ]]; then
|
||||||
|
sed -i 's;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${{ inputs.difficulty-calculator-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ '${{ inputs.score-processor-a }}' != 'latest' ]]; then
|
||||||
|
sed -i 's;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${{ inputs.score-processor-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ '${{ inputs.score-processor-b }}' != 'latest' ]]; then
|
||||||
|
sed -i 's;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${{ inputs.score-processor-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ '${{ inputs.converts }}' == 'true' ]]; then
|
||||||
|
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
else
|
||||||
|
sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ '${{ inputs.ranked-only }}' == 'true' ]]; then
|
||||||
|
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
else
|
||||||
|
sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
scores:
|
||||||
|
name: Setup scores
|
||||||
|
needs: [ directory, environment ]
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: ${{ !cancelled() && needs.environment.result == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: Query latest data
|
||||||
|
id: query
|
||||||
|
run: |
|
||||||
|
ruleset=$(cat ${{ needs.directory.outputs.GENERATOR_ENV }} | grep -E '^RULESET=' | cut -d '=' -f2-)
|
||||||
|
performance_data_name=$(curl -s "https://data.ppy.sh/" | grep "performance_${ruleset}_top_1000\b" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||||
|
|
||||||
|
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Restore cache
|
||||||
|
id: restore-cache
|
||||||
|
uses: maxnowack/local-cache@v1
|
||||||
|
with:
|
||||||
|
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||||
|
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||||
|
|
||||||
|
- name: Download
|
||||||
|
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||||
|
|
||||||
|
- name: Extract
|
||||||
|
run: |
|
||||||
|
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||||
|
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
|
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
|
|
||||||
|
beatmaps:
|
||||||
|
name: Setup beatmaps
|
||||||
|
needs: directory
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: ${{ !cancelled() && needs.directory.result == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: Query latest data
|
||||||
|
id: query
|
||||||
|
run: |
|
||||||
|
beatmaps_data_name=$(curl -s "https://data.ppy.sh/" | grep "osu_files" | tail -1 | awk -F "'" '{print $2}' | sed 's/\.tar\.bz2//g')
|
||||||
|
|
||||||
|
echo "TARGET_DIR=${{ needs.directory.outputs.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Restore cache
|
||||||
|
id: restore-cache
|
||||||
|
uses: maxnowack/local-cache@v1
|
||||||
|
with:
|
||||||
|
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
|
||||||
|
key: ${{ steps.query.outputs.DATA_NAME }}
|
||||||
|
|
||||||
|
- name: Download
|
||||||
|
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
wget -q -nc "https://data.ppy.sh/${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||||
|
|
||||||
|
- name: Extract
|
||||||
|
run: |
|
||||||
|
tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_NAME }}.tar.bz2"
|
||||||
|
rm -r "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
|
mv "${{ steps.query.outputs.DATA_NAME }}" "${{ steps.query.outputs.TARGET_DIR }}"
|
||||||
|
|
||||||
|
generator:
|
||||||
|
name: Run generator
|
||||||
|
needs: [ directory, environment, scores, beatmaps ]
|
||||||
|
runs-on: self-hosted
|
||||||
|
timeout-minutes: 720
|
||||||
|
if: ${{ !cancelled() && needs.scores.result == 'success' && needs.beatmaps.result == 'success' }}
|
||||||
|
outputs:
|
||||||
|
TARGET: ${{ steps.run.outputs.TARGET }}
|
||||||
|
SPREADSHEET_LINK: ${{ steps.run.outputs.SPREADSHEET_LINK }}
|
||||||
|
steps:
|
||||||
|
- name: Run
|
||||||
|
id: run
|
||||||
|
run: |
|
||||||
|
# Add the GitHub token. This needs to be done here because it's unique per-job.
|
||||||
|
sed -i 's/^GH_TOKEN=.*$/GH_TOKEN=${{ github.token }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
|
||||||
|
|
||||||
|
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||||
|
docker-compose up --build generator
|
||||||
|
|
||||||
|
link=$(docker-compose logs generator -n 10 | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
|
||||||
|
target=$(cat "${{ needs.directory.outputs.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
|
||||||
|
|
||||||
|
echo "TARGET=${target}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "SPREADSHEET_LINK=${link}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Shutdown
|
||||||
|
if: ${{ always() }}
|
||||||
|
run: |
|
||||||
|
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
- name: Output info
|
||||||
|
if: ${{ success() }}
|
||||||
|
run: |
|
||||||
|
echo "Target: ${{ steps.run.outputs.TARGET }}"
|
||||||
|
echo "Spreadsheet: ${{ steps.run.outputs.SPREADSHEET_LINK }}"
|
||||||
|
|
||||||
|
update-comment:
|
||||||
|
name: Update PR comment
|
||||||
|
needs: [ create-comment, generator ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '!diffcalc') && github.event.comment.author_association == 'OWNER' }}
|
||||||
|
steps:
|
||||||
|
- name: Update comment on success
|
||||||
|
if: ${{ needs.generator.result == 'success' }}
|
||||||
|
uses: thollander/actions-comment-pull-request@v2
|
||||||
|
with:
|
||||||
|
comment_tag: ${{ env.COMMENT_TAG }}
|
||||||
|
mode: upsert
|
||||||
|
create_if_not_exists: false
|
||||||
|
message: |
|
||||||
|
Target: ${{ needs.generator.outputs.TARGET }}
|
||||||
|
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
|
||||||
|
|
||||||
|
- name: Update comment on failure
|
||||||
|
if: ${{ needs.generator.result == 'failure' }}
|
||||||
|
uses: thollander/actions-comment-pull-request@v2
|
||||||
|
with:
|
||||||
|
comment_tag: ${{ env.COMMENT_TAG }}
|
||||||
|
mode: upsert
|
||||||
|
create_if_not_exists: false
|
||||||
|
message: |
|
||||||
|
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.922.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1006.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
|||||||
X = xPositionData?.X ?? 0,
|
X = xPositionData?.X ?? 0,
|
||||||
NewCombo = comboData?.NewCombo ?? false,
|
NewCombo = comboData?.NewCombo ?? false,
|
||||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||||
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0,
|
|
||||||
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
|
||||||
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
|
||||||
}.Yield();
|
}.Yield();
|
||||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
int nodeIndex = 0;
|
int nodeIndex = 0;
|
||||||
SliderEventDescriptor? lastEvent = null;
|
SliderEventDescriptor? lastEvent = null;
|
||||||
|
|
||||||
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
|
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken))
|
||||||
{
|
{
|
||||||
// generate tiny droplets since the last point
|
// generate tiny droplets since the last point
|
||||||
if (lastEvent != null)
|
if (lastEvent != null)
|
||||||
@ -104,8 +104,8 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// this also includes LegacyLastTick and this is used for TinyDroplet generation above.
|
// this also includes LastTick and this is used for TinyDroplet generation above.
|
||||||
// this means that the final segment of TinyDroplets are increasingly mistimed where LegacyLastTickOffset is being applied.
|
// this means that the final segment of TinyDroplets are increasingly mistimed where LastTick is being applied.
|
||||||
lastEvent = e;
|
lastEvent = e;
|
||||||
|
|
||||||
switch (e.Type)
|
switch (e.Type)
|
||||||
@ -162,7 +162,5 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
public double Distance => Path.Distance;
|
public double Distance => Path.Distance;
|
||||||
|
|
||||||
public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = new List<IList<HitSampleInfo>>();
|
||||||
|
|
||||||
public double? LegacyLastTickOffset { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public partial class LegacyBananaPiece : LegacyCatchHitObjectPiece
|
public partial class LegacyBananaPiece : LegacyCatchHitObjectPiece
|
||||||
{
|
{
|
||||||
private static readonly Vector2 banana_max_size = new Vector2(128);
|
private static readonly Vector2 banana_max_size = new Vector2(160);
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece
|
public partial class LegacyDropletPiece : LegacyCatchHitObjectPiece
|
||||||
{
|
{
|
||||||
private static readonly Vector2 droplet_max_size = new Vector2(82, 103);
|
private static readonly Vector2 droplet_max_size = new Vector2(160);
|
||||||
|
|
||||||
public LegacyDropletPiece()
|
public LegacyDropletPiece()
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
internal partial class LegacyFruitPiece : LegacyCatchHitObjectPiece
|
internal partial class LegacyFruitPiece : LegacyCatchHitObjectPiece
|
||||||
{
|
{
|
||||||
private static readonly Vector2 fruit_max_size = new Vector2(128);
|
private static readonly Vector2 fruit_max_size = new Vector2(160);
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
|||||||
{
|
{
|
||||||
public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset, ISupportConstantAlgorithmToggle
|
public partial class DrawableManiaEditorRuleset : DrawableManiaRuleset, ISupportConstantAlgorithmToggle
|
||||||
{
|
{
|
||||||
public BindableBool ShowSpeedChanges { get; set; } = new BindableBool();
|
public BindableBool ShowSpeedChanges { get; } = new BindableBool();
|
||||||
|
|
||||||
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
|
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
|
||||||
|
|
||||||
|
@ -163,7 +163,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
|||||||
slider = new Slider
|
slider = new Slider
|
||||||
{
|
{
|
||||||
Position = new Vector2(0, 50),
|
Position = new Vector2(0, 50),
|
||||||
LegacyLastTickOffset = 36, // This is necessary for undo to retain the sample control point
|
|
||||||
Path = new SliderPath(new[]
|
Path = new SliderPath(new[]
|
||||||
{
|
{
|
||||||
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
new PathControlPoint(Vector2.Zero, PathType.PerfectCurve),
|
||||||
|
@ -44,7 +44,6 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
|
|||||||
Position = positionData?.Position ?? Vector2.Zero,
|
Position = positionData?.Position ?? Vector2.Zero,
|
||||||
NewCombo = comboData?.NewCombo ?? false,
|
NewCombo = comboData?.NewCombo ?? false,
|
||||||
ComboOffset = comboData?.ComboOffset ?? 0,
|
ComboOffset = comboData?.ComboOffset ?? 0,
|
||||||
LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset,
|
|
||||||
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
|
||||||
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
|
||||||
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
|
||||||
|
@ -315,7 +315,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
|||||||
StartTime = HitObject.StartTime,
|
StartTime = HitObject.StartTime,
|
||||||
Position = HitObject.Position + splitControlPoints[0].Position,
|
Position = HitObject.Position + splitControlPoints[0].Position,
|
||||||
NewCombo = HitObject.NewCombo,
|
NewCombo = HitObject.NewCombo,
|
||||||
LegacyLastTickOffset = HitObject.LegacyLastTickOffset,
|
|
||||||
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
|
Samples = HitObject.Samples.Select(s => s.With()).ToList(),
|
||||||
RepeatCount = HitObject.RepeatCount,
|
RepeatCount = HitObject.RepeatCount,
|
||||||
NodeSamples = HitObject.NodeSamples.Select(n => (IList<HitSampleInfo>)n.Select(s => s.With()).ToList()).ToList(),
|
NodeSamples = HitObject.NodeSamples.Select(n => (IList<HitSampleInfo>)n.Select(s => s.With()).ToList()).ToList(),
|
||||||
|
@ -96,14 +96,13 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
Position = original.Position;
|
Position = original.Position;
|
||||||
NewCombo = original.NewCombo;
|
NewCombo = original.NewCombo;
|
||||||
ComboOffset = original.ComboOffset;
|
ComboOffset = original.ComboOffset;
|
||||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
|
||||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||||
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
SliderVelocityMultiplier = original.SliderVelocityMultiplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
|
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken);
|
||||||
|
|
||||||
foreach (var e in sliderEvents)
|
foreach (var e in sliderEvents)
|
||||||
{
|
{
|
||||||
@ -130,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SliderEventType.LegacyLastTick:
|
case SliderEventType.LastTick:
|
||||||
AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
|
AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
|
||||||
{
|
{
|
||||||
RepeatIndex = e.SpanIndex,
|
RepeatIndex = e.SpanIndex,
|
||||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
|
|
||||||
d.HitObjectApplied += _ =>
|
d.HitObjectApplied += _ =>
|
||||||
{
|
{
|
||||||
// slider tails are a painful edge case, as their start time is offset 36ms back (see `LegacyLastTick`).
|
// slider tails are a painful edge case, as their start time is offset 36ms back (see `LastTick`).
|
||||||
// to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap.
|
// to work around this, look up the slider tail's parenting slider's end time instead to ensure proper snap.
|
||||||
double snapTime = d is DrawableSliderTail tail
|
double snapTime = d is DrawableSliderTail tail
|
||||||
? tail.Slider.GetEndTime()
|
? tail.Slider.GetEndTime()
|
||||||
|
@ -296,7 +296,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
public override void PlaySamples()
|
public override void PlaySamples()
|
||||||
{
|
{
|
||||||
// rather than doing it this way, we should probably attach the sample to the tail circle.
|
// rather than doing it this way, we should probably attach the sample to the tail circle.
|
||||||
// this can only be done after we stop using LegacyLastTick.
|
// this can only be done if we stop using LastTick.
|
||||||
if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit)
|
if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit)
|
||||||
base.PlaySamples();
|
base.PlaySamples();
|
||||||
}
|
}
|
||||||
|
@ -71,8 +71,6 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public double? LegacyLastTickOffset { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The position of the cursor at the point of completion of this <see cref="Slider"/> if it was hit
|
/// The position of the cursor at the point of completion of this <see cref="Slider"/> if it was hit
|
||||||
/// with as few movements as possible. This is set and used by difficulty calculation.
|
/// with as few movements as possible. This is set and used by difficulty calculation.
|
||||||
@ -179,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
base.CreateNestedHitObjects(cancellationToken);
|
base.CreateNestedHitObjects(cancellationToken);
|
||||||
|
|
||||||
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
|
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), cancellationToken);
|
||||||
|
|
||||||
foreach (var e in sliderEvents)
|
foreach (var e in sliderEvents)
|
||||||
{
|
{
|
||||||
@ -206,10 +204,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SliderEventType.LegacyLastTick:
|
case SliderEventType.LastTick:
|
||||||
// we need to use the LegacyLastTick here for compatibility reasons (difficulty).
|
// Of note, we are directly mapping LastTick (instead of `SliderEventType.Tail`) to SliderTailCircle.
|
||||||
// it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay.
|
// It is required as difficulty calculation and gameplay relies on reading this value.
|
||||||
// if this is to change, we should revisit this.
|
// (although it is displayed in classic skins, which may be a concern).
|
||||||
|
// If this is to change, we should revisit this.
|
||||||
AddNested(TailCircle = new SliderTailCircle(this)
|
AddNested(TailCircle = new SliderTailCircle(this)
|
||||||
{
|
{
|
||||||
RepeatIndex = e.SpanIndex,
|
RepeatIndex = e.SpanIndex,
|
||||||
@ -264,7 +263,9 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
if (HeadCircle != null)
|
if (HeadCircle != null)
|
||||||
HeadCircle.Samples = this.GetNodeSamples(0);
|
HeadCircle.Samples = this.GetNodeSamples(0);
|
||||||
|
|
||||||
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
|
// The samples should be attached to the slider tail, however this can only be done if LastTick is removed otherwise they would play earlier than they're intended to.
|
||||||
|
// (see mapping logic in `CreateNestedHitObjects` above)
|
||||||
|
//
|
||||||
// For now, the samples are played by the slider itself at the correct end time.
|
// For now, the samples are played by the slider itself at the correct end time.
|
||||||
TailSamples = this.GetNodeSamples(repeatCount + 1);
|
TailSamples = this.GetNodeSamples(repeatCount + 1);
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Note that this should not be used for timing correctness.
|
/// Note that this should not be used for timing correctness.
|
||||||
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
|
/// See <see cref="SliderEventType.LastTick"/> usage in <see cref="Slider"/> for more information.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SliderTailCircle : SliderEndCircle
|
public class SliderTailCircle : SliderEndCircle
|
||||||
{
|
{
|
||||||
|
@ -70,8 +70,11 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
|
|
||||||
double secondsDuration = Duration / 1000;
|
double secondsDuration = Duration / 1000;
|
||||||
|
|
||||||
SpinsRequired = (int)(minRps * secondsDuration);
|
// Allow a 0.1ms floating point precision error in the calculation of the duration.
|
||||||
MaximumBonusSpins = Math.Max(0, (int)(maxRps * secondsDuration) - SpinsRequired - bonus_spins_gap);
|
const double duration_error = 0.0001;
|
||||||
|
|
||||||
|
SpinsRequired = (int)(minRps * secondsDuration + duration_error);
|
||||||
|
MaximumBonusSpins = Math.Max(0, (int)(maxRps * secondsDuration + duration_error) - SpinsRequired - bonus_spins_gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS) })
|
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2) })
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: OsuHitObject.OBJECT_DIMENSIONS))
|
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d, maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2))
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
|
var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null);
|
||||||
|
|
||||||
InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true, maxSize: OsuHitObject.OBJECT_DIMENSIONS) ?? Empty()).With(d =>
|
InternalChild = arrow = (skin?.GetAnimation(lookupName, true, true, maxSize: OsuHitObject.OBJECT_DIMENSIONS * 2) ?? Empty()).With(d =>
|
||||||
{
|
{
|
||||||
d.Anchor = Anchor.Centre;
|
d.Anchor = Anchor.Centre;
|
||||||
d.Origin = Anchor.Centre;
|
d.Origin = Anchor.Centre;
|
||||||
|
@ -7,7 +7,6 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -47,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS),
|
Texture = skin.GetTexture("sliderb-nd")?.WithMaximumSize(OsuLegacySkinTransformer.MAX_FOLLOW_CIRCLE_AREA_SIZE),
|
||||||
Colour = new Color4(5, 5, 5, 255),
|
Colour = new Color4(5, 5, 5, 255),
|
||||||
},
|
},
|
||||||
LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
|
LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d =>
|
||||||
@ -59,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS),
|
Texture = skin.GetTexture("sliderb-spec")?.WithMaximumSize(OsuLegacySkinTransformer.MAX_FOLLOW_CIRCLE_AREA_SIZE),
|
||||||
Blending = BlendingParameters.Additive,
|
Blending = BlendingParameters.Additive,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public const float LEGACY_CIRCLE_RADIUS = OsuHitObject.OBJECT_RADIUS - 5;
|
public const float LEGACY_CIRCLE_RADIUS = OsuHitObject.OBJECT_RADIUS - 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum allowed size of sprites that reside in the follow circle area of a slider.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The reason this is extracted out to a constant, rather than be inlined in the follow circle sprite retrieval,
|
||||||
|
/// is that some skins will use `sliderb` elements to emulate a slider follow circle with slightly different visual effects applied
|
||||||
|
/// (`sliderb` is always shown and doesn't pulsate; `sliderfollowcircle` isn't always shown and pulsates).
|
||||||
|
/// </remarks>
|
||||||
|
public static readonly Vector2 MAX_FOLLOW_CIRCLE_AREA_SIZE = OsuHitObject.OBJECT_DIMENSIONS * 3;
|
||||||
|
|
||||||
public OsuLegacySkinTransformer(ISkin skin)
|
public OsuLegacySkinTransformer(ISkin skin)
|
||||||
: base(skin)
|
: base(skin)
|
||||||
{
|
{
|
||||||
@ -42,14 +52,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
return this.GetAnimation("sliderscorepoint", false, false);
|
return this.GetAnimation("sliderscorepoint", false, false);
|
||||||
|
|
||||||
case OsuSkinComponents.SliderFollowCircle:
|
case OsuSkinComponents.SliderFollowCircle:
|
||||||
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: new Vector2(308f));
|
var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
|
||||||
if (followCircleContent != null)
|
if (followCircleContent != null)
|
||||||
return new LegacyFollowCircle(followCircleContent);
|
return new LegacyFollowCircle(followCircleContent);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
case OsuSkinComponents.SliderBall:
|
case OsuSkinComponents.SliderBall:
|
||||||
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "", maxSize: OsuHitObject.OBJECT_DIMENSIONS);
|
var sliderBallContent = this.GetAnimation("sliderb", true, true, animationSeparator: "", maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
|
||||||
|
|
||||||
// todo: slider ball has a custom frame delay based on velocity
|
// todo: slider ball has a custom frame delay based on velocity
|
||||||
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
|
// Math.Max((150 / Velocity) * GameBase.SIXTY_FRAME_TIME, GameBase.SIXTY_FRAME_TIME);
|
||||||
@ -139,10 +149,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
if (!this.HasFont(LegacyFont.HitCircle))
|
if (!this.HasFont(LegacyFont.HitCircle))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return new LegacySpriteText(LegacyFont.HitCircle, OsuHitObject.OBJECT_DIMENSIONS)
|
const float hitcircle_text_scale = 0.8f;
|
||||||
|
return new LegacySpriteText(LegacyFont.HitCircle, OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale)
|
||||||
{
|
{
|
||||||
// stable applies a blanket 0.8x scale to hitcircle fonts
|
// stable applies a blanket 0.8x scale to hitcircle fonts
|
||||||
Scale = new Vector2(0.8f),
|
Scale = new Vector2(hitcircle_text_scale),
|
||||||
};
|
};
|
||||||
|
|
||||||
case OsuSkinComponents.SpinnerBody:
|
case OsuSkinComponents.SpinnerBody:
|
||||||
|
@ -115,6 +115,48 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
|
|||||||
AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
|
AddAssert("all tick offsets are 0", () => JudgementResults.Where(r => r.HitObject is SwellTick).All(r => r.TimeOffset == 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAtMostOneSwellTickJudgedPerFrame()
|
||||||
|
{
|
||||||
|
const double swell_time = 1000;
|
||||||
|
|
||||||
|
Swell swell = new Swell
|
||||||
|
{
|
||||||
|
StartTime = swell_time,
|
||||||
|
Duration = 1000,
|
||||||
|
RequiredHits = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
List<ReplayFrame> frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new TaikoReplayFrame(1000),
|
||||||
|
new TaikoReplayFrame(1250, TaikoAction.LeftCentre, TaikoAction.LeftRim),
|
||||||
|
new TaikoReplayFrame(1251),
|
||||||
|
new TaikoReplayFrame(1500, TaikoAction.LeftCentre, TaikoAction.LeftRim, TaikoAction.RightCentre, TaikoAction.RightRim),
|
||||||
|
new TaikoReplayFrame(1501),
|
||||||
|
new TaikoReplayFrame(2000),
|
||||||
|
};
|
||||||
|
|
||||||
|
PerformTest(frames, CreateBeatmap(swell));
|
||||||
|
|
||||||
|
AssertJudgementCount(11);
|
||||||
|
|
||||||
|
// this is a charitable interpretation of the inputs.
|
||||||
|
//
|
||||||
|
// for the frame at time 1250, we only count either one of the input actions - simple.
|
||||||
|
//
|
||||||
|
// for the frame at time 1500, we give the user the benefit of the doubt,
|
||||||
|
// and we ignore actions that wouldn't otherwise cause a hit due to not alternating,
|
||||||
|
// but we still count one (just one) of the actions that _would_ normally cause a hit.
|
||||||
|
// this is done as a courtesy to avoid stuff like key chattering after press blocking legitimate inputs.
|
||||||
|
for (int i = 0; i < 2; i++)
|
||||||
|
AssertResult<SwellTick>(i, HitResult.IgnoreHit);
|
||||||
|
for (int i = 2; i < swell.RequiredHits; i++)
|
||||||
|
AssertResult<SwellTick>(i, HitResult.IgnoreMiss);
|
||||||
|
|
||||||
|
AssertResult<Swell>(0, HitResult.IgnoreMiss);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensure input is correctly sent to subsequent hits if a swell is fully completed.
|
/// Ensure input is correctly sent to subsequent hits if a swell is fully completed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
{
|
{
|
||||||
gameplayClock = new GameplayClockContainer(manualClock)
|
gameplayClock = new GameplayClockContainer(manualClock, false, false)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Edit
|
|||||||
{
|
{
|
||||||
public partial class DrawableTaikoEditorRuleset : DrawableTaikoRuleset, ISupportConstantAlgorithmToggle
|
public partial class DrawableTaikoEditorRuleset : DrawableTaikoRuleset, ISupportConstantAlgorithmToggle
|
||||||
{
|
{
|
||||||
public BindableBool ShowSpeedChanges { get; set; } = new BindableBool();
|
public BindableBool ShowSpeedChanges { get; } = new BindableBool();
|
||||||
|
|
||||||
public DrawableTaikoEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
public DrawableTaikoEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
: base(ruleset, beatmap, mods)
|
: base(ruleset, beatmap, mods)
|
||||||
|
@ -17,6 +17,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||||
@ -38,6 +39,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
private readonly CircularContainer targetRing;
|
private readonly CircularContainer targetRing;
|
||||||
private readonly CircularContainer expandingRing;
|
private readonly CircularContainer expandingRing;
|
||||||
|
|
||||||
|
private double? lastPressHandleTime;
|
||||||
|
|
||||||
public override bool DisplayResult => false;
|
public override bool DisplayResult => false;
|
||||||
|
|
||||||
public DrawableSwell()
|
public DrawableSwell()
|
||||||
@ -140,6 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
UnproxyContent();
|
UnproxyContent();
|
||||||
|
|
||||||
lastWasCentre = null;
|
lastWasCentre = null;
|
||||||
|
lastPressHandleTime = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
protected override void AddNestedHitObject(DrawableHitObject hitObject)
|
||||||
@ -266,6 +270,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
ProxyContent();
|
ProxyContent();
|
||||||
else
|
else
|
||||||
UnproxyContent();
|
UnproxyContent();
|
||||||
|
|
||||||
|
if ((Clock as IGameplayClock)?.IsRewinding == true)
|
||||||
|
lastPressHandleTime = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool? lastWasCentre;
|
private bool? lastWasCentre;
|
||||||
@ -285,7 +292,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
if (lastWasCentre == isCentre)
|
if (lastWasCentre == isCentre)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
// If we've already successfully judged a tick this frame, do not judge more.
|
||||||
|
// Note that the ordering is important here - this is intentionally placed after the alternating check.
|
||||||
|
// That is done to prevent accidental double inputs blocking simultaneous but legitimate hits from registering.
|
||||||
|
if (lastPressHandleTime == Time.Current)
|
||||||
|
return true;
|
||||||
|
|
||||||
lastWasCentre = isCentre;
|
lastWasCentre = isCentre;
|
||||||
|
lastPressHandleTime = Time.Current;
|
||||||
|
|
||||||
UpdateResult(true);
|
UpdateResult(true);
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
public partial class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
|
public partial class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
|
||||||
{
|
{
|
||||||
private static readonly Vector2 circle_piece_size = new Vector2(128);
|
private static readonly Vector2 circle_piece_size = new Vector2(128);
|
||||||
|
private static readonly Vector2 max_circle_sprite_size = new Vector2(160);
|
||||||
|
|
||||||
private Drawable backgroundLayer = null!;
|
private Drawable backgroundLayer = null!;
|
||||||
private Drawable? foregroundLayer;
|
private Drawable? foregroundLayer;
|
||||||
@ -54,9 +55,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
|||||||
|
|
||||||
string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit;
|
string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit;
|
||||||
|
|
||||||
return skin.GetAnimation($"{prefix}{lookup}", true, false, maxSize: circle_piece_size) ??
|
return skin.GetAnimation($"{prefix}{lookup}", true, false, maxSize: max_circle_sprite_size) ??
|
||||||
// fallback to regular size if "big" version doesn't exist.
|
// fallback to regular size if "big" version doesn't exist.
|
||||||
skin.GetAnimation($"{normal_hit}{lookup}", true, false, maxSize: circle_piece_size);
|
skin.GetAnimation($"{normal_hit}{lookup}", true, false, maxSize: max_circle_sprite_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
|
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSingleSpan()
|
public void TestSingleSpan()
|
||||||
{
|
{
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, null).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1).ToArray();
|
||||||
|
|
||||||
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
||||||
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
||||||
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestRepeat()
|
public void TestRepeat()
|
||||||
{
|
{
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2, null).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 2).ToArray();
|
||||||
|
|
||||||
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
||||||
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
||||||
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestNonEvenTicks()
|
public void TestNonEvenTicks()
|
||||||
{
|
{
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2, null).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, 300, span_duration, 2).ToArray();
|
||||||
|
|
||||||
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
Assert.That(events[0].Type, Is.EqualTo(SliderEventType.Head));
|
||||||
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
Assert.That(events[0].Time, Is.EqualTo(start_time));
|
||||||
@ -83,12 +83,12 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestLegacyLastTickOffset()
|
public void TestLastTickOffset()
|
||||||
{
|
{
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1, 100).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, 1, span_duration / 2, span_duration, 1).ToArray();
|
||||||
|
|
||||||
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LegacyLastTick));
|
Assert.That(events[2].Type, Is.EqualTo(SliderEventType.LastTick));
|
||||||
Assert.That(events[2].Time, Is.EqualTo(900));
|
Assert.That(events[2].Time, Is.EqualTo(span_duration + SliderEventGenerator.LAST_TICK_OFFSET));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
const double velocity = 5;
|
const double velocity = 5;
|
||||||
const double min_distance = velocity * 10;
|
const double min_distance = velocity * 10;
|
||||||
|
|
||||||
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2, 0).ToArray();
|
var events = SliderEventGenerator.Generate(start_time, span_duration, velocity, velocity, span_duration, 2).ToArray();
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
|
@ -126,7 +126,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
public void TestSamplePlaybackWithBeatmapHitsoundsOff()
|
public void TestSamplePlaybackWithBeatmapHitsoundsOff()
|
||||||
{
|
{
|
||||||
GameplayClockContainer gameplayContainer = null;
|
GameplayClockContainer gameplayContainer = null;
|
||||||
TestDrawableStoryboardSample sample = null;
|
DrawableStoryboardSample sample = null;
|
||||||
|
|
||||||
AddStep("disable beatmap hitsounds", () => config.SetValue(OsuSetting.BeatmapHitsounds, false));
|
AddStep("disable beatmap hitsounds", () => config.SetValue(OsuSetting.BeatmapHitsounds, false));
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
Child = beatmapSkinSourceContainer
|
Child = beatmapSkinSourceContainer
|
||||||
});
|
});
|
||||||
|
|
||||||
beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
|
beatmapSkinSourceContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
|
||||||
{
|
{
|
||||||
Clock = gameplayContainer
|
Clock = gameplayContainer
|
||||||
});
|
});
|
||||||
@ -199,14 +199,6 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
protected internal override ISkin GetSkin() => new TestSkin("test-sample", resources);
|
protected internal override ISkin GetSkin() => new TestSkin("test-sample", resources);
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class TestDrawableStoryboardSample : DrawableStoryboardSample
|
|
||||||
{
|
|
||||||
public TestDrawableStoryboardSample(StoryboardSampleInfo sampleInfo)
|
|
||||||
: base(sampleInfo)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IResourceStorageProvider
|
#region IResourceStorageProvider
|
||||||
|
|
||||||
public IRenderer Renderer => host.Renderer;
|
public IRenderer Renderer => host.Renderer;
|
||||||
|
@ -42,22 +42,22 @@ namespace osu.Game.Tests.Mods
|
|||||||
private class ClassWithSettings
|
private class ClassWithSettings
|
||||||
{
|
{
|
||||||
[SettingSource("Unordered setting", "Should be last")]
|
[SettingSource("Unordered setting", "Should be last")]
|
||||||
public BindableFloat UnorderedSetting { get; set; } = new BindableFloat();
|
public BindableFloat UnorderedSetting { get; } = new BindableFloat();
|
||||||
|
|
||||||
[SettingSource("Second setting", "Another description", 2)]
|
[SettingSource("Second setting", "Another description", 2)]
|
||||||
public BindableBool SecondSetting { get; set; } = new BindableBool();
|
public BindableBool SecondSetting { get; } = new BindableBool();
|
||||||
|
|
||||||
[SettingSource("First setting", "A description", 1)]
|
[SettingSource("First setting", "A description", 1)]
|
||||||
public BindableDouble FirstSetting { get; set; } = new BindableDouble();
|
public BindableDouble FirstSetting { get; } = new BindableDouble();
|
||||||
|
|
||||||
[SettingSource("Third setting", "Yet another description", 3)]
|
[SettingSource("Third setting", "Yet another description", 3)]
|
||||||
public BindableInt ThirdSetting { get; set; } = new BindableInt();
|
public BindableInt ThirdSetting { get; } = new BindableInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ClassWithCustomSettingControl
|
private class ClassWithCustomSettingControl
|
||||||
{
|
{
|
||||||
[SettingSource("Custom setting", "Should be a custom control", SettingControlType = typeof(CustomSettingsControl))]
|
[SettingSource("Custom setting", "Should be a custom control", SettingControlType = typeof(CustomSettingsControl))]
|
||||||
public BindableInt UnorderedSetting { get; set; } = new BindableInt();
|
public BindableInt UnorderedSetting { get; } = new BindableInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class CustomSettingsControl : SettingsItem<int>
|
private partial class CustomSettingsControl : SettingsItem<int>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
@ -16,16 +17,16 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
[TestCase(1)]
|
[TestCase(1)]
|
||||||
public void TestTrueGameplayRateWithGameplayAdjustment(double underlyingClockRate)
|
public void TestTrueGameplayRateWithGameplayAdjustment(double underlyingClockRate)
|
||||||
{
|
{
|
||||||
var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate });
|
var trackVirtual = new TrackVirtual(60000) { Frequency = { Value = underlyingClockRate } };
|
||||||
var gameplayClock = new TestGameplayClockContainer(framedClock);
|
var gameplayClock = new TestGameplayClockContainer(trackVirtual);
|
||||||
|
|
||||||
Assert.That(gameplayClock.GetTrueGameplayRate(), Is.EqualTo(2));
|
Assert.That(gameplayClock.GetTrueGameplayRate(), Is.EqualTo(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
private partial class TestGameplayClockContainer : GameplayClockContainer
|
private partial class TestGameplayClockContainer : GameplayClockContainer
|
||||||
{
|
{
|
||||||
public TestGameplayClockContainer(IFrameBasedClock underlyingClock)
|
public TestGameplayClockContainer(IClock underlyingClock)
|
||||||
: base(underlyingClock)
|
: base(underlyingClock, false, false)
|
||||||
{
|
{
|
||||||
AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(2.0));
|
AdjustmentsFromMods.AddAdjustment(AdjustableProperty.Frequency, new BindableDouble(2.0));
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,9 @@ using osu.Game.Tests.Visual;
|
|||||||
|
|
||||||
namespace osu.Game.Tests.OnlinePlay
|
namespace osu.Game.Tests.OnlinePlay
|
||||||
{
|
{
|
||||||
|
// NOTE: This test scene never calls ProcessFrame on clocks.
|
||||||
|
// The current tests are fine without this as they are testing very static scenarios, but it's worth knowing
|
||||||
|
// if adding further tests to this class.
|
||||||
[HeadlessTest]
|
[HeadlessTest]
|
||||||
public partial class TestSceneCatchUpSyncManager : OsuTestScene
|
public partial class TestSceneCatchUpSyncManager : OsuTestScene
|
||||||
{
|
{
|
||||||
@ -28,7 +31,7 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock()));
|
syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock(), false, false));
|
||||||
player1 = syncManager.CreateManagedClock();
|
player1 = syncManager.CreateManagedClock();
|
||||||
player2 = syncManager.CreateManagedClock();
|
player2 = syncManager.CreateManagedClock();
|
||||||
|
|
||||||
@ -188,6 +191,8 @@ namespace osu.Game.Tests.OnlinePlay
|
|||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
|
IsRunning = false;
|
||||||
|
CurrentTime = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetSpeedAdjustments()
|
public void ResetSpeedAdjustments()
|
||||||
|
@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("seek near end", () => EditorClock.Seek(EditorClock.TrackLength - 250));
|
AddStep("seek near end", () => EditorClock.Seek(EditorClock.TrackLength - 250));
|
||||||
AddUntilStep("clock stops", () => !EditorClock.IsRunning);
|
AddUntilStep("clock stops", () => !EditorClock.IsRunning);
|
||||||
|
|
||||||
AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime - EditorClock.TotalAppliedOffset, () => Is.EqualTo(EditorClock.TrackLength));
|
AddUntilStep("clock stopped at end", () => EditorClock.CurrentTime, () => Is.EqualTo(EditorClock.TrackLength));
|
||||||
|
|
||||||
AddStep("start clock again", () => EditorClock.Start());
|
AddStep("start clock again", () => EditorClock.Start());
|
||||||
AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500);
|
AddAssert("clock looped to start", () => EditorClock.IsRunning && EditorClock.CurrentTime < 500);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
@ -12,7 +13,6 @@ using osu.Game.Rulesets.Osu.Judgements;
|
|||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osuTK;
|
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -22,6 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Cached(typeof(HealthProcessor))]
|
[Cached(typeof(HealthProcessor))]
|
||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
|
private ArgonHealthDisplay healthDisplay = null!;
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
@ -37,14 +39,25 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = Color4.Gray,
|
Colour = Color4.Gray,
|
||||||
},
|
},
|
||||||
new ArgonHealthDisplay
|
healthDisplay = new ArgonHealthDisplay
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Scale = new Vector2(2f),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddSliderStep("Width", 0, 1f, 1f, val =>
|
||||||
|
{
|
||||||
|
if (healthDisplay.IsNotNull())
|
||||||
|
healthDisplay.BarLength.Value = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddSliderStep("Height", 0, 64, 0, val =>
|
||||||
|
{
|
||||||
|
if (healthDisplay.IsNotNull())
|
||||||
|
healthDisplay.BarHeight.Value = val;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -6,11 +6,11 @@ using System.Diagnostics;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
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;
|
||||||
using osu.Framework.Timing;
|
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[Cached(typeof(IGameplayClock))]
|
[Cached(typeof(IGameplayClock))]
|
||||||
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
|
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false);
|
||||||
|
|
||||||
// best way to check without exposing.
|
// best way to check without exposing.
|
||||||
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
|
private Drawable hideTarget => hudOverlay.ChildrenOfType<SkinComponentsContainer>().First();
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -173,7 +174,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
string? filePath = null;
|
string? filePath = null;
|
||||||
|
|
||||||
// Files starting with _ are temporary, created by CreateFileSafely call.
|
// Files starting with _ are temporary, created by CreateFileSafely call.
|
||||||
AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !f.StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null);
|
AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null);
|
||||||
AddAssert("filesize is non-zero", () =>
|
AddAssert("filesize is non-zero", () =>
|
||||||
{
|
{
|
||||||
using (var stream = LocalStorage.GetStream(filePath))
|
using (var stream = LocalStorage.GetStream(filePath))
|
||||||
|
@ -3,14 +3,20 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
@ -23,6 +29,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers
|
public partial class TestScenePlayerMaxDimensions : TestSceneAllRulesetPlayers
|
||||||
{
|
{
|
||||||
|
// scale textures to 4 times their size.
|
||||||
|
private const int scale_factor = 4;
|
||||||
|
|
||||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
{
|
{
|
||||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||||
@ -63,18 +72,66 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
remove { }
|
remove { }
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
|
||||||
{
|
|
||||||
var texture = base.GetTexture(componentName, wrapModeS, wrapModeT);
|
|
||||||
|
|
||||||
if (texture != null)
|
|
||||||
texture.ScaleAdjust /= 8f;
|
|
||||||
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => this;
|
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => this;
|
||||||
public IEnumerable<ISkin> AllSources => new[] { this };
|
public IEnumerable<ISkin> AllSources => new[] { this };
|
||||||
|
|
||||||
|
protected override IResourceStore<TextureUpload> CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore<byte[]> storage)
|
||||||
|
=> new UpscaledTextureLoaderStore(base.CreateTextureLoaderStore(resources, storage));
|
||||||
|
|
||||||
|
private class UpscaledTextureLoaderStore : IResourceStore<TextureUpload>
|
||||||
|
{
|
||||||
|
private readonly IResourceStore<TextureUpload>? textureStore;
|
||||||
|
|
||||||
|
public UpscaledTextureLoaderStore(IResourceStore<TextureUpload>? textureStore)
|
||||||
|
{
|
||||||
|
this.textureStore = textureStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
textureStore?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextureUpload Get(string name)
|
||||||
|
{
|
||||||
|
var textureUpload = textureStore?.Get(name);
|
||||||
|
|
||||||
|
// NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
|
||||||
|
if (textureUpload == null)
|
||||||
|
return null!;
|
||||||
|
|
||||||
|
return upscale(textureUpload);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken())
|
||||||
|
{
|
||||||
|
// NRT not enabled on framework side classes (IResourceStore / TextureLoaderStore), welp.
|
||||||
|
if (textureStore == null)
|
||||||
|
return null!;
|
||||||
|
|
||||||
|
var textureUpload = await textureStore.GetAsync(name, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (textureUpload == null)
|
||||||
|
return null!;
|
||||||
|
|
||||||
|
return await Task.Run(() => upscale(textureUpload), cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TextureUpload upscale(TextureUpload textureUpload)
|
||||||
|
{
|
||||||
|
var image = Image.LoadPixelData(textureUpload.Data.ToArray(), textureUpload.Width, textureUpload.Height);
|
||||||
|
|
||||||
|
// The original texture upload will no longer be returned or used.
|
||||||
|
textureUpload.Dispose();
|
||||||
|
|
||||||
|
image.Mutate(i => i.Resize(new Size(textureUpload.Width, textureUpload.Height) * scale_factor));
|
||||||
|
return new TextureUpload(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream? GetStream(string name) => textureStore?.GetStream(name);
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAvailableResources() => textureStore?.GetAvailableResources() ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
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;
|
||||||
using osu.Framework.Timing;
|
|
||||||
using osu.Game.Overlays.SkinEditor;
|
using osu.Game.Overlays.SkinEditor;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[Cached(typeof(IGameplayClock))]
|
[Cached(typeof(IGameplayClock))]
|
||||||
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
|
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false);
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
public readonly EditorClipboard Clipboard = new EditorClipboard();
|
||||||
|
@ -8,11 +8,11 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
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;
|
||||||
using osu.Framework.Timing;
|
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
private GameplayState gameplayState = TestGameplayState.Create(new OsuRuleset());
|
||||||
|
|
||||||
[Cached(typeof(IGameplayClock))]
|
[Cached(typeof(IGameplayClock))]
|
||||||
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new FramedClock());
|
private readonly IGameplayClock gameplayClock = new GameplayClockContainer(new TrackVirtual(60000), false, false);
|
||||||
|
|
||||||
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
|
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu.Objects;
|
|||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
@ -20,9 +21,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Cached(typeof(HealthProcessor))]
|
[Cached(typeof(HealthProcessor))]
|
||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay();
|
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f) };
|
||||||
protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay();
|
protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) };
|
||||||
protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay();
|
protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) };
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
@ -62,4 +63,4 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
}, 3);
|
}, 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,14 +106,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
if (storyboard != null)
|
if (storyboard != null)
|
||||||
storyboardContainer.Remove(storyboard, true);
|
storyboardContainer.Remove(storyboard, true);
|
||||||
|
|
||||||
var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
|
storyboardContainer.Clock = new FramedClock(Beatmap.Value.Track);
|
||||||
storyboardContainer.Clock = decoupledClock;
|
|
||||||
|
|
||||||
storyboard = toLoad.CreateDrawable(SelectedMods.Value);
|
storyboard = toLoad.CreateDrawable(SelectedMods.Value);
|
||||||
storyboard.Passing = false;
|
storyboard.Passing = false;
|
||||||
|
|
||||||
storyboardContainer.Add(storyboard);
|
storyboardContainer.Add(storyboard);
|
||||||
decoupledClock.ChangeSource(Beatmap.Value.Track);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadStoryboard(string filename, Action<Storyboard>? setUpStoryboard = null)
|
private void loadStoryboard(string filename, Action<Storyboard>? setUpStoryboard = null)
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
@ -80,11 +79,11 @@ namespace osu.Game.Tournament.Tests.Components
|
|||||||
{
|
{
|
||||||
Team1 =
|
Team1 =
|
||||||
{
|
{
|
||||||
Value = new TournamentTeam { Players = new BindableList<TournamentUser> { redUser } }
|
Value = new TournamentTeam { Players = { redUser } }
|
||||||
},
|
},
|
||||||
Team2 =
|
Team2 =
|
||||||
{
|
{
|
||||||
Value = new TournamentTeam { Players = new BindableList<TournamentUser> { blueUser, blueUserWithCustomColour } }
|
Value = new TournamentTeam { Players = { blueUser, blueUserWithCustomColour } }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Tournament.Models
|
|||||||
};
|
};
|
||||||
|
|
||||||
[JsonProperty]
|
[JsonProperty]
|
||||||
public BindableList<TournamentUser> Players { get; set; } = new BindableList<TournamentUser>();
|
public BindableList<TournamentUser> Players { get; } = new BindableList<TournamentUser>();
|
||||||
|
|
||||||
public TournamentTeam()
|
public TournamentTeam()
|
||||||
{
|
{
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio.Track;
|
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
@ -28,16 +27,6 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
private readonly bool applyOffsets;
|
private readonly bool applyOffsets;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The length of the underlying beatmap track. Will default to 60 seconds if unavailable.
|
|
||||||
/// </summary>
|
|
||||||
public double TrackLength => Track.Length;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The underlying beatmap track, if available.
|
|
||||||
/// </summary>
|
|
||||||
public Track Track { get; private set; } = new TrackVirtual(60000);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The total frequency adjustment from pause transforms. Should eventually be handled in a better way.
|
/// The total frequency adjustment from pause transforms. Should eventually be handled in a better way.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -53,7 +42,7 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
private IDisposable? beatmapOffsetSubscription;
|
private IDisposable? beatmapOffsetSubscription;
|
||||||
|
|
||||||
private readonly DecoupleableInterpolatingFramedClock decoupledClock;
|
private readonly DecouplingFramedClock decoupledTrack;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; } = null!;
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
@ -66,25 +55,21 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public bool IsRewinding { get; private set; }
|
public bool IsRewinding { get; private set; }
|
||||||
|
|
||||||
public bool IsCoupled
|
public FramedBeatmapClock(bool applyOffsets, bool requireDecoupling, IClock? source = null)
|
||||||
{
|
|
||||||
get => decoupledClock.IsCoupled;
|
|
||||||
set => decoupledClock.IsCoupled = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FramedBeatmapClock(bool applyOffsets = false)
|
|
||||||
{
|
{
|
||||||
this.applyOffsets = applyOffsets;
|
this.applyOffsets = applyOffsets;
|
||||||
|
|
||||||
// A decoupled clock is used to ensure precise time values even when the host audio subsystem is not reporting
|
decoupledTrack = new DecouplingFramedClock(source) { AllowDecoupling = requireDecoupling };
|
||||||
|
|
||||||
|
// An interpolating clock is used to ensure precise time values even when the host audio subsystem is not reporting
|
||||||
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
|
// high precision times (on windows there's generally only 5-10ms reporting intervals, as an example).
|
||||||
decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
|
var interpolatedTrack = new InterpolatingFramedClock(decoupledTrack);
|
||||||
|
|
||||||
if (applyOffsets)
|
if (applyOffsets)
|
||||||
{
|
{
|
||||||
// Audio timings in general with newer BASS versions don't match stable.
|
// Audio timings in general with newer BASS versions don't match stable.
|
||||||
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck.
|
||||||
platformOffsetClock = new OffsetCorrectionClock(decoupledClock, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
|
platformOffsetClock = new OffsetCorrectionClock(interpolatedTrack, ExternalPauseFrequencyAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 };
|
||||||
|
|
||||||
// User global offset (set in settings) should also be applied.
|
// User global offset (set in settings) should also be applied.
|
||||||
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust);
|
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock, ExternalPauseFrequencyAdjust);
|
||||||
@ -94,7 +79,7 @@ namespace osu.Game.Beatmaps
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
finalClockSource = decoupledClock;
|
finalClockSource = interpolatedTrack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +95,7 @@ namespace osu.Game.Beatmaps
|
|||||||
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
|
||||||
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
|
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
|
||||||
|
|
||||||
|
// TODO: this doesn't update when using ChangeSource() to change beatmap.
|
||||||
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
|
beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
|
||||||
r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
|
r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
|
||||||
settings => settings.Offset,
|
settings => settings.Offset,
|
||||||
@ -124,17 +110,7 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (Source != null && Source is not IAdjustableClock && Source.CurrentTime < decoupledClock.CurrentTime - 100)
|
finalClockSource.ProcessFrame();
|
||||||
{
|
|
||||||
// InterpolatingFramedClock won't interpolate backwards unless its source has an ElapsedFrameTime.
|
|
||||||
// See https://github.com/ppy/osu-framework/blob/ba1385330cc501f34937e08257e586c84e35d772/osu.Framework/Timing/InterpolatingFramedClock.cs#L91-L93
|
|
||||||
// This is not always the case here when doing large seeks.
|
|
||||||
// (Of note, this is not an issue if the source is adjustable, as the source is seeked to be in time by DecoupleableInterpolatingFramedClock).
|
|
||||||
// Rather than trying to get around this by fixing the framework clock stack, let's work around it for now.
|
|
||||||
Seek(Source.CurrentTime);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
finalClockSource.ProcessFrame();
|
|
||||||
|
|
||||||
if (Clock.ElapsedFrameTime != 0)
|
if (Clock.ElapsedFrameTime != 0)
|
||||||
IsRewinding = Clock.ElapsedFrameTime < 0;
|
IsRewinding = Clock.ElapsedFrameTime < 0;
|
||||||
@ -157,46 +133,42 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
#region Delegation of IAdjustableClock / ISourceChangeableClock to decoupled clock.
|
#region Delegation of IAdjustableClock / ISourceChangeableClock to decoupled clock.
|
||||||
|
|
||||||
public void ChangeSource(IClock? source)
|
public void ChangeSource(IClock? source) => decoupledTrack.ChangeSource(source);
|
||||||
{
|
|
||||||
Track = source as Track ?? new TrackVirtual(60000);
|
|
||||||
decoupledClock.ChangeSource(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IClock? Source => decoupledClock.Source;
|
public IClock Source => decoupledTrack.Source;
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
decoupledClock.Reset();
|
decoupledTrack.Reset();
|
||||||
finalClockSource.ProcessFrame();
|
finalClockSource.ProcessFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
decoupledClock.Start();
|
decoupledTrack.Start();
|
||||||
finalClockSource.ProcessFrame();
|
finalClockSource.ProcessFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
decoupledClock.Stop();
|
decoupledTrack.Stop();
|
||||||
finalClockSource.ProcessFrame();
|
finalClockSource.ProcessFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Seek(double position)
|
public bool Seek(double position)
|
||||||
{
|
{
|
||||||
bool success = decoupledClock.Seek(position - TotalAppliedOffset);
|
bool success = decoupledTrack.Seek(position - TotalAppliedOffset);
|
||||||
finalClockSource.ProcessFrame();
|
finalClockSource.ProcessFrame();
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ResetSpeedAdjustments() => decoupledClock.ResetSpeedAdjustments();
|
public void ResetSpeedAdjustments() => decoupledTrack.ResetSpeedAdjustments();
|
||||||
|
|
||||||
public double Rate
|
public double Rate
|
||||||
{
|
{
|
||||||
get => decoupledClock.Rate;
|
get => decoupledTrack.Rate;
|
||||||
set => decoupledClock.Rate = value;
|
set => decoupledTrack.Rate = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of dim to be used when <see cref="IgnoreUserSettings"/> is <c>true</c>.
|
/// The amount of dim to be used when <see cref="IgnoreUserSettings"/> is <c>true</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Bindable<float> DimWhenUserSettingsIgnored { get; set; } = new Bindable<float>();
|
public Bindable<float> DimWhenUserSettingsIgnored { get; } = new Bindable<float>();
|
||||||
|
|
||||||
protected Bindable<bool> LightenDuringBreaks { get; private set; } = null!;
|
protected Bindable<bool> LightenDuringBreaks { get; private set; } = null!;
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Net.Sockets;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
|
||||||
@ -99,6 +100,11 @@ namespace osu.Game.Online.API
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (SocketException)
|
||||||
|
{
|
||||||
|
// Network failure.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
catch (HttpRequestException)
|
catch (HttpRequestException)
|
||||||
{
|
{
|
||||||
// Network failure.
|
// Network failure.
|
||||||
@ -106,7 +112,7 @@ namespace osu.Game.Online.API
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Force a full re-reauthentication.
|
// Force a full re-authentication.
|
||||||
Token.Value = null;
|
Token.Value = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -215,7 +215,7 @@ namespace osu.Game
|
|||||||
/// For now, this is used as a source specifically for beat synced components.
|
/// For now, this is used as a source specifically for beat synced components.
|
||||||
/// Going forward, it could potentially be used as the single source-of-truth for beatmap timing.
|
/// Going forward, it could potentially be used as the single source-of-truth for beatmap timing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(true);
|
private readonly FramedBeatmapClock beatmapClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: false);
|
||||||
|
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content => content;
|
||||||
|
|
||||||
@ -441,16 +441,7 @@ namespace osu.Game
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction)
|
private void onTrackChanged(WorkingBeatmap beatmap, TrackChangeDirection direction) => beatmapClock.ChangeSource(beatmap.Track);
|
||||||
{
|
|
||||||
// FramedBeatmapClock uses a decoupled clock internally which will mutate the source if it is an `IAdjustableClock`.
|
|
||||||
// We don't want this for now, as the intention of beatmapClock is to be a read-only source for beat sync components.
|
|
||||||
//
|
|
||||||
// Encapsulating in a FramedClock will avoid any mutations.
|
|
||||||
var framedClock = new FramedClock(beatmap.Track);
|
|
||||||
|
|
||||||
beatmapClock.ChangeSource(framedClock);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void InitialiseFonts()
|
protected virtual void InitialiseFonts()
|
||||||
{
|
{
|
||||||
|
@ -13,7 +13,7 @@ using osu.Game.Beatmaps.ControlPoints;
|
|||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy
|
namespace osu.Game.Rulesets.Objects.Legacy
|
||||||
{
|
{
|
||||||
internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, IHasSliderVelocity
|
internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasSliderVelocity
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Scoring distance with a speed-adjusted beat length of 1 second.
|
/// Scoring distance with a speed-adjusted beat length of 1 second.
|
||||||
@ -59,7 +59,5 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
|
|
||||||
Velocity = scoringDistance / timingPoint.BeatLength;
|
Velocity = scoringDistance / timingPoint.BeatLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double LegacyLastTickOffset => 36;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,9 +10,17 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
{
|
{
|
||||||
public static class SliderEventGenerator
|
public static class SliderEventGenerator
|
||||||
{
|
{
|
||||||
// ReSharper disable once MethodOverloadWithOptionalParameter
|
/// <summary>
|
||||||
|
/// Historically, slider's final tick (aka the place where the slider would receive a final judgement) was offset by -36 ms. Originally this was
|
||||||
|
/// done to workaround a technical detail (unimportant), but over the years it has become an expectation of players that you don't need to hold
|
||||||
|
/// until the true end of the slider. This very small amount of leniency makes it easier to jump away from fast sliders to the next hit object.
|
||||||
|
///
|
||||||
|
/// After discussion on how this should be handled going forward, players have unanimously stated that this lenience should remain in some way.
|
||||||
|
/// </summary>
|
||||||
|
public const double LAST_TICK_OFFSET = -36;
|
||||||
|
|
||||||
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
|
public static IEnumerable<SliderEventDescriptor> Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
|
||||||
double? legacyLastTickOffset, CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
// A very lenient maximum length of a slider for ticks to be generated.
|
// A very lenient maximum length of a slider for ticks to be generated.
|
||||||
// This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
|
// This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
|
||||||
@ -76,14 +84,14 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
|
|
||||||
int finalSpanIndex = spanCount - 1;
|
int finalSpanIndex = spanCount - 1;
|
||||||
double finalSpanStartTime = startTime + finalSpanIndex * spanDuration;
|
double finalSpanStartTime = startTime + finalSpanIndex * spanDuration;
|
||||||
double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) - (legacyLastTickOffset ?? 0));
|
double finalSpanEndTime = Math.Max(startTime + totalDuration / 2, (finalSpanStartTime + spanDuration) + LAST_TICK_OFFSET);
|
||||||
double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration;
|
double finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration;
|
||||||
|
|
||||||
if (spanCount % 2 == 0) finalProgress = 1 - finalProgress;
|
if (spanCount % 2 == 0) finalProgress = 1 - finalProgress;
|
||||||
|
|
||||||
yield return new SliderEventDescriptor
|
yield return new SliderEventDescriptor
|
||||||
{
|
{
|
||||||
Type = SliderEventType.LegacyLastTick,
|
Type = SliderEventType.LastTick,
|
||||||
SpanIndex = finalSpanIndex,
|
SpanIndex = finalSpanIndex,
|
||||||
SpanStartTime = finalSpanStartTime,
|
SpanStartTime = finalSpanStartTime,
|
||||||
Time = finalSpanEndTime,
|
Time = finalSpanEndTime,
|
||||||
@ -173,7 +181,11 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
public enum SliderEventType
|
public enum SliderEventType
|
||||||
{
|
{
|
||||||
Tick,
|
Tick,
|
||||||
LegacyLastTick,
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs just before the tail. See <see cref="SliderEventGenerator.LAST_TICK_OFFSET"/>.
|
||||||
|
/// </summary>
|
||||||
|
LastTick,
|
||||||
Head,
|
Head,
|
||||||
Tail,
|
Tail,
|
||||||
Repeat
|
Repeat
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Types
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A type of <see cref="HitObject"/> which may require the last tick to be offset.
|
|
||||||
/// This is specific to osu!stable conversion, and should not be used elsewhere.
|
|
||||||
/// </summary>
|
|
||||||
public interface IHasLegacyLastTickOffset
|
|
||||||
{
|
|
||||||
double LegacyLastTickOffset { get; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -862,7 +862,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
private void resetTrack(bool seekToStart = false)
|
private void resetTrack(bool seekToStart = false)
|
||||||
{
|
{
|
||||||
Beatmap.Value.Track.Stop();
|
clock.Stop();
|
||||||
|
|
||||||
if (seekToStart)
|
if (seekToStart)
|
||||||
{
|
{
|
||||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
this.beatDivisor = beatDivisor ?? new BindableBeatDivisor();
|
this.beatDivisor = beatDivisor ?? new BindableBeatDivisor();
|
||||||
|
|
||||||
underlyingClock = new FramedBeatmapClock(applyOffsets: true) { IsCoupled = false };
|
underlyingClock = new FramedBeatmapClock(applyOffsets: true, requireDecoupling: true);
|
||||||
AddInternal(underlyingClock);
|
AddInternal(underlyingClock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,8 +158,6 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
public double CurrentTime => underlyingClock.CurrentTime;
|
public double CurrentTime => underlyingClock.CurrentTime;
|
||||||
|
|
||||||
public double TotalAppliedOffset => underlyingClock.TotalAppliedOffset;
|
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
ClearTransforms();
|
ClearTransforms();
|
||||||
|
@ -94,7 +94,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
|
controlPointGroups.BindTo(editorBeatmap.ControlPointInfo.Groups);
|
||||||
controlPointGroups.BindCollectionChanged((_, _) => updateTimingGroup());
|
controlPointGroups.BindCollectionChanged((_, _) => updateTimingGroup());
|
||||||
|
|
||||||
beatLength.BindValueChanged(_ => regenerateDisplay(true), true);
|
beatLength.BindValueChanged(_ => Scheduler.AddOnce(regenerateDisplay, true), true);
|
||||||
|
|
||||||
displayLocked.BindValueChanged(locked =>
|
displayLocked.BindValueChanged(locked =>
|
||||||
{
|
{
|
||||||
@ -186,11 +186,18 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
displayedTime = time;
|
displayedTime = time;
|
||||||
regenerateDisplay(animated);
|
Scheduler.AddOnce(regenerateDisplay, animated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void regenerateDisplay(bool animated)
|
private void regenerateDisplay(bool animated)
|
||||||
{
|
{
|
||||||
|
// Before a track is loaded, it won't have a valid length, which will break things.
|
||||||
|
if (!beatmap.Value.Track.IsLoaded)
|
||||||
|
{
|
||||||
|
Scheduler.AddOnce(regenerateDisplay, animated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
double index = (displayedTime - selectedGroupStartTime) / timingPoint.BeatLength;
|
double index = (displayedTime - selectedGroupStartTime) / timingPoint.BeatLength;
|
||||||
|
|
||||||
// Chosen as a pretty usable number across all BPMs.
|
// Chosen as a pretty usable number across all BPMs.
|
||||||
|
@ -36,7 +36,6 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
private Sample welcome;
|
private Sample welcome;
|
||||||
|
|
||||||
private DecoupleableInterpolatingFramedClock decoupledClock;
|
|
||||||
private TrianglesIntroSequence intro;
|
private TrianglesIntroSequence intro;
|
||||||
|
|
||||||
public IntroTriangles([CanBeNull] Func<MainMenu> createNextScreen = null)
|
public IntroTriangles([CanBeNull] Func<MainMenu> createNextScreen = null)
|
||||||
@ -59,18 +58,12 @@ namespace osu.Game.Screens.Menu
|
|||||||
{
|
{
|
||||||
PrepareMenuLoad();
|
PrepareMenuLoad();
|
||||||
|
|
||||||
decoupledClock = new DecoupleableInterpolatingFramedClock
|
var decouplingClock = new DecouplingFramedClock(UsingThemedIntro ? Track : null);
|
||||||
{
|
|
||||||
IsCoupled = false
|
|
||||||
};
|
|
||||||
|
|
||||||
if (UsingThemedIntro)
|
|
||||||
decoupledClock.ChangeSource(Track);
|
|
||||||
|
|
||||||
LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground())
|
LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground())
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Clock = decoupledClock,
|
Clock = new InterpolatingFramedClock(decouplingClock),
|
||||||
LoadMenu = LoadMenu
|
LoadMenu = LoadMenu
|
||||||
}, _ =>
|
}, _ =>
|
||||||
{
|
{
|
||||||
@ -94,7 +87,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
StartTrack();
|
StartTrack();
|
||||||
|
|
||||||
// no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure.
|
// no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure.
|
||||||
decoupledClock.Start();
|
decouplingClock.Start();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -456,6 +456,7 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
|
|
||||||
private IEnumerable<Drawable> createButtons() => new[]
|
private IEnumerable<Drawable> createButtons() => new[]
|
||||||
{
|
{
|
||||||
|
beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap),
|
||||||
showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie)
|
showResultsButton = new GrayButton(FontAwesome.Solid.ChartPie)
|
||||||
{
|
{
|
||||||
Size = new Vector2(30, 30),
|
Size = new Vector2(30, 30),
|
||||||
@ -463,7 +464,6 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
Alpha = AllowShowingResults ? 1 : 0,
|
Alpha = AllowShowingResults ? 1 : 0,
|
||||||
TooltipText = "View results"
|
TooltipText = "View results"
|
||||||
},
|
},
|
||||||
beatmap == null ? Empty() : new PlaylistDownloadButton(beatmap),
|
|
||||||
editButton = new PlaylistEditButton
|
editButton = new PlaylistEditButton
|
||||||
{
|
{
|
||||||
Size = new Vector2(30, 30),
|
Size = new Vector2(30, 30),
|
||||||
|
@ -23,7 +23,18 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
{
|
{
|
||||||
public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
|
public partial class FooterButtonFreeMods : FooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
|
||||||
{
|
{
|
||||||
public Bindable<IReadOnlyList<Mod>> Current { get; set; } = new BindableWithCurrent<IReadOnlyList<Mod>>();
|
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||||
|
|
||||||
|
public Bindable<IReadOnlyList<Mod>> Current
|
||||||
|
{
|
||||||
|
get => current.Current;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
|
||||||
|
current.Current = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private OsuSpriteText count = null!;
|
private OsuSpriteText count = null!;
|
||||||
|
|
||||||
@ -106,17 +117,17 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
|
|
||||||
private void updateModDisplay()
|
private void updateModDisplay()
|
||||||
{
|
{
|
||||||
int current = Current.Value.Count;
|
int currentCount = Current.Value.Count;
|
||||||
|
|
||||||
if (current == allAvailableAndValidMods.Count())
|
if (currentCount == allAvailableAndValidMods.Count())
|
||||||
{
|
{
|
||||||
count.Text = "all";
|
count.Text = "all";
|
||||||
count.FadeColour(colours.Gray2, 200, Easing.OutQuint);
|
count.FadeColour(colours.Gray2, 200, Easing.OutQuint);
|
||||||
circle.FadeColour(colours.Yellow, 200, Easing.OutQuint);
|
circle.FadeColour(colours.Yellow, 200, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
else if (current > 0)
|
else if (currentCount > 0)
|
||||||
{
|
{
|
||||||
count.Text = $"{current} mods";
|
count.Text = $"{currentCount} mods";
|
||||||
count.FadeColour(colours.Gray2, 200, Easing.OutQuint);
|
count.FadeColour(colours.Gray2, 200, Easing.OutQuint);
|
||||||
circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint);
|
circle.FadeColour(colours.YellowDark, 200, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay.
|
// The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay.
|
||||||
if (GameplayClockContainer.SourceClock.IsRunning)
|
if (GameplayClockContainer.IsRunning)
|
||||||
GameplayClockContainer.Start();
|
GameplayClockContainer.Start();
|
||||||
else
|
else
|
||||||
GameplayClockContainer.Stop();
|
GameplayClockContainer.Stop();
|
||||||
@ -67,7 +67,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
|||||||
|
|
||||||
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
|
||||||
{
|
{
|
||||||
var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock);
|
// Importantly, we don't want to apply decoupling because SpectatorPlayerClock updates its IsRunning directly.
|
||||||
|
// If we applied decoupling, this state change wouldn't actually cause the clock to stop.
|
||||||
|
// TODO: Can we just use Start/Stop rather than this workaround, now that DecouplingClock is more sane?
|
||||||
|
var gameplayClockContainer = new GameplayClockContainer(spectatorPlayerClock, applyOffsets: false, requireDecoupling: false);
|
||||||
clockAdjustmentsFromMods.BindAdjustments(gameplayClockContainer.AdjustmentsFromMods);
|
clockAdjustmentsFromMods.BindAdjustments(gameplayClockContainer.AdjustmentsFromMods);
|
||||||
return gameplayClockContainer;
|
return gameplayClockContainer;
|
||||||
}
|
}
|
||||||
|
@ -27,10 +27,10 @@ using osuTK.Graphics;
|
|||||||
namespace osu.Game.Screens.Play
|
namespace osu.Game.Screens.Play
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Manage the animation to be applied when a player fails.
|
/// Manage the animation to be applied when a player fails. Applies the animation to children.
|
||||||
/// Single use and automatically disposed after use.
|
/// Single use and automatically disposed after use.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class FailAnimation : Container
|
public partial class FailAnimationContainer : Container
|
||||||
{
|
{
|
||||||
public Action? OnComplete;
|
public Action? OnComplete;
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ namespace osu.Game.Screens.Play
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public BackgroundScreen? Background { private get; set; }
|
public BackgroundScreen? Background { private get; set; }
|
||||||
|
|
||||||
public FailAnimation(DrawableRuleset drawableRuleset)
|
public FailAnimationContainer(DrawableRuleset drawableRuleset)
|
||||||
{
|
{
|
||||||
this.drawableRuleset = drawableRuleset;
|
this.drawableRuleset = drawableRuleset;
|
||||||
|
|
@ -23,11 +23,6 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
public bool IsRewinding => GameplayClock.IsRewinding;
|
public bool IsRewinding => GameplayClock.IsRewinding;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The source clock. Should generally not be used for any timekeeping purposes.
|
|
||||||
/// </summary>
|
|
||||||
public IClock SourceClock { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked when a seek has been performed via <see cref="Seek"/>
|
/// Invoked when a seek has been performed via <see cref="Seek"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -60,15 +55,14 @@ namespace osu.Game.Screens.Play
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sourceClock">The source <see cref="IClock"/> used for timing.</param>
|
/// <param name="sourceClock">The source <see cref="IClock"/> used for timing.</param>
|
||||||
/// <param name="applyOffsets">Whether to apply platform, user and beatmap offsets to the mix.</param>
|
/// <param name="applyOffsets">Whether to apply platform, user and beatmap offsets to the mix.</param>
|
||||||
public GameplayClockContainer(IClock sourceClock, bool applyOffsets = false)
|
/// <param name="requireDecoupling">Whether decoupling logic should be applied on the source clock.</param>
|
||||||
|
public GameplayClockContainer(IClock sourceClock, bool applyOffsets, bool requireDecoupling)
|
||||||
{
|
{
|
||||||
SourceClock = sourceClock;
|
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
GameplayClock = new FramedBeatmapClock(applyOffsets) { IsCoupled = false },
|
GameplayClock = new FramedBeatmapClock(applyOffsets, requireDecoupling, sourceClock),
|
||||||
Content
|
Content
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -83,8 +77,6 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
isPaused.Value = false;
|
isPaused.Value = false;
|
||||||
|
|
||||||
ensureSourceClockSet();
|
|
||||||
|
|
||||||
PrepareStart();
|
PrepareStart();
|
||||||
|
|
||||||
// The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time.
|
// The case which caused this to be added is FrameStabilityContainer, which manages its own current and elapsed time.
|
||||||
@ -153,28 +145,11 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
Stop();
|
Stop();
|
||||||
|
|
||||||
ensureSourceClockSet();
|
|
||||||
|
|
||||||
if (time != null)
|
if (time != null)
|
||||||
StartTime = time.Value;
|
StartTime = time.Value;
|
||||||
|
|
||||||
Seek(StartTime);
|
Seek(StartTime);
|
||||||
|
|
||||||
// This is a workaround for the fact that DecoupleableInterpolatingFramedClock doesn't seek the source
|
|
||||||
// if the source is not IsRunning. (see https://github.com/ppy/osu-framework/blob/2102638056dfcf85d21b4d85266d53b5dd018767/osu.Framework/Timing/DecoupleableInterpolatingFramedClock.cs#L209-L210)
|
|
||||||
// I hope to remove this once we knock some sense into clocks in general.
|
|
||||||
//
|
|
||||||
// Without this seek, the multiplayer spectator start sequence breaks:
|
|
||||||
// - Individual clients' clocks are never updated to their expected time
|
|
||||||
// - The sync manager thinks they are running behind
|
|
||||||
// - Gameplay doesn't start when it should (until a timeout occurs because nothing is happening for 10+ seconds)
|
|
||||||
//
|
|
||||||
// In addition, we use `CurrentTime` for this seek instead of `StartTime` as the above seek may have applied inherent
|
|
||||||
// offsets which need to be accounted for (ie. FramedBeatmapClock.TotalAppliedOffset).
|
|
||||||
//
|
|
||||||
// See https://github.com/ppy/osu/pull/24451/files/87fee001c786b29db34063ef3350e9a9f024d3ab#diff-28ca02979641e2d98a15fe5d5e806f56acf60ac100258a059fa72503b6cc54e8.
|
|
||||||
(SourceClock as IAdjustableClock)?.Seek(CurrentTime);
|
|
||||||
|
|
||||||
if (!wasPaused || startClock)
|
if (!wasPaused || startClock)
|
||||||
Start();
|
Start();
|
||||||
}
|
}
|
||||||
@ -183,20 +158,7 @@ namespace osu.Game.Screens.Play
|
|||||||
/// Changes the source clock.
|
/// Changes the source clock.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sourceClock">The new source.</param>
|
/// <param name="sourceClock">The new source.</param>
|
||||||
protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(SourceClock = sourceClock);
|
protected void ChangeSource(IClock sourceClock) => GameplayClock.ChangeSource(sourceClock);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures that the <see cref="GameplayClock"/> is set to <see cref="SourceClock"/>, if it hasn't been given a source yet.
|
|
||||||
/// This is usually done before a seek to avoid accidentally seeking only the adjustable source in decoupled mode,
|
|
||||||
/// but not the actual source clock.
|
|
||||||
/// That will pretty much only happen on the very first call of this method, as the source clock is passed in the constructor,
|
|
||||||
/// but it is not yet set on the adjustable source there.
|
|
||||||
/// </summary>
|
|
||||||
private void ensureSourceClockSet()
|
|
||||||
{
|
|
||||||
if (GameplayClock.Source == null)
|
|
||||||
ChangeSource(SourceClock);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IAdjustableClock
|
#region IAdjustableClock
|
||||||
|
|
||||||
|
@ -5,14 +5,16 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Lines;
|
using osu.Framework.Graphics.Lines;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Layout;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -27,6 +29,22 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
public bool UsesFixedAnchor { get; set; }
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
|
[SettingSource("Bar height")]
|
||||||
|
public BindableFloat BarHeight { get; } = new BindableFloat(20)
|
||||||
|
{
|
||||||
|
MinValue = 0,
|
||||||
|
MaxValue = 64,
|
||||||
|
Precision = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
[SettingSource("Bar length")]
|
||||||
|
public BindableFloat BarLength { get; } = new BindableFloat(0.98f)
|
||||||
|
{
|
||||||
|
MinValue = 0.2f,
|
||||||
|
MaxValue = 1,
|
||||||
|
Precision = 0.01f,
|
||||||
|
};
|
||||||
|
|
||||||
private BarPath mainBar = null!;
|
private BarPath mainBar = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -46,7 +64,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
private readonly List<Vector2> missBarVertices = new List<Vector2>();
|
private readonly List<Vector2> missBarVertices = new List<Vector2>();
|
||||||
private readonly List<Vector2> healthBarVertices = new List<Vector2>();
|
private readonly List<Vector2> healthBarVertices = new List<Vector2>();
|
||||||
|
|
||||||
private double glowBarValue = 1;
|
private double glowBarValue;
|
||||||
|
|
||||||
public double GlowBarValue
|
public double GlowBarValue
|
||||||
{
|
{
|
||||||
@ -61,7 +79,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private double healthBarValue = 1;
|
private double healthBarValue;
|
||||||
|
|
||||||
public double HealthBarValue
|
public double HealthBarValue
|
||||||
{
|
{
|
||||||
@ -76,59 +94,46 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const float main_path_radius = 10f;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.X;
|
||||||
|
AutoSizeAxes = Axes.Y;
|
||||||
|
|
||||||
InternalChild = new FillFlowContainer
|
InternalChild = new Container
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
AutoSizeAxes = Axes.Both,
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
Spacing = new Vector2(4f, 0f),
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new Circle
|
background = new BackgroundPath
|
||||||
{
|
{
|
||||||
Margin = new MarginPadding { Top = 8.5f, Left = -2 },
|
PathRadius = main_path_radius,
|
||||||
Size = new Vector2(50f, 3f),
|
|
||||||
},
|
},
|
||||||
new Container
|
glowBar = new BarPath
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
BarColour = Color4.White,
|
||||||
Children = new Drawable[]
|
GlowColour = OsuColour.Gray(0.5f),
|
||||||
{
|
Blending = BlendingParameters.Additive,
|
||||||
background = new BackgroundPath
|
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White),
|
||||||
{
|
PathRadius = 40f,
|
||||||
PathRadius = 10f,
|
// Kinda hacky, but results in correct positioning with increased path radius.
|
||||||
},
|
Margin = new MarginPadding(-30f),
|
||||||
glowBar = new BarPath
|
GlowPortion = 0.9f,
|
||||||
{
|
},
|
||||||
BarColour = Color4.White,
|
mainBar = new BarPath
|
||||||
GlowColour = OsuColour.Gray(0.5f),
|
{
|
||||||
Blending = BlendingParameters.Additive,
|
AutoSizeAxes = Axes.None,
|
||||||
Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White),
|
RelativeSizeAxes = Axes.Both,
|
||||||
PathRadius = 40f,
|
Blending = BlendingParameters.Additive,
|
||||||
// Kinda hacky, but results in correct positioning with increased path radius.
|
BarColour = main_bar_colour,
|
||||||
Margin = new MarginPadding(-30f),
|
GlowColour = main_bar_glow_colour,
|
||||||
GlowPortion = 0.9f,
|
PathRadius = main_path_radius,
|
||||||
},
|
GlowPortion = 0.6f,
|
||||||
mainBar = new BarPath
|
},
|
||||||
{
|
}
|
||||||
AutoSizeAxes = Axes.None,
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Blending = BlendingParameters.Additive,
|
|
||||||
BarColour = main_bar_colour,
|
|
||||||
GlowColour = main_bar_glow_colour,
|
|
||||||
PathRadius = 10f,
|
|
||||||
GlowPortion = 0.6f,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
updatePath();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
@ -140,10 +145,24 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
if (v.NewValue >= GlowBarValue)
|
if (v.NewValue >= GlowBarValue)
|
||||||
finishMissDisplay();
|
finishMissDisplay();
|
||||||
|
|
||||||
this.TransformTo(nameof(HealthBarValue), v.NewValue, 300, Easing.OutQuint);
|
double time = v.NewValue > GlowBarValue ? 500 : 250;
|
||||||
|
|
||||||
|
this.TransformTo(nameof(HealthBarValue), v.NewValue, time, Easing.OutQuint);
|
||||||
if (resetMissBarDelegate == null)
|
if (resetMissBarDelegate == null)
|
||||||
this.TransformTo(nameof(GlowBarValue), v.NewValue, 300, Easing.OutQuint);
|
this.TransformTo(nameof(GlowBarValue), v.NewValue, time, Easing.OutQuint);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
BarLength.BindValueChanged(l => Width = l.NewValue, true);
|
||||||
|
BarHeight.BindValueChanged(_ => updatePath());
|
||||||
|
updatePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source)
|
||||||
|
{
|
||||||
|
if ((invalidation & Invalidation.DrawSize) > 0)
|
||||||
|
updatePath();
|
||||||
|
|
||||||
|
return base.OnInvalidate(invalidation, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -214,25 +233,24 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
|
|
||||||
private void updatePath()
|
private void updatePath()
|
||||||
{
|
{
|
||||||
const float curve_start = 280;
|
float barLength = DrawWidth - main_path_radius * 2;
|
||||||
const float curve_end = 310;
|
float curveStart = barLength - 70;
|
||||||
|
float curveEnd = barLength - 40;
|
||||||
|
|
||||||
const float curve_smoothness = 10;
|
const float curve_smoothness = 10;
|
||||||
|
|
||||||
const float bar_length = 350;
|
Vector2 diagonalDir = (new Vector2(curveEnd, BarHeight.Value) - new Vector2(curveStart, 0)).Normalized();
|
||||||
const float bar_verticality = 32.5f;
|
|
||||||
|
|
||||||
Vector2 diagonalDir = (new Vector2(curve_end, bar_verticality) - new Vector2(curve_start, 0)).Normalized();
|
|
||||||
|
|
||||||
barPath = new SliderPath(new[]
|
barPath = new SliderPath(new[]
|
||||||
{
|
{
|
||||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||||
new PathControlPoint(new Vector2(curve_start - curve_smoothness, 0), PathType.Bezier),
|
new PathControlPoint(new Vector2(curveStart - curve_smoothness, 0), PathType.Bezier),
|
||||||
new PathControlPoint(new Vector2(curve_start, 0)),
|
new PathControlPoint(new Vector2(curveStart, 0)),
|
||||||
new PathControlPoint(new Vector2(curve_start, 0) + diagonalDir * curve_smoothness, PathType.Linear),
|
new PathControlPoint(new Vector2(curveStart, 0) + diagonalDir * curve_smoothness, PathType.Linear),
|
||||||
new PathControlPoint(new Vector2(curve_end, bar_verticality) - diagonalDir * curve_smoothness, PathType.Bezier),
|
new PathControlPoint(new Vector2(curveEnd, BarHeight.Value) - diagonalDir * curve_smoothness, PathType.Bezier),
|
||||||
new PathControlPoint(new Vector2(curve_end, bar_verticality)),
|
new PathControlPoint(new Vector2(curveEnd, BarHeight.Value)),
|
||||||
new PathControlPoint(new Vector2(curve_end + curve_smoothness, bar_verticality), PathType.Linear),
|
new PathControlPoint(new Vector2(curveEnd + curve_smoothness, BarHeight.Value), PathType.Linear),
|
||||||
new PathControlPoint(new Vector2(bar_length, bar_verticality)),
|
new PathControlPoint(new Vector2(barLength, BarHeight.Value)),
|
||||||
});
|
});
|
||||||
|
|
||||||
List<Vector2> vertices = new List<Vector2>();
|
List<Vector2> vertices = new List<Vector2>();
|
||||||
@ -267,7 +285,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
protected override Color4 ColourAt(float position)
|
protected override Color4 ColourAt(float position)
|
||||||
{
|
{
|
||||||
if (position <= 0.128f)
|
if (position <= 0.16f)
|
||||||
return Color4.White.Opacity(0.8f);
|
return Color4.White.Opacity(0.8f);
|
||||||
|
|
||||||
return Interpolation.ValueAt(position,
|
return Interpolation.ValueAt(position,
|
||||||
|
@ -29,6 +29,8 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Bindable<bool> ShowHealth = new Bindable<bool>();
|
public readonly Bindable<bool> ShowHealth = new Bindable<bool>();
|
||||||
|
|
||||||
|
protected override bool PlayInitialIncreaseAnimation => false;
|
||||||
|
|
||||||
private const float max_alpha = 0.4f;
|
private const float max_alpha = 0.4f;
|
||||||
private const int fade_time = 400;
|
private const int fade_time = 400;
|
||||||
private const float gradient_size = 0.2f;
|
private const float gradient_size = 0.2f;
|
||||||
|
@ -6,7 +6,9 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
@ -23,12 +25,18 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
protected HealthProcessor HealthProcessor { get; private set; } = null!;
|
protected HealthProcessor HealthProcessor { get; private set; } = null!;
|
||||||
|
|
||||||
public Bindable<double> Current { get; } = new BindableDouble(1)
|
protected virtual bool PlayInitialIncreaseAnimation => true;
|
||||||
|
|
||||||
|
public Bindable<double> Current { get; } = new BindableDouble
|
||||||
{
|
{
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 1
|
MaxValue = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private BindableNumber<double> health = null!;
|
||||||
|
|
||||||
|
private ScheduledDelegate? initialIncrease;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Triggered when a <see cref="Judgement"/> is a successful hit, signaling the health display to perform a flash animation (if designed to do so).
|
/// Triggered when a <see cref="Judgement"/> is a successful hit, signaling the health display to perform a flash animation (if designed to do so).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -52,14 +60,56 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Current.BindTo(HealthProcessor.Health);
|
|
||||||
HealthProcessor.NewJudgement += onNewJudgement;
|
HealthProcessor.NewJudgement += onNewJudgement;
|
||||||
|
|
||||||
|
// Don't bind directly so we can animate the startup procedure.
|
||||||
|
health = HealthProcessor.Health.GetBoundCopy();
|
||||||
|
health.BindValueChanged(h =>
|
||||||
|
{
|
||||||
|
finishInitialAnimation();
|
||||||
|
Current.Value = h.NewValue;
|
||||||
|
});
|
||||||
|
|
||||||
if (hudOverlay != null)
|
if (hudOverlay != null)
|
||||||
showHealthBar.BindTo(hudOverlay.ShowHealthBar);
|
showHealthBar.BindTo(hudOverlay.ShowHealthBar);
|
||||||
|
|
||||||
// this probably shouldn't be operating on `this.`
|
// this probably shouldn't be operating on `this.`
|
||||||
showHealthBar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true);
|
showHealthBar.BindValueChanged(healthBar => this.FadeTo(healthBar.NewValue ? 1 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING), true);
|
||||||
|
|
||||||
|
if (PlayInitialIncreaseAnimation)
|
||||||
|
startInitialAnimation();
|
||||||
|
else
|
||||||
|
Current.Value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startInitialAnimation()
|
||||||
|
{
|
||||||
|
// TODO: this should run in gameplay time, including showing a larger increase when skipping.
|
||||||
|
// TODO: it should also start increasing relative to the first hitobject.
|
||||||
|
const double increase_delay = 150;
|
||||||
|
|
||||||
|
initialIncrease = Scheduler.AddDelayed(() =>
|
||||||
|
{
|
||||||
|
double newValue = Current.Value + 0.05f;
|
||||||
|
this.TransformBindableTo(Current, newValue, increase_delay);
|
||||||
|
Flash(new JudgementResult(new HitObject(), new Judgement()));
|
||||||
|
|
||||||
|
if (newValue >= 1)
|
||||||
|
finishInitialAnimation();
|
||||||
|
}, increase_delay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishInitialAnimation()
|
||||||
|
{
|
||||||
|
initialIncrease?.Cancel();
|
||||||
|
initialIncrease = null;
|
||||||
|
|
||||||
|
// aside from the repeating `initialIncrease` scheduled task,
|
||||||
|
// there may also be a `Current` transform in progress from that schedule.
|
||||||
|
// ensure it plays out fully, to prevent changes to `Current.Value` being discarded by the ongoing transform.
|
||||||
|
// and yes, this funky `targetMember` spec is seemingly the only way to do this
|
||||||
|
// (see: https://github.com/ppy/osu-framework/blob/fe2769171c6e26d1b6fdd6eb7ea8353162fe9065/osu.Framework/Graphics/Transforms/TransformBindable.cs#L21)
|
||||||
|
FinishTransforms(targetMember: $"{Current.GetHashCode()}.{nameof(Current.Value)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onNewJudgement(JudgementResult judgement)
|
private void onNewJudgement(JudgementResult judgement)
|
||||||
|
@ -22,16 +22,16 @@ namespace osu.Game.Screens.Play.HUD.JudgementCounter
|
|||||||
public bool UsesFixedAnchor { get; set; }
|
public bool UsesFixedAnchor { get; set; }
|
||||||
|
|
||||||
[SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayMode))]
|
[SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.JudgementDisplayMode))]
|
||||||
public Bindable<DisplayMode> Mode { get; set; } = new Bindable<DisplayMode>();
|
public Bindable<DisplayMode> Mode { get; } = new Bindable<DisplayMode>();
|
||||||
|
|
||||||
[SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.FlowDirection))]
|
[SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.FlowDirection))]
|
||||||
public Bindable<Direction> FlowDirection { get; set; } = new Bindable<Direction>();
|
public Bindable<Direction> FlowDirection { get; } = new Bindable<Direction>();
|
||||||
|
|
||||||
[SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.ShowJudgementNames))]
|
[SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.ShowJudgementNames))]
|
||||||
public BindableBool ShowJudgementNames { get; set; } = new BindableBool(true);
|
public BindableBool ShowJudgementNames { get; } = new BindableBool(true);
|
||||||
|
|
||||||
[SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.ShowMaxJudgement))]
|
[SettingSource(typeof(JudgementCounterDisplayStrings), nameof(JudgementCounterDisplayStrings.ShowMaxJudgement))]
|
||||||
public BindableBool ShowMaxJudgement { get; set; } = new BindableBool(true);
|
public BindableBool ShowMaxJudgement { get; } = new BindableBool(true);
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private JudgementCountController judgementCountController { get; set; } = null!;
|
private JudgementCountController judgementCountController { get; set; } = null!;
|
||||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
{
|
{
|
||||||
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription),
|
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.CornerRadius), nameof(SkinnableComponentStrings.CornerRadiusDescription),
|
||||||
SettingControlType = typeof(SettingsPercentageSlider<float>))]
|
SettingControlType = typeof(SettingsPercentageSlider<float>))]
|
||||||
public new BindableFloat CornerRadius { get; set; } = new BindableFloat(0.25f)
|
public new BindableFloat CornerRadius { get; } = new BindableFloat(0.25f)
|
||||||
{
|
{
|
||||||
MinValue = 0,
|
MinValue = 0,
|
||||||
MaxValue = 0.5f,
|
MaxValue = 0.5f,
|
||||||
|
@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play
|
|||||||
/// <param name="beatmap">The beatmap to be used for time and metadata references.</param>
|
/// <param name="beatmap">The beatmap to be used for time and metadata references.</param>
|
||||||
/// <param name="skipTargetTime">The latest time which should be used when introducing gameplay. Will be used when skipping forward.</param>
|
/// <param name="skipTargetTime">The latest time which should be used when introducing gameplay. Will be used when skipping forward.</param>
|
||||||
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime)
|
public MasterGameplayClockContainer(WorkingBeatmap beatmap, double skipTargetTime)
|
||||||
: base(beatmap.Track, true)
|
: base(beatmap.Track, applyOffsets: true, requireDecoupling: true)
|
||||||
{
|
{
|
||||||
this.beatmap = beatmap;
|
this.beatmap = beatmap;
|
||||||
this.skipTargetTime = skipTargetTime;
|
this.skipTargetTime = skipTargetTime;
|
||||||
@ -187,7 +187,13 @@ namespace osu.Game.Screens.Play
|
|||||||
public void StopUsingBeatmapClock()
|
public void StopUsingBeatmapClock()
|
||||||
{
|
{
|
||||||
removeSourceClockAdjustments();
|
removeSourceClockAdjustments();
|
||||||
ChangeSource(new TrackVirtual(beatmap.Track.Length));
|
|
||||||
|
var virtualTrack = new TrackVirtual(beatmap.Track.Length);
|
||||||
|
virtualTrack.Seek(CurrentTime);
|
||||||
|
if (IsRunning)
|
||||||
|
virtualTrack.Start();
|
||||||
|
ChangeSource(virtualTrack);
|
||||||
|
|
||||||
addSourceClockAdjustments();
|
addSourceClockAdjustments();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,7 +257,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
rulesetSkinProvider.AddRange(new Drawable[]
|
rulesetSkinProvider.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
failAnimationLayer = new FailAnimation(DrawableRuleset)
|
failAnimationContainer = new FailAnimationContainer(DrawableRuleset)
|
||||||
{
|
{
|
||||||
OnComplete = onFailComplete,
|
OnComplete = onFailComplete,
|
||||||
Children = new[]
|
Children = new[]
|
||||||
@ -310,7 +310,7 @@ namespace osu.Game.Screens.Play
|
|||||||
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
|
// add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components.
|
||||||
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
|
// also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.)
|
||||||
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
|
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
|
||||||
failAnimationLayer.Add(createOverlayComponents(Beatmap.Value));
|
failAnimationContainer.Add(createOverlayComponents(Beatmap.Value));
|
||||||
|
|
||||||
if (!DrawableRuleset.AllowGameplayOverlays)
|
if (!DrawableRuleset.AllowGameplayOverlays)
|
||||||
{
|
{
|
||||||
@ -587,7 +587,7 @@ namespace osu.Game.Screens.Play
|
|||||||
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
|
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
|
||||||
if (ValidForResume && GameplayState.HasFailed)
|
if (ValidForResume && GameplayState.HasFailed)
|
||||||
{
|
{
|
||||||
failAnimationLayer.FinishTransforms(true);
|
failAnimationContainer.FinishTransforms(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -888,7 +888,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
protected FailOverlay FailOverlay { get; private set; }
|
protected FailOverlay FailOverlay { get; private set; }
|
||||||
|
|
||||||
private FailAnimation failAnimationLayer;
|
private FailAnimationContainer failAnimationContainer;
|
||||||
|
|
||||||
private bool onFail()
|
private bool onFail()
|
||||||
{
|
{
|
||||||
@ -913,7 +913,7 @@ namespace osu.Game.Screens.Play
|
|||||||
if (PauseOverlay.State.Value == Visibility.Visible)
|
if (PauseOverlay.State.Value == Visibility.Visible)
|
||||||
PauseOverlay.Hide();
|
PauseOverlay.Hide();
|
||||||
|
|
||||||
failAnimationLayer.Start();
|
failAnimationContainer.Start();
|
||||||
|
|
||||||
if (GameplayState.Mods.OfType<IApplicableFailOverride>().Any(m => m.RestartOnFail))
|
if (GameplayState.Mods.OfType<IApplicableFailOverride>().Any(m => m.RestartOnFail))
|
||||||
Restart(true);
|
Restart(true);
|
||||||
@ -1044,7 +1044,7 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
|
b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
|
||||||
|
|
||||||
failAnimationLayer.Background = b;
|
failAnimationContainer.Background = b;
|
||||||
});
|
});
|
||||||
|
|
||||||
HUDOverlay.IsPlaying.BindTo(localUserPlaying);
|
HUDOverlay.IsPlaying.BindTo(localUserPlaying);
|
||||||
@ -1099,7 +1099,7 @@ namespace osu.Game.Screens.Play
|
|||||||
screenSuspension?.RemoveAndDisposeImmediately();
|
screenSuspension?.RemoveAndDisposeImmediately();
|
||||||
|
|
||||||
// Eagerly clean these up as disposal of child components is asynchronous and may leave sounds playing beyond user expectations.
|
// Eagerly clean these up as disposal of child components is asynchronous and may leave sounds playing beyond user expectations.
|
||||||
failAnimationLayer?.Stop();
|
failAnimationContainer?.Stop();
|
||||||
PauseOverlay?.StopAllSamples();
|
PauseOverlay?.StopAllSamples();
|
||||||
|
|
||||||
if (LoadedBeatmapSuccessfully)
|
if (LoadedBeatmapSuccessfully)
|
||||||
|
@ -138,7 +138,7 @@ namespace osu.Game.Screens.Select
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
internal IOverlayManager? OverlayManager { get; private set; }
|
internal IOverlayManager? OverlayManager { get; private set; }
|
||||||
|
|
||||||
private Bindable<bool> configBackgroundBlur { get; set; } = new BindableBool();
|
private Bindable<bool> configBackgroundBlur = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config)
|
private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog? manageCollectionsDialog, DifficultyRecommender? recommender, OsuConfigManager config)
|
||||||
|
@ -109,6 +109,7 @@ namespace osu.Game.Skinning
|
|||||||
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
case SkinComponentsContainerLookup.TargetArea.MainHUDComponents:
|
||||||
var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container =>
|
var skinnableTargetWrapper = new DefaultSkinComponentsContainer(container =>
|
||||||
{
|
{
|
||||||
|
var health = container.OfType<ArgonHealthDisplay>().FirstOrDefault();
|
||||||
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
|
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
|
||||||
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
|
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
|
||||||
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
|
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
|
||||||
@ -128,6 +129,13 @@ namespace osu.Game.Skinning
|
|||||||
|
|
||||||
score.Position = new Vector2(0, vertical_offset);
|
score.Position = new Vector2(0, vertical_offset);
|
||||||
|
|
||||||
|
if (health != null)
|
||||||
|
{
|
||||||
|
health.Origin = Anchor.TopCentre;
|
||||||
|
health.Anchor = Anchor.TopCentre;
|
||||||
|
health.Y = 5;
|
||||||
|
}
|
||||||
|
|
||||||
if (ppCounter != null)
|
if (ppCounter != null)
|
||||||
{
|
{
|
||||||
ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4;
|
ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4;
|
||||||
@ -191,7 +199,7 @@ namespace osu.Game.Skinning
|
|||||||
new DefaultComboCounter(),
|
new DefaultComboCounter(),
|
||||||
new DefaultScoreCounter(),
|
new DefaultScoreCounter(),
|
||||||
new DefaultAccuracyCounter(),
|
new DefaultAccuracyCounter(),
|
||||||
new DefaultHealthDisplay(),
|
new ArgonHealthDisplay(),
|
||||||
new ArgonSongProgress(),
|
new ArgonSongProgress(),
|
||||||
new ArgonKeyCounterDisplay(),
|
new ArgonKeyCounterDisplay(),
|
||||||
new BarHitErrorMeter(),
|
new BarHitErrorMeter(),
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Skinning.Components
|
|||||||
public Bindable<BeatmapAttribute> Attribute { get; } = new Bindable<BeatmapAttribute>(BeatmapAttribute.StarRating);
|
public Bindable<BeatmapAttribute> Attribute { get; } = new Bindable<BeatmapAttribute>(BeatmapAttribute.StarRating);
|
||||||
|
|
||||||
[SettingSource(typeof(BeatmapAttributeTextStrings), nameof(BeatmapAttributeTextStrings.Template), nameof(BeatmapAttributeTextStrings.TemplateDescription))]
|
[SettingSource(typeof(BeatmapAttributeTextStrings), nameof(BeatmapAttributeTextStrings.Template), nameof(BeatmapAttributeTextStrings.TemplateDescription))]
|
||||||
public Bindable<string> Template { get; set; } = new Bindable<string>("{Label}: {Value}");
|
public Bindable<string> Template { get; } = new Bindable<string>("{Label}: {Value}");
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||||
|
@ -66,6 +66,7 @@ namespace osu.Game.Skinning
|
|||||||
marker.Current.BindTo(Current);
|
marker.Current.BindTo(Current);
|
||||||
|
|
||||||
maxFillWidth = fill.Width;
|
maxFillWidth = fill.Width;
|
||||||
|
fill.Width = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Animations;
|
using osu.Framework.Graphics.Animations;
|
||||||
|
using osu.Framework.Graphics.Primitives;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -112,9 +113,11 @@ namespace osu.Game.Skinning
|
|||||||
if (texture.DisplayWidth <= maxSize.X && texture.DisplayHeight <= maxSize.Y)
|
if (texture.DisplayWidth <= maxSize.X && texture.DisplayHeight <= maxSize.Y)
|
||||||
return texture;
|
return texture;
|
||||||
|
|
||||||
// use scale adjust property for downscaling the texture in order to meet the specified maximum dimensions.
|
maxSize *= texture.ScaleAdjust;
|
||||||
texture.ScaleAdjust *= Math.Max(texture.DisplayWidth / maxSize.X, texture.DisplayHeight / maxSize.Y);
|
|
||||||
return texture;
|
var croppedTexture = texture.Crop(new RectangleF(texture.Width / 2f - maxSize.X / 2f, texture.Height / 2f - maxSize.Y / 2f, maxSize.X, maxSize.Y));
|
||||||
|
croppedTexture.ScaleAdjust = texture.ScaleAdjust;
|
||||||
|
return croppedTexture;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool HasFont(this ISkin source, LegacyFont font)
|
public static bool HasFont(this ISkin source, LegacyFont font)
|
||||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Skinning
|
|||||||
}
|
}
|
||||||
|
|
||||||
Samples = samples;
|
Samples = samples;
|
||||||
Textures = new TextureStore(resources.Renderer, new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage)));
|
Textures = new TextureStore(resources.Renderer, CreateTextureLoaderStore(resources, storage));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -171,6 +171,9 @@ namespace osu.Game.Skinning
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual IResourceStore<TextureUpload> CreateTextureLoaderStore(IStorageResourceProvider resources, IResourceStore<byte[]> storage)
|
||||||
|
=> new MaxDimensionLimitedTextureLoaderStore(resources.CreateTextureLoaderStore(storage));
|
||||||
|
|
||||||
protected virtual void ParseConfigurationStream(Stream stream)
|
protected virtual void ParseConfigurationStream(Stream stream)
|
||||||
{
|
{
|
||||||
using (LineBufferedReader reader = new LineBufferedReader(stream, true))
|
using (LineBufferedReader reader = new LineBufferedReader(stream, true))
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="11.5.0" />
|
<PackageReference Include="Realm" Version="11.5.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2023.922.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2023.1006.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1003.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1003.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.39.1" />
|
<PackageReference Include="Sentry" Version="3.39.1" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||||
|
@ -23,6 +23,6 @@
|
|||||||
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.922.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.1006.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
Loading…
Reference in New Issue
Block a user