diff --git a/.github/workflows/_diffcalc_processor.yml b/.github/workflows/_diffcalc_processor.yml
new file mode 100644
index 0000000000..4e221d0550
--- /dev/null
+++ b/.github/workflows/_diffcalc_processor.yml
@@ -0,0 +1,228 @@
+name: "🔒diffcalc (do not use)"
+
+on:
+ workflow_call:
+ inputs:
+ id:
+ type: string
+ head-sha:
+ type: string
+ pr-url:
+ type: string
+ pr-text:
+ type: string
+ dispatch-inputs:
+ type: string
+ outputs:
+ target:
+ description: The comparison target.
+ value: ${{ jobs.generator.outputs.target }}
+ sheet:
+ description: The comparison spreadsheet.
+ value: ${{ jobs.generator.outputs.sheet }}
+ secrets:
+ DIFFCALC_GOOGLE_CREDENTIALS:
+ required: true
+
+env:
+ GENERATOR_DIR: ${{ github.workspace }}/${{ inputs.id }}
+ GENERATOR_ENV: ${{ github.workspace }}/${{ inputs.id }}/.env
+
+defaults:
+ run:
+ shell: bash -euo pipefail {0}
+
+jobs:
+ generator:
+ name: Run
+ runs-on: self-hosted
+ timeout-minutes: 720
+
+ outputs:
+ target: ${{ steps.run.outputs.target }}
+ sheet: ${{ steps.run.outputs.sheet }}
+
+ steps:
+ - name: Checkout diffcalc-sheet-generator
+ uses: actions/checkout@v4
+ with:
+ path: ${{ inputs.id }}
+ repository: 'smoogipoo/diffcalc-sheet-generator'
+
+ - name: Add base environment
+ env:
+ GOOGLE_CREDS_FILE: ${{ github.workspace }}/${{ inputs.id }}/google-credentials.json
+ VARS_JSON: ${{ (vars != null && toJSON(vars)) || '' }}
+ run: |
+ # Required by diffcalc-sheet-generator
+ cp '${{ env.GENERATOR_DIR }}/.env.sample' "${{ env.GENERATOR_ENV }}"
+
+ # Add Google credentials
+ echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ env.GOOGLE_CREDS_FILE }}"
+
+ # Add repository variables
+ 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};" "${{ env.GENERATOR_ENV }}"
+ fi
+ done
+
+ - name: Add HEAD environment
+ run: |
+ sed -i "s;^OSU_A=.*$;OSU_A=${{ inputs.head-sha }};" "${{ env.GENERATOR_ENV }}"
+
+ - name: Add pull-request environment
+ if: ${{ inputs.pr-url != '' }}
+ run: |
+ sed -i "s;^OSU_B=.*$;OSU_B=${{ inputs.pr-url }};" "${{ env.GENERATOR_ENV }}"
+
+ - name: Add comment environment
+ if: ${{ inputs.pr-text != '' }}
+ env:
+ PR_TEXT: ${{ inputs.pr-text }}
+ run: |
+ # Add comment environment
+ echo "${PR_TEXT}" | sed -r 's/\r$//' | { grep -E '^\w+=' || true; } | while read -r line; do
+ opt=$(echo "${line}" | cut -d '=' -f1)
+ sed -i "s;^${opt}=.*$;${line};" "${{ env.GENERATOR_ENV }}"
+ done
+
+ - name: Add dispatch environment
+ if: ${{ inputs.dispatch-inputs != '' }}
+ env:
+ DISPATCH_INPUTS_JSON: ${{ inputs.dispatch-inputs }}
+ run: |
+ function get_input() {
+ echo "${DISPATCH_INPUTS_JSON}" | jq -r ".\"$1\""
+ }
+
+ osu_a=$(get_input 'osu-a')
+ osu_b=$(get_input 'osu-b')
+ ruleset=$(get_input 'ruleset')
+ generators=$(get_input 'generators')
+ difficulty_calculator_a=$(get_input 'difficulty-calculator-a')
+ difficulty_calculator_b=$(get_input 'difficulty-calculator-b')
+ score_processor_a=$(get_input 'score-processor-a')
+ score_processor_b=$(get_input 'score-processor-b')
+ converts=$(get_input 'converts')
+ ranked_only=$(get_input 'ranked-only')
+
+ sed -i "s;^OSU_B=.*$;OSU_B=${osu_b};" "${{ env.GENERATOR_ENV }}"
+ sed -i "s/^RULESET=.*$/RULESET=${ruleset}/" "${{ env.GENERATOR_ENV }}"
+ sed -i "s/^GENERATORS=.*$/GENERATORS=${generators}/" "${{ env.GENERATOR_ENV }}"
+
+ if [[ "${osu_a}" != 'latest' ]]; then
+ sed -i "s;^OSU_A=.*$;OSU_A=${osu_a};" "${{ env.GENERATOR_ENV }}"
+ fi
+
+ if [[ "${difficulty_calculator_a}" != 'latest' ]]; then
+ sed -i "s;^DIFFICULTY_CALCULATOR_A=.*$;DIFFICULTY_CALCULATOR_A=${difficulty_calculator_a};" "${{ env.GENERATOR_ENV }}"
+ fi
+
+ if [[ "${difficulty_calculator_b}" != 'latest' ]]; then
+ sed -i "s;^DIFFICULTY_CALCULATOR_B=.*$;DIFFICULTY_CALCULATOR_B=${difficulty_calculator_b};" "${{ env.GENERATOR_ENV }}"
+ fi
+
+ if [[ "${score_processor_a}" != 'latest' ]]; then
+ sed -i "s;^SCORE_PROCESSOR_A=.*$;SCORE_PROCESSOR_A=${score_processor_a};" "${{ env.GENERATOR_ENV }}"
+ fi
+
+ if [[ "${score_processor_b}" != 'latest' ]]; then
+ sed -i "s;^SCORE_PROCESSOR_B=.*$;SCORE_PROCESSOR_B=${score_processor_b};" "${{ env.GENERATOR_ENV }}"
+ fi
+
+ if [[ "${converts}" == 'true' ]]; then
+ sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=0/' "${{ env.GENERATOR_ENV }}"
+ else
+ sed -i 's/^NO_CONVERTS=.*$/NO_CONVERTS=1/' "${{ env.GENERATOR_ENV }}"
+ fi
+
+ if [[ "${ranked_only}" == 'true' ]]; then
+ sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=1/' "${{ env.GENERATOR_ENV }}"
+ else
+ sed -i 's/^RANKED_ONLY=.*$/RANKED_ONLY=0/' "${{ env.GENERATOR_ENV }}"
+ fi
+
+ - name: Query latest scores
+ id: query-scores
+ run: |
+ ruleset=$(cat ${{ env.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=${{ env.GENERATOR_DIR }}/sql/${ruleset}" >> "${GITHUB_OUTPUT}"
+ echo "DATA_NAME=${performance_data_name}" >> "${GITHUB_OUTPUT}"
+ echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
+
+ - name: Restore score cache
+ id: restore-score-cache
+ uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
+ with:
+ path: ${{ steps.query-scores.outputs.DATA_PKG }}
+ key: ${{ steps.query-scores.outputs.DATA_NAME }}
+
+ - name: Download scores
+ if: steps.restore-score-cache.outputs.cache-hit != 'true'
+ run: |
+ wget -q -O "${{ steps.query-scores.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query-scores.outputs.DATA_PKG }}"
+
+ - name: Extract scores
+ run: |
+ tar -I lbzip2 -xf "${{ steps.query-scores.outputs.DATA_PKG }}"
+ rm -r "${{ steps.query-scores.outputs.TARGET_DIR }}"
+ mv "${{ steps.query-scores.outputs.DATA_NAME }}" "${{ steps.query-scores.outputs.TARGET_DIR }}"
+
+ - name: Query latest beatmaps
+ id: query-beatmaps
+ 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=${{ env.GENERATOR_DIR }}/beatmaps" >> "${GITHUB_OUTPUT}"
+ echo "DATA_NAME=${beatmaps_data_name}" >> "${GITHUB_OUTPUT}"
+ echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
+
+ - name: Restore beatmap cache
+ id: restore-beatmap-cache
+ uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
+ with:
+ path: ${{ steps.query-beatmaps.outputs.DATA_PKG }}
+ key: ${{ steps.query-beatmaps.outputs.DATA_NAME }}
+
+ - name: Download beatmap
+ if: steps.restore-beatmap-cache.outputs.cache-hit != 'true'
+ run: |
+ wget -q -O "${{ steps.query-beatmaps.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query-beatmaps.outputs.DATA_PKG }}"
+
+ - name: Extract beatmap
+ run: |
+ tar -I lbzip2 -xf "${{ steps.query-beatmaps.outputs.DATA_PKG }}"
+ rm -r "${{ steps.query-beatmaps.outputs.TARGET_DIR }}"
+ mv "${{ steps.query-beatmaps.outputs.DATA_NAME }}" "${{ steps.query-beatmaps.outputs.TARGET_DIR }}"
+
+ - 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 }}/' "${{ env.GENERATOR_ENV }}"
+
+ cd "${{ env.GENERATOR_DIR }}"
+
+ docker compose up --build --detach
+ docker compose logs --follow &
+ docker compose wait generator
+
+ link=$(docker compose logs --tail 10 generator | grep 'http' | sed -E 's/^.*(http.*)$/\1/')
+ target=$(cat "${{ env.GENERATOR_ENV }}" | grep -E '^OSU_B=' | cut -d '=' -f2-)
+
+ echo "target=${target}" >> "${GITHUB_OUTPUT}"
+ echo "sheet=${link}" >> "${GITHUB_OUTPUT}"
+
+ - name: Shutdown
+ if: ${{ always() }}
+ run: |
+ cd "${{ env.GENERATOR_DIR }}"
+ docker compose down --volumes
+ rm -rf "${{ env.GENERATOR_DIR }}"
diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml
index c2eeff20df..4297a88e89 100644
--- a/.github/workflows/diffcalc.yml
+++ b/.github/workflows/diffcalc.yml
@@ -103,26 +103,11 @@ permissions:
env:
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
+defaults:
+ run:
+ shell: bash -euo pipefail {0}
+
jobs:
- master-environment:
- name: Save master environment
- runs-on: ubuntu-latest
- outputs:
- HEAD: ${{ steps.get-head.outputs.HEAD }}
- steps:
- - name: Checkout osu
- uses: actions/checkout@v4
- with:
- ref: master
- sparse-checkout: |
- README.md
-
- - name: Get HEAD ref
- id: get-head
- run: |
- ref=$(git log -1 --format='%H')
- echo "HEAD=https://github.com/${{ github.repository }}/commit/${ref}" >> "${GITHUB_OUTPUT}"
-
check-permissions:
name: Check permissions
runs-on: ubuntu-latest
@@ -138,9 +123,23 @@ jobs:
done
exit 1
+ run-diffcalc:
+ name: Run spreadsheet generator
+ needs: check-permissions
+ uses: ./.github/workflows/_diffcalc_processor.yml
+ with:
+ # Can't reference env... Why GitHub, WHY?
+ id: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
+ head-sha: https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha || github.sha }}
+ pr-url: ${{ github.event.issue.pull_request.html_url || '' }}
+ pr-text: ${{ github.event.comment.body || '' }}
+ dispatch-inputs: ${{ (github.event.type == 'workflow_dispatch' && toJSON(inputs)) || '' }}
+ secrets:
+ DIFFCALC_GOOGLE_CREDENTIALS: ${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}
+
create-comment:
name: Create PR comment
- needs: [ master-environment, check-permissions ]
+ needs: check-permissions
runs-on: ubuntu-latest
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
steps:
@@ -153,249 +152,34 @@ jobs:
*This comment will update on completion*
- directory:
- name: Prepare directory
- needs: check-permissions
- runs-on: self-hosted
- outputs:
- GENERATOR_DIR: ${{ steps.set-outputs.outputs.GENERATOR_DIR }}
- GENERATOR_ENV: ${{ steps.set-outputs.outputs.GENERATOR_ENV }}
- GOOGLE_CREDS_FILE: ${{ steps.set-outputs.outputs.GOOGLE_CREDS_FILE }}
- steps:
- - name: Checkout diffcalc-sheet-generator
- uses: actions/checkout@v4
- with:
- path: ${{ env.EXECUTION_ID }}
- repository: 'smoogipoo/diffcalc-sheet-generator'
-
- - name: Set outputs
- id: set-outputs
- run: |
- echo "GENERATOR_DIR=${{ github.workspace }}/${{ env.EXECUTION_ID }}" >> "${GITHUB_OUTPUT}"
- echo "GENERATOR_ENV=${{ github.workspace }}/${{ env.EXECUTION_ID }}/.env" >> "${GITHUB_OUTPUT}"
- echo "GOOGLE_CREDS_FILE=${{ github.workspace }}/${{ env.EXECUTION_ID }}/google-credentials.json" >> "${GITHUB_OUTPUT}"
-
- environment:
- name: Setup environment
- needs: [ master-environment, directory ]
- runs-on: self-hosted
- env:
- VARS_JSON: ${{ toJSON(vars) }}
- steps:
- - name: Add base environment
- run: |
- # Required by diffcalc-sheet-generator
- cp '${{ needs.directory.outputs.GENERATOR_DIR }}/.env.sample' "${{ needs.directory.outputs.GENERATOR_ENV }}"
-
- # Add Google credentials
- echo '${{ secrets.DIFFCALC_GOOGLE_CREDENTIALS }}' | base64 -d > "${{ needs.directory.outputs.GOOGLE_CREDS_FILE }}"
-
- # Add repository variables
- 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
-
- - name: Add master environment
- run: |
- sed -i "s;^OSU_A=.*$;OSU_A=${{ needs.master-environment.outputs.HEAD }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
-
- - name: Add pull-request environment
- if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
- run: |
- sed -i "s;^OSU_B=.*$;OSU_B=${{ github.event.issue.pull_request.html_url }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
-
- - name: Add comment environment
- if: ${{ github.event_name == 'issue_comment' }}
- env:
- COMMENT_BODY: ${{ github.event.comment.body }}
- run: |
- # Add comment environment
- echo "$COMMENT_BODY" | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
- opt=$(echo "${line}" | cut -d '=' -f1)
- sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
- done
-
- - name: Add dispatch environment
- if: ${{ github.event_name == 'workflow_dispatch' }}
- run: |
- sed -i 's;^OSU_B=.*$;OSU_B=${{ inputs.osu-b }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
- sed -i 's/^RULESET=.*$/RULESET=${{ inputs.ruleset }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
- sed -i 's/^GENERATORS=.*$/GENERATORS=${{ inputs.generators }}/' "${{ needs.directory.outputs.GENERATOR_ENV }}"
-
- if [[ '${{ inputs.osu-a }}' != 'latest' ]]; then
- sed -i 's;^OSU_A=.*$;OSU_A=${{ inputs.osu-a }};' "${{ needs.directory.outputs.GENERATOR_ENV }}"
- fi
-
- 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
- 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}"
- echo "DATA_PKG=${performance_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
-
- - name: Restore cache
- id: restore-cache
- uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
- with:
- path: ${{ steps.query.outputs.DATA_PKG }}
- key: ${{ steps.query.outputs.DATA_NAME }}
-
- - name: Download
- if: steps.restore-cache.outputs.cache-hit != 'true'
- run: |
- wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
-
- - name: Extract
- run: |
- tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
- 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
- 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}"
- echo "DATA_PKG=${beatmaps_data_name}.tar.bz2" >> "${GITHUB_OUTPUT}"
-
- - name: Restore cache
- id: restore-cache
- uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
- with:
- path: ${{ steps.query.outputs.DATA_PKG }}
- key: ${{ steps.query.outputs.DATA_NAME }}
-
- - name: Download
- if: steps.restore-cache.outputs.cache-hit != 'true'
- run: |
- wget -q -O "${{ steps.query.outputs.DATA_PKG }}" "https://data.ppy.sh/${{ steps.query.outputs.DATA_PKG }}"
-
- - name: Extract
- run: |
- tar -I lbzip2 -xf "${{ steps.query.outputs.DATA_PKG }}"
- 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
- 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 --detach
- docker compose logs --follow &
- docker compose wait generator
-
- link=$(docker compose logs --tail 10 generator | 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 --volumes
-
output-cli:
- name: Output info
- needs: generator
+ name: Info
+ needs: run-diffcalc
runs-on: ubuntu-latest
steps:
- name: Output info
run: |
- echo "Target: ${{ needs.generator.outputs.TARGET }}"
- echo "Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}"
-
- cleanup:
- name: Cleanup
- needs: [ directory, generator ]
- if: ${{ always() && needs.directory.result == 'success' }}
- runs-on: self-hosted
- steps:
- - name: Cleanup
- run: |
- rm -rf "${{ needs.directory.outputs.GENERATOR_DIR }}"
+ echo "Target: ${{ needs.run-diffcalc.outputs.target }}"
+ echo "Spreadsheet: ${{ needs.run-diffcalc.outputs.sheet }}"
update-comment:
name: Update PR comment
- needs: [ create-comment, generator ]
+ needs: [ create-comment, run-diffcalc ]
runs-on: ubuntu-latest
if: ${{ always() && needs.create-comment.result == 'success' }}
steps:
- name: Update comment on success
- if: ${{ needs.generator.result == 'success' }}
+ if: ${{ needs.run-diffcalc.result == 'success' }}
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: recreate
message: |
- Target: ${{ needs.generator.outputs.TARGET }}
- Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
+ Target: ${{ needs.run-diffcalc.outputs.target }}
+ Spreadsheet: ${{ needs.run-diffcalc.outputs.sheet }}
- name: Update comment on failure
- if: ${{ needs.generator.result == 'failure' }}
+ if: ${{ needs.run-diffcalc.result == 'failure' }}
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
@@ -404,7 +188,7 @@ jobs:
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Update comment on cancellation
- if: ${{ needs.generator.result == 'cancelled' }}
+ if: ${{ needs.run-diffcalc.result == 'cancelled' }}
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml
index c44f46d70a..14f0208fc8 100644
--- a/.github/workflows/report-nunit.yml
+++ b/.github/workflows/report-nunit.yml
@@ -5,33 +5,40 @@
name: Annotate CI run with test results
on:
workflow_run:
- workflows: ["Continuous Integration"]
+ workflows: [ "Continuous Integration" ]
types:
- completed
-permissions: {}
+
+permissions:
+ contents: read
+ actions: read
+ checks: write
+
jobs:
annotate:
- permissions:
- checks: write # to create checks (dorny/test-reporter)
-
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
- strategy:
- fail-fast: false
- matrix:
- os:
- - { prettyname: Windows }
- - { prettyname: macOS }
- - { prettyname: Linux }
- threadingMode: ['SingleThread', 'MultiThreaded']
timeout-minutes: 5
steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ repository: ${{ github.event.workflow_run.repository.full_name }}
+ ref: ${{ github.event.workflow_run.head_sha }}
+
+ - name: Download results
+ uses: actions/download-artifact@v4
+ with:
+ pattern: osu-test-results-*
+ merge-multiple: true
+ run-id: ${{ github.event.workflow_run.id }}
+ github-token: ${{ github.token }}
+
- name: Annotate CI run with test results
uses: dorny/test-reporter@v1.8.0
with:
- artifact: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
- name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}})
+ name: Results
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs
index 0b09613ba0..fd372cddc5 100644
--- a/osu.Desktop/NVAPI.cs
+++ b/osu.Desktop/NVAPI.cs
@@ -141,12 +141,12 @@ namespace osu.Desktop
// Make sure that this is a laptop.
IntPtr[] gpus = new IntPtr[64];
- if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount)))
+ if (checkError(EnumPhysicalGPUs(gpus, out int gpuCount), nameof(EnumPhysicalGPUs)))
return false;
for (int i = 0; i < gpuCount; i++)
{
- if (checkError(GetSystemType(gpus[i], out var type)))
+ if (checkError(GetSystemType(gpus[i], out var type), nameof(GetSystemType)))
return false;
if (type == NvSystemType.LAPTOP)
@@ -182,7 +182,7 @@ namespace osu.Desktop
bool success = setSetting(NvSettingID.OGL_THREAD_CONTROL_ID, (uint)value);
- Logger.Log(success ? $"Threaded optimizations set to \"{value}\"!" : "Threaded optimizations set failed!");
+ Logger.Log(success ? $"[NVAPI] Threaded optimizations set to \"{value}\"!" : "[NVAPI] Threaded optimizations set failed!");
}
}
@@ -205,7 +205,7 @@ namespace osu.Desktop
uint numApps = profile.NumOfApps;
- if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications)))
+ if (checkError(EnumApplications(sessionHandle, profileHandle, 0, ref numApps, applications), nameof(EnumApplications)))
return false;
for (uint i = 0; i < numApps; i++)
@@ -236,10 +236,10 @@ namespace osu.Desktop
isApplicationSpecific = true;
- if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application)))
+ if (checkError(FindApplicationByName(sessionHandle, osu_filename, out profileHandle, ref application), nameof(FindApplicationByName)))
{
isApplicationSpecific = false;
- if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle)))
+ if (checkError(GetCurrentGlobalProfile(sessionHandle, out profileHandle), nameof(GetCurrentGlobalProfile)))
return false;
}
@@ -258,12 +258,10 @@ namespace osu.Desktop
Version = NvProfile.Stride,
IsPredefined = 0,
ProfileName = PROFILE_NAME,
- GPUSupport = new uint[32]
+ GpuSupport = NvDrsGpuSupport.Geforce
};
- newProfile.GPUSupport[0] = 1;
-
- if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle)))
+ if (checkError(CreateProfile(sessionHandle, ref newProfile, out profileHandle), nameof(CreateProfile)))
return false;
return true;
@@ -284,7 +282,7 @@ namespace osu.Desktop
SettingID = settingId
};
- if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting)))
+ if (checkError(GetSetting(sessionHandle, profileHandle, settingId, ref setting), nameof(GetSetting)))
return false;
return true;
@@ -313,7 +311,7 @@ namespace osu.Desktop
};
// Set the thread state
- if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting)))
+ if (checkError(SetSetting(sessionHandle, profileHandle, ref newSetting), nameof(SetSetting)))
return false;
// Get the profile (needed to check app count)
@@ -321,7 +319,7 @@ namespace osu.Desktop
{
Version = NvProfile.Stride
};
- if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile)))
+ if (checkError(GetProfileInfo(sessionHandle, profileHandle, ref profile), nameof(GetProfileInfo)))
return false;
if (!containsApplication(profileHandle, profile, out application))
@@ -332,12 +330,12 @@ namespace osu.Desktop
application.AppName = osu_filename;
application.UserFriendlyName = APPLICATION_NAME;
- if (checkError(CreateApplication(sessionHandle, profileHandle, ref application)))
+ if (checkError(CreateApplication(sessionHandle, profileHandle, ref application), nameof(CreateApplication)))
return false;
}
// Save!
- return !checkError(SaveSettings(sessionHandle));
+ return !checkError(SaveSettings(sessionHandle), nameof(SaveSettings));
}
///
@@ -346,20 +344,25 @@ namespace osu.Desktop
/// If the operation succeeded.
private static bool createSession()
{
- if (checkError(CreateSession(out sessionHandle)))
+ if (checkError(CreateSession(out sessionHandle), nameof(CreateSession)))
return false;
// Load settings into session
- if (checkError(LoadSettings(sessionHandle)))
+ if (checkError(LoadSettings(sessionHandle), nameof(LoadSettings)))
return false;
return true;
}
- private static bool checkError(NvStatus status)
+ private static bool checkError(NvStatus status, string caller)
{
Status = status;
- return status != NvStatus.OK;
+
+ bool hasError = status != NvStatus.OK;
+ if (hasError)
+ Logger.Log($"[NVAPI] {caller} call failed with status code {status}");
+
+ return hasError;
}
static NVAPI()
@@ -458,9 +461,7 @@ namespace osu.Desktop
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NVAPI.UNICODE_STRING_MAX)]
public string ProfileName;
- [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
- public uint[] GPUSupport;
-
+ public NvDrsGpuSupport GpuSupport;
public uint IsPredefined;
public uint NumOfApps;
public uint NumOfSettings;
@@ -606,6 +607,7 @@ namespace osu.Desktop
SYNC_NOT_ACTIVE = -194, // The requested action cannot be performed without Sync being enabled.
SYNC_MASTER_NOT_FOUND = -195, // The requested action cannot be performed without Sync Master being enabled.
INVALID_SYNC_TOPOLOGY = -196, // Invalid displays passed in the NV_GSYNC_DISPLAY pointer.
+
ECID_SIGN_ALGO_UNSUPPORTED = -197, // The specified signing algorithm is not supported. Either an incorrect value was entered or the current installed driver/hardware does not support the input value.
ECID_KEY_VERIFICATION_FAILED = -198, // The encrypted public key verification has failed.
FIRMWARE_OUT_OF_DATE = -199, // The device's firmware is out of date.
@@ -744,4 +746,12 @@ namespace osu.Desktop
OGL_THREAD_CONTROL_NUM_VALUES = 2,
OGL_THREAD_CONTROL_DEFAULT = 0
}
+
+ [Flags]
+ internal enum NvDrsGpuSupport : uint
+ {
+ Geforce = 1 << 0,
+ Quadro = 1 << 1,
+ Nvs = 1 << 2
+ }
}
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 904f5edf2b..2cdbaddf72 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
index acf01b2a83..575e03051c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs
@@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
- double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains();
- double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains();
+ double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains();
+ double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains();
if (mods.Any(m => m is OsuModTouchDevice))
{
diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
index b8f5849aaf..31b00dba2b 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs
@@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
// We add tick misses here since they too mean that the player didn't follow the slider properly
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
- estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, estimateDifficultSliders);
+ estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders);
}
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
index e9f857d78e..ba37ce981c 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs
@@ -26,7 +26,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
///
protected virtual double ReducedStrainBaseline => 0.75;
- protected List ObjectStrains = new List();
protected double Difficulty;
protected OsuStrainSkill(Mod[] mods)
@@ -63,27 +62,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return Difficulty;
}
- ///
- /// Returns the number of strains weighted against the top strain.
- /// The result is scaled by clock rate as it affects the total number of strains.
- ///
- public double CountDifficultStrains()
- {
- if (Difficulty == 0)
- return 0.0;
-
- double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical
- // Use a weighted sum of all strains. Constants are arbitrary and give nice values
- return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
- }
-
- protected override double StrainValueAt(DifficultyHitObject current)
- {
- double strain = base.StrainValueAt(current);
- ObjectStrains.Add(strain);
- return strain;
- }
-
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
}
}
diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
index 58c5aeb078..b329497cb0 100644
--- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
+++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs
@@ -39,7 +39,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
double totalStrain = CurrentStrain * currentRhythm;
- ObjectStrains.Add(totalStrain);
return totalStrain;
}
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
index b706e96bdb..be2a5ac144 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
@@ -54,17 +54,17 @@ namespace osu.Game.Rulesets.Taiko.Edit
public void SetStrongState(bool state)
{
- if (SelectedItems.OfType().All(h => h.IsStrong == state))
+ if (SelectedItems.OfType().All(h => h.IsStrong == state))
return;
EditorBeatmap.PerformOnSelection(h =>
{
- if (!(h is Hit taikoHit)) return;
+ if (h is not TaikoStrongableHitObject strongable) return;
- if (taikoHit.IsStrong != state)
+ if (strongable.IsStrong != state)
{
- taikoHit.IsStrong = state;
- EditorBeatmap.Update(taikoHit);
+ strongable.IsStrong = state;
+ EditorBeatmap.Update(strongable);
}
});
}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
index 3f58476571..547d0afe4a 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs
@@ -81,6 +81,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.RecreatePieces();
updateColour();
+ Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE;
}
protected override void OnFree()
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
index 0333fd71a9..64d2020edc 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs
@@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -44,6 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
IsFirstTick.Value = HitObject.FirstTick;
}
+ protected override void RecreatePieces()
+ {
+ base.RecreatePieces();
+ Size = new Vector2(HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
+ }
+
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (!userTriggered)
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
index a5e63c373f..28831a6d2c 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
updateActionsFromType();
base.RecreatePieces();
+ Size = new Vector2(HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
}
protected override void OnFree()
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
index f2fcd185dd..28617b35f6 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs
@@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
///
private const double ring_appear_offset = 100;
+ private Vector2 baseSize;
+
private readonly Container ticks;
private readonly Container bodyContainer;
private readonly CircularContainer targetRing;
@@ -141,6 +144,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
Origin = Anchor.Centre,
});
+ protected override void RecreatePieces()
+ {
+ base.RecreatePieces();
+ Size = baseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
+ }
+
protected override void OnFree()
{
base.OnFree();
@@ -269,7 +278,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.Update();
- Size = BaseSize * Parent!.RelativeChildSize;
+ Size = baseSize * Parent!.RelativeChildSize;
// Make the swell stop at the hit target
X = Math.Max(0, X);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
index 3f4694d71d..0cf9651965 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs
@@ -130,7 +130,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public new TObject HitObject => (TObject)base.HitObject;
- protected Vector2 BaseSize;
protected SkinnableDrawable MainPiece;
protected DrawableTaikoHitObject([CanBeNull] TObject hitObject)
@@ -152,8 +151,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
protected virtual void RecreatePieces()
{
- Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE);
-
if (MainPiece != null)
Content.Remove(MainPiece, true);
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs
index 4d7cdf3243..7c3ff4f27e 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs
@@ -8,7 +8,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osuTK;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
@@ -44,13 +43,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
isStrong.UnbindEvents();
}
- protected override void RecreatePieces()
- {
- base.RecreatePieces();
- if (HitObject.IsStrong)
- Size = BaseSize = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE);
- }
-
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
diff --git a/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs b/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs
new file mode 100644
index 0000000000..5f5a1760ea
--- /dev/null
+++ b/osu.Game.Tests/Editing/TimingSectionAdjustmentsTest.cs
@@ -0,0 +1,161 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit.Timing;
+
+namespace osu.Game.Tests.Editing
+{
+ [TestFixture]
+ public class TimingSectionAdjustmentsTest
+ {
+ [Test]
+ public void TestOffsetAdjustment()
+ {
+ var controlPoints = new ControlPointInfo();
+
+ controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 });
+ controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 });
+ controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 });
+
+ var beatmap = new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ HitObjects = new List
+ {
+ new HitCircle { StartTime = 0 },
+ new HitCircle { StartTime = 200 },
+ new HitCircle { StartTime = 49_900 },
+ new HitCircle { StartTime = 50_000 },
+ new HitCircle { StartTime = 50_200 },
+ new HitCircle { StartTime = 99_800 },
+ new HitCircle { StartTime = 100_000 },
+ new HitCircle { StartTime = 100_050 },
+ new HitCircle { StartTime = 100_550 },
+ }
+ };
+
+ moveTimingPoint(beatmap, 100, -50);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(-50));
+ Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150));
+ Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850));
+ Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(50_000));
+ });
+
+ moveTimingPoint(beatmap, 50_000, 1_000);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(49_850));
+ Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(51_000));
+ Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200));
+ Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(100_800));
+ Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(100_000));
+ });
+
+ moveTimingPoint(beatmap, 100_000, 10_000);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(51_200));
+ Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(110_800));
+ Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(110_000));
+ Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(110_050));
+ Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(110_550));
+ });
+ }
+
+ [Test]
+ public void TestBPMAdjustment()
+ {
+ var controlPoints = new ControlPointInfo();
+
+ controlPoints.Add(100, new TimingControlPoint { BeatLength = 100 });
+ controlPoints.Add(50_000, new TimingControlPoint { BeatLength = 200 });
+ controlPoints.Add(100_000, new TimingControlPoint { BeatLength = 50 });
+
+ var beatmap = new Beatmap
+ {
+ ControlPointInfo = controlPoints,
+ HitObjects = new List
+ {
+ new HitCircle { StartTime = 0 },
+ new HitCircle { StartTime = 200 },
+ new Spinner { StartTime = 500, EndTime = 1000 },
+ new HitCircle { StartTime = 49_900 },
+ new HitCircle { StartTime = 50_000 },
+ new HitCircle { StartTime = 50_200 },
+ new HitCircle { StartTime = 99_800 },
+ new HitCircle { StartTime = 100_000 },
+ new HitCircle { StartTime = 100_050 },
+ new HitCircle { StartTime = 100_550 },
+ }
+ };
+
+ adjustBeatLength(beatmap, 100, 50);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.HitObjects[0].StartTime, Is.EqualTo(50));
+ Assert.That(beatmap.HitObjects[1].StartTime, Is.EqualTo(150));
+ Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300));
+ Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550));
+ Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000));
+ Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000));
+ });
+
+ adjustBeatLength(beatmap, 50_000, 400);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.HitObjects[2].StartTime, Is.EqualTo(300));
+ Assert.That(beatmap.HitObjects[2].GetEndTime(), Is.EqualTo(550));
+ Assert.That(beatmap.HitObjects[3].StartTime, Is.EqualTo(25_000));
+ Assert.That(beatmap.HitObjects[4].StartTime, Is.EqualTo(50_000));
+ Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400));
+ Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(149_600));
+ Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000));
+ });
+
+ adjustBeatLength(beatmap, 100_000, 100);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(beatmap.HitObjects[5].StartTime, Is.EqualTo(50_400));
+ Assert.That(beatmap.HitObjects[6].StartTime, Is.EqualTo(199_200));
+ Assert.That(beatmap.HitObjects[7].StartTime, Is.EqualTo(100_000));
+ Assert.That(beatmap.HitObjects[8].StartTime, Is.EqualTo(100_100));
+ Assert.That(beatmap.HitObjects[9].StartTime, Is.EqualTo(101_100));
+ });
+ }
+
+ private static void moveTimingPoint(IBeatmap beatmap, double originalTime, double adjustment)
+ {
+ var controlPoints = beatmap.ControlPointInfo;
+ var controlPointGroup = controlPoints.GroupAt(originalTime);
+ var timingPoint = controlPointGroup.ControlPoints.OfType().Single();
+ controlPoints.RemoveGroup(controlPointGroup);
+ TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, timingPoint, adjustment);
+ controlPoints.Add(originalTime - adjustment, timingPoint);
+ }
+
+ private static void adjustBeatLength(IBeatmap beatmap, double groupTime, double newBeatLength)
+ {
+ var controlPoints = beatmap.ControlPointInfo;
+ var controlPointGroup = controlPoints.GroupAt(groupTime);
+ var timingPoint = controlPointGroup.ControlPoints.OfType().Single();
+ double oldBeatLength = timingPoint.BeatLength;
+ timingPoint.BeatLength = newBeatLength;
+ TimingSectionAdjustments.SetHitObjectBPM(beatmap, timingPoint, oldBeatLength);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
index 2d61c26a6b..3b10509895 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using Moq;
@@ -36,15 +34,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
private readonly Bindable beatmapAvailability = new Bindable();
private readonly Bindable room = new Bindable();
- private MultiplayerRoom multiplayerRoom;
- private MultiplayerRoomUser localUser;
- private OngoingOperationTracker ongoingOperationTracker;
+ private MultiplayerRoom multiplayerRoom = null!;
+ private MultiplayerRoomUser localUser = null!;
+ private OngoingOperationTracker ongoingOperationTracker = null!;
- private PopoverContainer content;
- private MatchStartControl control;
+ private PopoverContainer content = null!;
+ private MatchStartControl control = null!;
private OsuButton readyButton => control.ChildrenOfType().Single();
+ [Cached(typeof(IBindable))]
+ private readonly Bindable currentItem = new Bindable();
+
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
new CachedModelDependencyContainer(base.CreateChildDependencies(parent)) { Model = { BindTarget = room } };
@@ -112,15 +113,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
- var playlistItem = new PlaylistItem(Beatmap.Value.BeatmapInfo)
+ currentItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
room.Value = new Room
{
- Playlist = { playlistItem },
- CurrentPlaylistItem = { Value = playlistItem }
+ Playlist = { currentItem.Value },
+ CurrentPlaylistItem = { BindTarget = currentItem }
};
localUser = new MultiplayerRoomUser(API.LocalUser.Value.Id) { User = API.LocalUser.Value };
@@ -129,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Playlist =
{
- TestMultiplayerClient.CreateMultiplayerPlaylistItem(playlistItem),
+ TestMultiplayerClient.CreateMultiplayerPlaylistItem(currentItem.Value),
},
Users = { localUser },
Host = localUser,
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
index c2d3b17ccb..9d8ef76e75 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchFooter.cs
@@ -1,15 +1,21 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
+using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerMatchFooter : MultiplayerTestScene
{
+ [Cached(typeof(IBindable))]
+ private readonly Bindable currentItem = new Bindable();
+
public override void SetUpSteps()
{
base.SetUpSteps();
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 88cc7eb9b3..bd635b1669 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -15,6 +16,7 @@ using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
@@ -42,6 +44,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private Live importedBeatmapSet;
+ [Resolved]
+ private OsuConfigManager configManager { get; set; }
+
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
@@ -57,10 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Add(detachedBeatmapStore);
}
- public override void SetUpSteps()
+ private void setUp()
{
- base.SetUpSteps();
-
AddStep("reset", () =>
{
Ruleset.Value = new OsuRuleset().RulesetInfo;
@@ -75,6 +78,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestSelectFreeMods()
{
+ setUp();
+
AddStep("set some freemods", () => songSelect.FreeMods.Value = new OsuRuleset().GetModsFor(ModType.Fun).ToArray());
AddStep("set all freemods", () => songSelect.FreeMods.Value = new OsuRuleset().CreateAllMods().ToArray());
AddStep("set no freemods", () => songSelect.FreeMods.Value = Array.Empty());
@@ -85,6 +90,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
BeatmapInfo selectedBeatmap = null;
+ setUp();
+
AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
AddStep("select beatmap",
() => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == new TaikoRuleset().LegacyID)));
@@ -107,6 +114,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible.
public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod)
{
+ setUp();
+
AddStep("change ruleset", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) });
AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) });
@@ -120,6 +129,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertFreeModNotShown(requiredMod);
}
+ [Test]
+ public void TestChangeRulesetImmediatelyAfterLoadComplete()
+ {
+ AddStep("reset", () =>
+ {
+ configManager.SetValue(OsuSetting.ShowConvertedBeatmaps, false);
+ Beatmap.SetDefault();
+ SelectedMods.SetDefault();
+ });
+
+ AddStep("create song select", () =>
+ {
+ SelectedRoom.Value.Playlist.Single().RulesetID = 2;
+ songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single());
+ songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo;
+ LoadScreen(songSelect);
+ });
+ AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
+
+ AddStep("confirm selection", () => songSelect.FinaliseSelection());
+ AddAssert("beatmap is taiko", () => Beatmap.Value.BeatmapInfo.Ruleset.OnlineID, () => Is.EqualTo(1));
+ AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID, () => Is.EqualTo(1));
+ }
+
private void assertFreeModNotShown(Type type)
{
AddAssert($"{type.ReadableName()} not displayed in freemod overlay",
@@ -138,8 +171,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new BeatmapCarousel Carousel => base.Carousel;
- public TestMultiplayerMatchSongSelect(Room room)
- : base(room)
+ public TestMultiplayerMatchSongSelect(Room room, [CanBeNull] PlaylistItem itemToEdit = null)
+ : base(room, itemToEdit)
{
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
index 2100f82886..3baabecd84 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerPlaylist.cs
@@ -1,13 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
+using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
@@ -29,10 +28,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerPlaylist : MultiplayerTestScene
{
- private MultiplayerPlaylist list;
- private BeatmapManager beatmaps;
- private BeatmapSetInfo importedSet;
- private BeatmapInfo importedBeatmap;
+ [Cached(typeof(IBindable))]
+ private readonly Bindable currentItem = new Bindable();
+
+ private MultiplayerPlaylist list = null!;
+ private BeatmapManager beatmaps = null!;
+ private BeatmapSetInfo importedSet = null!;
+ private BeatmapInfo importedBeatmap = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -198,7 +200,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
addItemStep();
addItemStep();
- DrawableRoomPlaylistItem[] drawableItems = null;
+ DrawableRoomPlaylistItem[] drawableItems = null!;
AddStep("get drawable items", () => drawableItems = this.ChildrenOfType().ToArray());
// Add 1 item for another user.
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
index 816ba4ca32..5ae5d1e228 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerSpectateButton.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -28,13 +26,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
{
- private MultiplayerSpectateButton spectateButton;
- private MatchStartControl startControl;
+ [Cached(typeof(IBindable))]
+ private readonly Bindable currentItem = new Bindable();
- private readonly Bindable selectedItem = new Bindable();
+ private MultiplayerSpectateButton spectateButton = null!;
+ private MatchStartControl startControl = null!;
- private BeatmapSetInfo importedSet;
- private BeatmapManager beatmaps;
+ private BeatmapSetInfo importedSet = null!;
+ private BeatmapManager beatmaps = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -52,14 +51,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create button", () =>
{
- AvailabilityTracker.SelectedItem.BindTo(selectedItem);
+ AvailabilityTracker.SelectedItem.BindTo(currentItem);
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
- selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
- {
- RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
- };
+
+ currentItem.Value = SelectedRoom.Value.Playlist.First();
Child = new PopoverContainer
{
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 16c8bc1a6b..66862e1b78 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -10,9 +10,12 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
+using osu.Game.Database;
using osu.Game.Extensions;
+using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
@@ -191,8 +194,39 @@ namespace osu.Game.Tests.Visual.SongSelect
{
AddStep("present beatmap", () => Game.PresentBeatmap(getImport()));
- AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect);
+ AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.MatchesOnlineID(getImport().Beatmaps[expectedDiff - 1]));
}
+
+ protected override TestOsuGame CreateTestGame() => new NoBeatmapUpdateGame(LocalStorage, API);
+
+ private partial class NoBeatmapUpdateGame : TestOsuGame
+ {
+ public NoBeatmapUpdateGame(Storage storage, IAPIProvider api, string[] args = null)
+ : base(storage, api, args)
+ {
+ }
+
+ protected override IBeatmapUpdater CreateBeatmapUpdater() => new TestBeatmapUpdater();
+
+ private class TestBeatmapUpdater : IBeatmapUpdater
+ {
+ public void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
+ {
+ }
+
+ public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
+ {
+ }
+
+ public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
+ {
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
index b160043820..965f3be0aa 100644
--- a/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
+++ b/osu.Game/Beatmaps/BeatmapOnlineChangeIngest.cs
@@ -13,11 +13,11 @@ namespace osu.Game.Beatmaps
///
public partial class BeatmapOnlineChangeIngest : Component
{
- private readonly BeatmapUpdater beatmapUpdater;
+ private readonly IBeatmapUpdater beatmapUpdater;
private readonly RealmAccess realm;
private readonly MetadataClient metadataClient;
- public BeatmapOnlineChangeIngest(BeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient)
+ public BeatmapOnlineChangeIngest(IBeatmapUpdater beatmapUpdater, RealmAccess realm, MetadataClient metadataClient)
{
this.beatmapUpdater = beatmapUpdater;
this.realm = realm;
diff --git a/osu.Game/Beatmaps/BeatmapUpdater.cs b/osu.Game/Beatmaps/BeatmapUpdater.cs
index e897d28916..efb432b84e 100644
--- a/osu.Game/Beatmaps/BeatmapUpdater.cs
+++ b/osu.Game/Beatmaps/BeatmapUpdater.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
@@ -15,10 +14,7 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Beatmaps
{
- ///
- /// Handles all processing required to ensure a local beatmap is in a consistent state with any changes.
- ///
- public class BeatmapUpdater : IDisposable
+ public class BeatmapUpdater : IBeatmapUpdater
{
private readonly IWorkingBeatmapCache workingBeatmapCache;
@@ -38,11 +34,6 @@ namespace osu.Game.Beatmaps
metadataLookup = new BeatmapUpdaterMetadataLookup(api, storage);
}
- ///
- /// Queue a beatmap for background processing.
- ///
- /// The managed beatmap set to update. A transaction will be opened to apply changes.
- /// The preferred scope to use for metadata lookup.
public void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
{
Logger.Log($"Queueing change for local beatmap {beatmapSet}");
@@ -50,55 +41,56 @@ namespace osu.Game.Beatmaps
updateScheduler);
}
- ///
- /// Run all processing on a beatmap immediately.
- ///
- /// The managed beatmap set to update. A transaction will be opened to apply changes.
- /// The preferred scope to use for metadata lookup.
- public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ =>
+ public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
{
- // Before we use below, we want to invalidate.
- workingBeatmapCache.Invalidate(beatmapSet);
-
- if (lookupScope != MetadataLookupScope.None)
- metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst);
-
- foreach (var beatmap in beatmapSet.Beatmaps)
+ beatmapSet.Realm!.Write(_ =>
{
- difficultyCache.Invalidate(beatmap);
+ // Before we use below, we want to invalidate.
+ workingBeatmapCache.Invalidate(beatmapSet);
- var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
- var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
+ if (lookupScope != MetadataLookupScope.None)
+ metadataLookup.Update(beatmapSet, lookupScope == MetadataLookupScope.OnlineFirst);
- Debug.Assert(ruleset != null);
+ foreach (var beatmap in beatmapSet.Beatmaps)
+ {
+ difficultyCache.Invalidate(beatmap);
- var calculator = ruleset.CreateDifficultyCalculator(working);
+ var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
+ var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
- beatmap.StarRating = calculator.Calculate().StarRating;
- beatmap.Length = working.Beatmap.CalculatePlayableLength();
- beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
- beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration);
- beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count;
- }
+ Debug.Assert(ruleset != null);
- // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
- workingBeatmapCache.Invalidate(beatmapSet);
- });
+ var calculator = ruleset.CreateDifficultyCalculator(working);
- public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapInfo.Realm!.Write(_ =>
+ beatmap.StarRating = calculator.Calculate().StarRating;
+ beatmap.Length = working.Beatmap.CalculatePlayableLength();
+ beatmap.BPM = 60000 / working.Beatmap.GetMostCommonBeatLength();
+ beatmap.EndTimeObjectCount = working.Beatmap.HitObjects.Count(h => h is IHasDuration);
+ beatmap.TotalObjectCount = working.Beatmap.HitObjects.Count;
+ }
+
+ // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
+ workingBeatmapCache.Invalidate(beatmapSet);
+ });
+ }
+
+ public void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst)
{
- // Before we use below, we want to invalidate.
- workingBeatmapCache.Invalidate(beatmapInfo);
+ beatmapInfo.Realm!.Write(_ =>
+ {
+ // Before we use below, we want to invalidate.
+ workingBeatmapCache.Invalidate(beatmapInfo);
- var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
- var beatmap = working.Beatmap;
+ var working = workingBeatmapCache.GetWorkingBeatmap(beatmapInfo);
+ var beatmap = working.Beatmap;
- beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
- beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count;
+ beatmapInfo.EndTimeObjectCount = beatmap.HitObjects.Count(h => h is IHasDuration);
+ beatmapInfo.TotalObjectCount = beatmap.HitObjects.Count;
- // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
- workingBeatmapCache.Invalidate(beatmapInfo);
- });
+ // And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
+ workingBeatmapCache.Invalidate(beatmapInfo);
+ });
+ }
#region Implementation of IDisposable
diff --git a/osu.Game/Beatmaps/IBeatmapUpdater.cs b/osu.Game/Beatmaps/IBeatmapUpdater.cs
new file mode 100644
index 0000000000..062984adf0
--- /dev/null
+++ b/osu.Game/Beatmaps/IBeatmapUpdater.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Game.Database;
+
+namespace osu.Game.Beatmaps
+{
+ ///
+ /// Handles all processing required to ensure a local beatmap is in a consistent state with any changes.
+ ///
+ public interface IBeatmapUpdater : IDisposable
+ {
+ ///
+ /// Queue a beatmap for background processing.
+ ///
+ /// The managed beatmap set to update. A transaction will be opened to apply changes.
+ /// The preferred scope to use for metadata lookup.
+ void Queue(Live beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst);
+
+ ///
+ /// Run all processing on a beatmap immediately.
+ ///
+ /// The managed beatmap set to update. A transaction will be opened to apply changes.
+ /// The preferred scope to use for metadata lookup.
+ void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst);
+
+ ///
+ /// Runs a subset of processing focused on updating any cached beatmap object counts.
+ ///
+ /// The managed beatmap to update. A transaction will be opened to apply changes.
+ /// The preferred scope to use for metadata lookup.
+ void ProcessObjectCounts(BeatmapInfo beatmapInfo, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst);
+ }
+}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index f642d23bb0..af6fd61a3d 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -196,6 +196,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorShowSpeedChanges, false);
SetDefault(OsuSetting.EditorScaleOrigin, EditorOrigin.GridCentre);
SetDefault(OsuSetting.EditorRotationOrigin, EditorOrigin.GridCentre);
+ SetDefault(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges, true);
SetDefault(OsuSetting.HideCountryFlags, false);
@@ -442,5 +443,6 @@ namespace osu.Game.Configuration
EditorScaleOrigin,
EditorRotationOrigin,
EditorTimelineShowBreaks,
+ EditorAdjustExistingObjectsOnTimingChanges,
}
}
diff --git a/osu.Game/Database/BackgroundDataStoreProcessor.cs b/osu.Game/Database/BackgroundDataStoreProcessor.cs
index 3efd4da3aa..1512b6be93 100644
--- a/osu.Game/Database/BackgroundDataStoreProcessor.cs
+++ b/osu.Game/Database/BackgroundDataStoreProcessor.cs
@@ -46,7 +46,7 @@ namespace osu.Game.Database
private RealmAccess realmAccess { get; set; } = null!;
[Resolved]
- private BeatmapUpdater beatmapUpdater { get; set; } = null!;
+ private IBeatmapUpdater beatmapUpdater { get; set; } = null!;
[Resolved]
private IBindable gameBeatmap { get; set; } = null!;
diff --git a/osu.Game/Localisation/EditorStrings.cs b/osu.Game/Localisation/EditorStrings.cs
index 1ba03b3fde..127bdd8355 100644
--- a/osu.Game/Localisation/EditorStrings.cs
+++ b/osu.Game/Localisation/EditorStrings.cs
@@ -39,6 +39,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString SetPreviewPointToCurrent => new TranslatableString(getKey(@"set_preview_point_to_current"), @"Set preview point to current time");
+ ///
+ /// "Move already placed objects when changing timing"
+ ///
+ public static LocalisableString AdjustExistingObjectsOnTimingChanges => new TranslatableString(getKey(@"adjust_existing_objects_on_timing_changes"), @"Move already placed objects when changing timing");
+
///
/// "For editing (.olz)"
///
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 5fd8b8b337..ff147aba10 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -202,7 +202,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(joinedRoom.Playlist.Count > 0);
APIRoom.Playlist.Clear();
- APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
+ APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(item => new PlaylistItem(item)));
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
// The server will null out the end date upon the host joining the room, but the null value is never communicated to the client.
@@ -734,7 +734,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(APIRoom != null);
Room.Playlist.Add(item);
- APIRoom.Playlist.Add(createPlaylistItem(item));
+ APIRoom.Playlist.Add(new PlaylistItem(item));
ItemAdded?.Invoke(item);
RoomUpdated?.Invoke();
@@ -780,7 +780,7 @@ namespace osu.Game.Online.Multiplayer
int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID));
APIRoom.Playlist.RemoveAt(existingIndex);
- APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item));
+ APIRoom.Playlist.Insert(existingIndex, new PlaylistItem(item));
}
catch (Exception ex)
{
@@ -853,18 +853,6 @@ namespace osu.Game.Online.Multiplayer
RoomUpdated?.Invoke();
}
- private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating })
- {
- ID = item.ID,
- OwnerID = item.OwnerID,
- RulesetID = item.RulesetID,
- Expired = item.Expired,
- PlaylistOrder = item.PlaylistOrder,
- PlayedAt = item.PlayedAt,
- RequiredMods = item.RequiredMods.ToArray(),
- AllowedMods = item.AllowedMods.ToArray()
- };
-
///
/// For the provided user ID, update whether the user is included in .
///
diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomExtensions.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomExtensions.cs
new file mode 100644
index 0000000000..0aeb85d2d8
--- /dev/null
+++ b/osu.Game/Online/Multiplayer/MultiplayerRoomExtensions.cs
@@ -0,0 +1,39 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Game.Online.Rooms;
+
+namespace osu.Game.Online.Multiplayer
+{
+ public static class MultiplayerRoomExtensions
+ {
+ ///
+ /// Returns all historical/expired items from the , in the order in which they were played.
+ ///
+ public static IEnumerable GetHistoricalItems(this MultiplayerRoom room)
+ => room.Playlist.Where(item => item.Expired).OrderBy(item => item.PlayedAt);
+
+ ///
+ /// Returns all non-expired items from the , in the order in which they are to be played.
+ ///
+ public static IEnumerable GetUpcomingItems(this MultiplayerRoom room)
+ => room.Playlist.Where(item => !item.Expired).OrderBy(item => item.PlaylistOrder);
+
+ ///
+ /// Returns the first non-expired in playlist order from the supplied ,
+ /// or the last-played if all items are expired,
+ /// or if was empty.
+ ///
+ public static MultiplayerPlaylistItem? GetCurrentItem(this MultiplayerRoom room)
+ {
+ if (room.Playlist.Count == 0)
+ return null;
+
+ return room.Playlist.All(item => item.Expired)
+ ? GetHistoricalItems(room).Last()
+ : GetUpcomingItems(room).First();
+ }
+ }
+}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index d4704d1c72..dc13924b4f 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -198,7 +198,7 @@ namespace osu.Game
public readonly Bindable>> AvailableMods = new Bindable>>(new Dictionary>());
private BeatmapDifficultyCache difficultyCache;
- private BeatmapUpdater beatmapUpdater;
+ private IBeatmapUpdater beatmapUpdater;
private UserLookupCache userCache;
private BeatmapLookupCache beatmapCache;
@@ -324,7 +324,7 @@ namespace osu.Game
base.Content.Add(difficultyCache);
// TODO: OsuGame or OsuGameBase?
- dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
+ dependencies.CacheAs(beatmapUpdater = CreateBeatmapUpdater());
dependencies.CacheAs(SpectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
@@ -563,6 +563,8 @@ namespace osu.Game
}
}
+ protected virtual IBeatmapUpdater CreateBeatmapUpdater() => new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage);
+
protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager();
protected virtual BatteryInfo CreateBatteryInfo() => null;
diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
index 981a5a238c..186fb2f26e 100644
--- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
+++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs
@@ -42,10 +42,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
protected double CurrentStrain;
private double currentSectionPeak; // We also keep track of the peak strain level in the current section.
-
private double currentSectionEnd;
private readonly List strainPeaks = new List();
+ protected readonly List ObjectStrains = new List(); // Store individual strains
protected StrainSkill(Mod[] mods)
: base(mods)
@@ -86,7 +86,25 @@ namespace osu.Game.Rulesets.Difficulty.Skills
currentSectionEnd += SectionLength;
}
- currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak);
+ double strain = StrainValueAt(current);
+ currentSectionPeak = Math.Max(strain, currentSectionPeak);
+
+ // Store the strain value for the object
+ ObjectStrains.Add(strain);
+ }
+
+ ///
+ /// Calculates the number of strains weighted against the top strain.
+ /// The result is scaled by clock rate as it affects the total number of strains.
+ ///
+ public virtual double CountTopWeightedStrains()
+ {
+ if (ObjectStrains.Count == 0)
+ return 0.0;
+
+ double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical
+ // Use a weighted sum of all strains. Constants are arbitrary and give nice values
+ return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
}
///
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 6262bb7aba..644e1afb3b 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -421,7 +421,7 @@ namespace osu.Game.Screens.Edit
{
Items = new MenuItem[]
{
- new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime)
+ new EditorMenuItem(EditorStrings.SetPreviewPointToCurrent, MenuItemType.Standard, SetPreviewPointToCurrentTime),
}
}
}
diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs
index d715c3ebc9..13e802a8e4 100644
--- a/osu.Game/Screens/Edit/Timing/GroupSection.cs
+++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
@@ -25,6 +26,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
protected EditorBeatmap Beatmap { get; private set; } = null!;
+ [Resolved]
+ private OsuConfigManager configManager { get; set; } = null!;
+
[Resolved]
private EditorClock clock { get; set; } = null!;
@@ -110,7 +114,16 @@ namespace osu.Game.Screens.Edit.Timing
Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value);
foreach (var cp in currentGroupItems)
+ {
+ // Only adjust hit object offsets if the group contains a timing control point
+ if (cp is TimingControlPoint tp && configManager.Get(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges))
+ {
+ TimingSectionAdjustments.AdjustHitObjectOffset(Beatmap, tp, time - SelectedGroup.Value.Time);
+ Beatmap.UpdateAllHitObjects();
+ }
+
Beatmap.ControlPointInfo.Add(time, cp);
+ }
// the control point might not necessarily exist yet, if currentGroupItems was empty.
SelectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(time, true);
diff --git a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs
index 8cdbd97ecb..f105c00726 100644
--- a/osu.Game/Screens/Edit/Timing/TapTimingControl.cs
+++ b/osu.Game/Screens/Edit/Timing/TapTimingControl.cs
@@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
@@ -26,6 +27,9 @@ namespace osu.Game.Screens.Edit.Timing
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
+ [Resolved]
+ private OsuConfigManager configManager { get; set; } = null!;
+
[Resolved]
private Bindable selectedGroup { get; set; } = null!;
@@ -202,15 +206,25 @@ namespace osu.Game.Screens.Edit.Timing
// VERY TEMPORARY
var currentGroupItems = selectedGroup.Value.ControlPoints.ToArray();
+ beatmap.BeginChange();
beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
double newOffset = selectedGroup.Value.Time + adjust;
foreach (var cp in currentGroupItems)
+ {
+ if (cp is TimingControlPoint tp)
+ {
+ TimingSectionAdjustments.AdjustHitObjectOffset(beatmap, tp, adjust);
+ beatmap.UpdateAllHitObjects();
+ }
+
beatmap.ControlPointInfo.Add(newOffset, cp);
+ }
// the control point might not necessarily exist yet, if currentGroupItems was empty.
selectedGroup.Value = beatmap.ControlPointInfo.GroupAt(newOffset, true);
+ beatmap.EndChange();
if (!editorClock.IsRunning && wasAtStart)
editorClock.Seek(newOffset);
@@ -223,7 +237,16 @@ namespace osu.Game.Screens.Edit.Timing
if (timing == null)
return;
+ double oldBeatLength = timing.BeatLength;
timing.BeatLength = 60000 / (timing.BPM + adjust);
+
+ if (configManager.Get(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges))
+ {
+ beatmap.BeginChange();
+ TimingSectionAdjustments.SetHitObjectBPM(beatmap, timing, oldBeatLength);
+ beatmap.UpdateAllHitObjects();
+ beatmap.EndChange();
+ }
}
private partial class InlineButton : OsuButton
diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs
index 838eb1f9fd..ae1ac02dd6 100644
--- a/osu.Game/Screens/Edit/Timing/TimingSection.cs
+++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs
@@ -1,11 +1,14 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Timing
{
@@ -15,11 +18,20 @@ namespace osu.Game.Screens.Edit.Timing
private LabelledSwitchButton omitBarLine = null!;
private BPMTextBox bpmTextEntry = null!;
+ [Resolved]
+ private OsuConfigManager configManager { get; set; } = null!;
+
[BackgroundDependencyLoader]
private void load()
{
Flow.AddRange(new Drawable[]
{
+ new LabelledSwitchButton
+ {
+ Label = EditorStrings.AdjustExistingObjectsOnTimingChanges,
+ FixedLabelWidth = 220,
+ Current = configManager.GetBindable(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges),
+ },
new TapTimingControl(),
bpmTextEntry = new BPMTextBox(),
timeSignature = new LabelledTimeSignature
@@ -42,6 +54,17 @@ namespace osu.Game.Screens.Edit.Timing
{
if (!isRebinding) ChangeHandler?.SaveState();
}
+
+ bpmTextEntry.OnCommit = (oldBeatLength, _) =>
+ {
+ if (!configManager.Get(OsuSetting.EditorAdjustExistingObjectsOnTimingChanges) || ControlPoint.Value == null)
+ return;
+
+ Beatmap.BeginChange();
+ TimingSectionAdjustments.SetHitObjectBPM(Beatmap, ControlPoint.Value, oldBeatLength);
+ Beatmap.UpdateAllHitObjects();
+ Beatmap.EndChange();
+ };
}
private bool isRebinding;
@@ -74,6 +97,8 @@ namespace osu.Game.Screens.Edit.Timing
private partial class BPMTextBox : LabelledTextBox
{
+ public new Action? OnCommit { get; set; }
+
private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable;
public BPMTextBox()
@@ -81,10 +106,12 @@ namespace osu.Game.Screens.Edit.Timing
Label = "BPM";
SelectAllOnFocus = true;
- OnCommit += (_, isNew) =>
+ base.OnCommit += (_, isNew) =>
{
if (!isNew) return;
+ double oldBeatLength = beatLengthBindable.Value;
+
try
{
if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0)
@@ -98,6 +125,7 @@ namespace osu.Game.Screens.Edit.Timing
// This is run regardless of parsing success as the parsed number may not actually trigger a change
// due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state.
beatLengthBindable.TriggerChange();
+ OnCommit?.Invoke(oldBeatLength, beatLengthBindable.Value);
};
beatLengthBindable.BindValueChanged(val =>
diff --git a/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs b/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs
new file mode 100644
index 0000000000..65edc47ff5
--- /dev/null
+++ b/osu.Game/Screens/Edit/Timing/TimingSectionAdjustments.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Utils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+
+namespace osu.Game.Screens.Edit.Timing
+{
+ public static class TimingSectionAdjustments
+ {
+ ///
+ /// Returns all objects from which are affected by the supplied .
+ ///
+ public static List HitObjectsInTimingRange(IBeatmap beatmap, TimingControlPoint timingControlPoint)
+ {
+ // If the first group, we grab all hitobjects prior to the next, if the last group, we grab all remaining hitobjects
+ double startTime = beatmap.ControlPointInfo.TimingPoints.Any(x => x.Time < timingControlPoint.Time) ? timingControlPoint.Time : double.MinValue;
+ double endTime = beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(x => x.Time > timingControlPoint.Time)?.Time ?? double.MaxValue;
+
+ return beatmap.HitObjects.Where(x => Precision.AlmostBigger(x.StartTime, startTime) && Precision.DefinitelyBigger(endTime, x.StartTime)).ToList();
+ }
+
+ ///
+ /// Moves all relevant objects after 's offset has been changed by .
+ ///
+ public static void AdjustHitObjectOffset(IBeatmap beatmap, TimingControlPoint timingControlPoint, double adjustment)
+ {
+ foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint))
+ {
+ hitObject.StartTime += adjustment;
+ }
+ }
+
+ ///
+ /// Ensures all relevant objects are still snapped to the same beats after 's beat length / BPM has been changed.
+ ///
+ public static void SetHitObjectBPM(IBeatmap beatmap, TimingControlPoint timingControlPoint, double oldBeatLength)
+ {
+ foreach (HitObject hitObject in HitObjectsInTimingRange(beatmap, timingControlPoint))
+ {
+ double beat = (hitObject.StartTime - timingControlPoint.Time) / oldBeatLength;
+
+ hitObject.StartTime = (beat * timingControlPoint.BeatLength) + timingControlPoint.Time;
+
+ if (hitObject is not IHasRepeats && hitObject is IHasDuration hitObjectWithDuration)
+ hitObjectWithDuration.Duration *= timingControlPoint.BeatLength / oldBeatLength;
+ }
+ }
+ }
+}
diff --git a/osu.Game/Screens/Menu/StorageErrorDialog.cs b/osu.Game/Screens/Menu/StorageErrorDialog.cs
index b48046d190..677a3b0278 100644
--- a/osu.Game/Screens/Menu/StorageErrorDialog.cs
+++ b/osu.Game/Screens/Menu/StorageErrorDialog.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Overlays;
@@ -16,6 +17,9 @@ namespace osu.Game.Screens.Menu
[Resolved]
private IDialogOverlay dialogOverlay { get; set; } = null!;
+ [Resolved]
+ private RealmAccess realmAccess { get; set; } = null!;
+
public StorageErrorDialog(OsuStorage storage, OsuStorageError error)
{
HeaderText = StorageErrorDialogStrings.StorageError;
@@ -35,7 +39,15 @@ namespace osu.Game.Screens.Menu
Text = StorageErrorDialogStrings.TryAgain,
Action = () =>
{
- if (!storage.TryChangeToCustomStorage(out var nextError))
+ bool success;
+ OsuStorageError nextError;
+
+ // blocking all operations has a side effect of closing & reopening the realm db,
+ // which is desirable here since the restoration of the old storage - if it succeeds - means the realm db has moved.
+ using (realmAccess.BlockAllOperations(@"restoration of previously unavailable storage"))
+ success = storage.TryChangeToCustomStorage(out nextError);
+
+ if (!success)
dialogOverlay.Push(new StorageErrorDialog(storage, nextError));
}
},
diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs
index adfc44fbd4..09aafa415a 100644
--- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs
+++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RankRangePill.cs
@@ -1,23 +1,25 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
-using osu.Game.Screens.OnlinePlay.Multiplayer;
+using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
- public partial class RankRangePill : MultiplayerRoomComposite
+ public partial class RankRangePill : CompositeDrawable
{
- private OsuTextFlowContainer rankFlow;
+ private OsuTextFlowContainer rankFlow = null!;
+
+ [Resolved]
+ private MultiplayerClient client { get; set; } = null!;
public RankRangePill()
{
@@ -55,20 +57,28 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
};
}
- protected override void OnRoomUpdated()
+ protected override void LoadComplete()
{
- base.OnRoomUpdated();
+ base.LoadComplete();
+ client.RoomUpdated += onRoomUpdated;
+ updateState();
+ }
+
+ private void onRoomUpdated() => Scheduler.AddOnce(updateState);
+
+ private void updateState()
+ {
rankFlow.Clear();
- if (Room == null || Room.Users.All(u => u.User == null))
+ if (client.Room == null || client.Room.Users.All(u => u.User == null))
{
rankFlow.AddText("-");
return;
}
- int minRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min();
- int maxRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max();
+ int minRank = client.Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min();
+ int maxRank = client.Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max();
rankFlow.AddText("#");
rankFlow.AddText(minRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
@@ -78,5 +88,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
rankFlow.AddText("#");
rankFlow.AddText(maxRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (client.IsNotNull())
+ client.RoomUpdated -= onRoomUpdated;
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs
index ba3508b24f..a82fa6e4bc 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MatchStartControl.cs
@@ -1,47 +1,50 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Diagnostics;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
+using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
- public partial class MatchStartControl : MultiplayerRoomComposite
+ public partial class MatchStartControl : CompositeDrawable
{
[Resolved]
- private OngoingOperationTracker ongoingOperationTracker { get; set; }
-
- [CanBeNull]
- private IDisposable clickOperation;
+ private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
[Resolved(canBeNull: true)]
- private IDialogOverlay dialogOverlay { get; set; }
+ private IDialogOverlay? dialogOverlay { get; set; }
- private Sample sampleReady;
- private Sample sampleReadyAll;
- private Sample sampleUnready;
+ [Resolved]
+ private MultiplayerClient client { get; set; } = null!;
+
+ [Resolved]
+ private IBindable currentItem { get; set; } = null!;
private readonly MultiplayerReadyButton readyButton;
private readonly MultiplayerCountdownButton countdownButton;
+
+ private IBindable operationInProgress = null!;
+ private ScheduledDelegate? readySampleDelegate;
+ private IDisposable? clickOperation;
+ private Sample? sampleReady;
+ private Sample? sampleReadyAll;
+ private Sample? sampleUnready;
private int countReady;
- private ScheduledDelegate readySampleDelegate;
- private IBindable operationInProgress;
public MatchStartControl()
{
@@ -91,34 +94,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
base.LoadComplete();
- CurrentPlaylistItem.BindValueChanged(_ => updateState());
- }
-
- protected override void OnRoomUpdated()
- {
- base.OnRoomUpdated();
+ currentItem.BindValueChanged(_ => updateState());
+ client.RoomUpdated += onRoomUpdated;
+ client.LoadRequested += onLoadRequested;
updateState();
}
- protected override void OnRoomLoadRequested()
- {
- base.OnRoomLoadRequested();
- endOperation();
- }
+ private void onRoomUpdated() => Scheduler.AddOnce(updateState);
+
+ private void onLoadRequested() => Scheduler.AddOnce(endOperation);
private void onReadyButtonClick()
{
- if (Room == null)
+ if (client.Room == null)
return;
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
- if (Client.IsHost)
+ if (client.IsHost)
{
- if (Room.State == MultiplayerRoomState.Open)
+ if (client.Room.State == MultiplayerRoomState.Open)
{
- if (isReady() && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
+ if (isReady() && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
startMatch();
else
toggleReady();
@@ -131,16 +129,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation));
}
}
- else if (Room.State != MultiplayerRoomState.Closed)
+ else if (client.Room.State != MultiplayerRoomState.Closed)
toggleReady();
- bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
+ bool isReady() => client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating;
- void toggleReady() => Client.ToggleReady().FireAndForget(
+ void toggleReady() => client.ToggleReady().FireAndForget(
onSuccess: endOperation,
onError: _ => endOperation());
- void startMatch() => Client.StartMatch().FireAndForget(onSuccess: () =>
+ void startMatch() => client.StartMatch().FireAndForget(onSuccess: () =>
{
// gameplay is starting, the button will be unblocked on load requested.
}, onError: _ =>
@@ -149,7 +147,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
endOperation();
});
- void abortMatch() => Client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
+ void abortMatch() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
}
private void startCountdown(TimeSpan duration)
@@ -157,19 +155,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
- Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
+ client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
}
private void cancelCountdown()
{
- if (Client.Room == null)
+ if (client.Room == null)
return;
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
- MultiplayerCountdown countdown = Client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
- Client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
+ MultiplayerCountdown countdown = client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
+ client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
}
private void endOperation()
@@ -180,19 +178,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void updateState()
{
- if (Room == null)
+ if (client.Room == null)
{
readyButton.Enabled.Value = false;
countdownButton.Enabled.Value = false;
return;
}
- var localUser = Client.LocalUser;
+ var localUser = client.LocalUser;
- int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
- int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
+ int newCountReady = client.Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
+ int newCountTotal = client.Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
- if (!Client.IsHost || Room.Settings.AutoStartEnabled)
+ if (!client.IsHost || client.Room.Settings.AutoStartEnabled)
countdownButton.Hide();
else
{
@@ -211,21 +209,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
readyButton.Enabled.Value = countdownButton.Enabled.Value =
- Room.State != MultiplayerRoomState.Closed
- && CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
- && !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
+ client.Room.State != MultiplayerRoomState.Closed
+ && currentItem.Value?.ID == client.Room.Settings.PlaylistItemId
+ && !client.Room.Playlist.Single(i => i.ID == client.Room.Settings.PlaylistItemId).Expired
&& !operationInProgress.Value;
// When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating)
- readyButton.Enabled.Value &= Client.IsHost && newCountReady > 0 && !Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
+ readyButton.Enabled.Value &= client.IsHost && newCountReady > 0 && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
// When the local user is not the host, the button should only be enabled when no match is in progress.
- if (!Client.IsHost)
- readyButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
+ if (!client.IsHost)
+ readyButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
// At all times, the countdown button should only be enabled when no match is in progress.
- countdownButton.Enabled.Value &= Room.State == MultiplayerRoomState.Open;
+ countdownButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
if (newCountReady == countReady)
return;
@@ -249,6 +247,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
});
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (client.IsNotNull())
+ {
+ client.RoomUpdated -= onRoomUpdated;
+ client.LoadRequested -= onLoadRequested;
+ }
+ }
+
public partial class ConfirmAbortDialog : DangerousActionDialog
{
public ConfirmAbortDialog(Action abortMatch, Action cancel)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs
index ea7ab2dce3..92edc9b979 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerSpectateButton.cs
@@ -5,7 +5,9 @@ using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
@@ -17,7 +19,7 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
- public partial class MultiplayerSpectateButton : MultiplayerRoomComposite
+ public partial class MultiplayerSpectateButton : CompositeDrawable
{
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; } = null!;
@@ -25,6 +27,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[Resolved]
private OsuColour colours { get; set; } = null!;
+ [Resolved]
+ private MultiplayerClient client { get; set; } = null!;
+
+ [Resolved]
+ private IBindable currentItem { get; set; } = null!;
+
private IBindable operationInProgress = null!;
private readonly RoundedButton button;
@@ -44,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
var clickOperation = ongoingOperationTracker.BeginOperation();
- Client.ToggleSpectate().ContinueWith(_ => endOperation());
+ client.ToggleSpectate().ContinueWith(_ => endOperation());
void endOperation() => clickOperation?.Dispose();
}
@@ -63,19 +71,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
base.LoadComplete();
- CurrentPlaylistItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true);
- }
-
- protected override void OnRoomUpdated()
- {
- base.OnRoomUpdated();
-
+ currentItem.BindValueChanged(_ => Scheduler.AddOnce(checkForAutomaticDownload), true);
+ client.RoomUpdated += onRoomUpdated;
updateState();
}
+ private void onRoomUpdated() => Scheduler.AddOnce(updateState);
+
private void updateState()
{
- switch (Client.LocalUser?.State)
+ switch (client.LocalUser?.State)
{
default:
button.Text = "Spectate";
@@ -88,8 +93,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
break;
}
- button.Enabled.Value = Client.Room != null
- && Client.Room.State != MultiplayerRoomState.Closed
+ button.Enabled.Value = client.Room != null
+ && client.Room.State != MultiplayerRoomState.Closed
&& !operationInProgress.Value;
Scheduler.AddOnce(checkForAutomaticDownload);
@@ -112,11 +117,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void checkForAutomaticDownload()
{
- PlaylistItem? currentItem = CurrentPlaylistItem.Value;
+ PlaylistItem? item = currentItem.Value;
downloadCheckCancellation?.Cancel();
- if (currentItem == null)
+ if (item == null)
return;
if (!automaticallyDownload.Value)
@@ -128,13 +133,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
//
// Rather than over-complicating this flow, let's only auto-download when spectating for the time being.
// A potential path forward would be to have a local auto-download checkbox above the playlist item list area.
- if (Client.LocalUser?.State != MultiplayerUserState.Spectating)
+ if (client.LocalUser?.State != MultiplayerUserState.Spectating)
return;
// In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes.
// ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised.
beatmapLookupCache
- .GetBeatmapAsync(currentItem.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
+ .GetBeatmapAsync(item.Beatmap.OnlineID, (downloadCheckCancellation = new CancellationTokenSource()).Token)
.ContinueWith(resolved => Schedule(() =>
{
var beatmapSet = resolved.GetResultSafely()?.BeatmapSet;
@@ -150,5 +155,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}
#endregion
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (client.IsNotNull())
+ client.RoomUpdated -= onRoomUpdated;
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
index 2d08d8ecf6..8ba85019d5 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerPlaylist.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
using System.Linq;
using osu.Framework.Allocation;
@@ -17,18 +15,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
///
/// The multiplayer playlist, containing lists to show the items from a in both gameplay-order and historical-order.
///
- public partial class MultiplayerPlaylist : MultiplayerRoomComposite
+ public partial class MultiplayerPlaylist : CompositeDrawable
{
public readonly Bindable DisplayMode = new Bindable();
///
/// Invoked when an item requests to be edited.
///
- public Action RequestEdit;
+ public Action? RequestEdit;
- private MultiplayerPlaylistTabControl playlistTabControl;
- private MultiplayerQueueList queueList;
- private MultiplayerHistoryList historyList;
+ [Resolved]
+ private MultiplayerClient client { get; set; } = null!;
+
+ [Resolved]
+ private IBindable currentItem { get; set; } = null!;
+
+ private MultiplayerPlaylistTabControl playlistTabControl = null!;
+ private MultiplayerQueueList queueList = null!;
+ private MultiplayerHistoryList historyList = null!;
private bool firstPopulation = true;
[BackgroundDependencyLoader]
@@ -54,14 +58,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
queueList = new MultiplayerQueueList
{
RelativeSizeAxes = Axes.Both,
- SelectedItem = { BindTarget = CurrentPlaylistItem },
+ SelectedItem = { BindTarget = currentItem },
RequestEdit = item => RequestEdit?.Invoke(item)
},
historyList = new MultiplayerHistoryList
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
- SelectedItem = { BindTarget = CurrentPlaylistItem }
+ SelectedItem = { BindTarget = currentItem }
}
}
}
@@ -73,7 +77,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
protected override void LoadComplete()
{
base.LoadComplete();
+
DisplayMode.BindValueChanged(onDisplayModeChanged, true);
+ client.ItemAdded += playlistItemAdded;
+ client.ItemRemoved += playlistItemRemoved;
+ client.ItemChanged += playlistItemChanged;
+ client.RoomUpdated += onRoomUpdated;
+ updateState();
}
private void onDisplayModeChanged(ValueChangedEvent mode)
@@ -82,11 +92,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
queueList.FadeTo(mode.NewValue == MultiplayerPlaylistDisplayMode.Queue ? 1 : 0, 100);
}
- protected override void OnRoomUpdated()
- {
- base.OnRoomUpdated();
+ private void onRoomUpdated() => Scheduler.AddOnce(updateState);
- if (Room == null)
+ private void updateState()
+ {
+ if (client.Room == null)
{
historyList.Items.Clear();
queueList.Items.Clear();
@@ -96,34 +106,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
if (firstPopulation)
{
- foreach (var item in Room.Playlist)
+ foreach (var item in client.Room.Playlist)
addItemToLists(item);
firstPopulation = false;
}
}
- protected override void PlaylistItemAdded(MultiplayerPlaylistItem item)
- {
- base.PlaylistItemAdded(item);
- addItemToLists(item);
- }
+ private void playlistItemAdded(MultiplayerPlaylistItem item) => Schedule(() => addItemToLists(item));
- protected override void PlaylistItemRemoved(long item)
- {
- base.PlaylistItemRemoved(item);
- removeItemFromLists(item);
- }
+ private void playlistItemRemoved(long item) => Schedule(() => removeItemFromLists(item));
- protected override void PlaylistItemChanged(MultiplayerPlaylistItem item)
+ private void playlistItemChanged(MultiplayerPlaylistItem item) => Schedule(() =>
{
- base.PlaylistItemChanged(item);
+ if (client.Room == null)
+ return;
- var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
+ var newApiItem = new PlaylistItem(item);
var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
// Test if the only change between the two playlist items is the order.
- if (newApiItem != null && existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
+ if (existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
{
// Set the new playlist order directly without refreshing the DrawablePlaylistItem.
existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder;
@@ -137,20 +140,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
removeItemFromLists(item.ID);
addItemToLists(item);
}
- }
+ });
private void addItemToLists(MultiplayerPlaylistItem item)
{
- var apiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
+ var apiItem = client.Room?.Playlist.SingleOrDefault(i => i.ID == item.ID);
// Item could have been removed from the playlist while the local player was in gameplay.
if (apiItem == null)
return;
if (item.Expired)
- historyList.Items.Add(apiItem);
+ historyList.Items.Add(new PlaylistItem(apiItem));
else
- queueList.Items.Add(apiItem);
+ queueList.Items.Add(new PlaylistItem(apiItem));
}
private void removeItemFromLists(long item)
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
deleted file mode 100644
index ee5c84bf40..0000000000
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-#nullable disable
-
-using JetBrains.Annotations;
-using osu.Framework.Allocation;
-using osu.Game.Online.Multiplayer;
-using osu.Game.Online.Rooms;
-
-namespace osu.Game.Screens.OnlinePlay.Multiplayer
-{
- public abstract partial class MultiplayerRoomComposite : OnlinePlayComposite
- {
- [CanBeNull]
- protected MultiplayerRoom Room => Client.Room;
-
- [Resolved]
- protected MultiplayerClient Client { get; private set; }
-
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- Client.RoomUpdated += invokeOnRoomUpdated;
- Client.LoadRequested += invokeOnRoomLoadRequested;
- Client.UserLeft += invokeUserLeft;
- Client.UserKicked += invokeUserKicked;
- Client.UserJoined += invokeUserJoined;
- Client.ItemAdded += invokeItemAdded;
- Client.ItemRemoved += invokeItemRemoved;
- Client.ItemChanged += invokeItemChanged;
-
- OnRoomUpdated();
- }
-
- private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated);
- private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.Add(() => UserJoined(user));
- private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.Add(() => UserKicked(user));
- private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.Add(() => UserLeft(user));
- private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item));
- private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item));
- private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item));
- private void invokeOnRoomLoadRequested() => Scheduler.AddOnce(OnRoomLoadRequested);
-
- ///
- /// Invoked when a user has joined the room.
- ///
- /// The user.
- protected virtual void UserJoined(MultiplayerRoomUser user)
- {
- }
-
- ///
- /// Invoked when a user has been kicked from the room (including the local user).
- ///
- /// The user.
- protected virtual void UserKicked(MultiplayerRoomUser user)
- {
- }
-
- ///
- /// Invoked when a user has left the room.
- ///
- /// The user.
- protected virtual void UserLeft(MultiplayerRoomUser user)
- {
- }
-
- ///
- /// Invoked when a playlist item is added to the room.
- ///
- /// The added playlist item.
- protected virtual void PlaylistItemAdded(MultiplayerPlaylistItem item)
- {
- }
-
- ///
- /// Invoked when a playlist item is removed from the room.
- ///
- /// The ID of the removed playlist item.
- protected virtual void PlaylistItemRemoved(long item)
- {
- }
-
- ///
- /// Invoked when a playlist item is changed in the room.
- ///
- /// The new playlist item, with an existing item's ID.
- protected virtual void PlaylistItemChanged(MultiplayerPlaylistItem item)
- {
- }
-
- ///
- /// Invoked when any change occurs to the multiplayer room.
- ///
- protected virtual void OnRoomUpdated()
- {
- }
-
- ///
- /// Invoked when the room requests the local user to load into gameplay.
- ///
- protected virtual void OnRoomLoadRequested()
- {
- }
-
- protected override void Dispose(bool isDisposing)
- {
- if (Client != null)
- {
- Client.RoomUpdated -= invokeOnRoomUpdated;
- Client.LoadRequested -= invokeOnRoomLoadRequested;
- Client.UserLeft -= invokeUserLeft;
- Client.UserKicked -= invokeUserKicked;
- Client.UserJoined -= invokeUserJoined;
- Client.ItemAdded -= invokeItemAdded;
- Client.ItemRemoved -= invokeItemRemoved;
- Client.ItemChanged -= invokeItemChanged;
- }
-
- base.Dispose(isDisposing);
- }
- }
-}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs
index 90595bc33b..d53e485c86 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomSounds.cs
@@ -1,23 +1,26 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
-using osu.Framework.Bindables;
-using osu.Game.Online.API.Requests.Responses;
+using osu.Framework.Extensions.ObjectExtensions;
+using osu.Framework.Graphics.Containers;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
- public partial class MultiplayerRoomSounds : MultiplayerRoomComposite
+ public partial class MultiplayerRoomSounds : CompositeDrawable
{
- private Sample hostChangedSample;
- private Sample userJoinedSample;
- private Sample userLeftSample;
- private Sample userKickedSample;
+ [Resolved]
+ private MultiplayerClient client { get; set; } = null!;
+
+ private Sample? hostChangedSample;
+ private Sample? userJoinedSample;
+ private Sample? userLeftSample;
+ private Sample? userKickedSample;
+ private MultiplayerRoomUser? host;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@@ -32,36 +35,47 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
- Host.BindValueChanged(hostChanged);
+ client.RoomUpdated += onRoomUpdated;
+ client.UserJoined += onUserJoined;
+ client.UserLeft += onUserLeft;
+ client.UserKicked += onUserKicked;
+ updateState();
}
- protected override void UserJoined(MultiplayerRoomUser user)
+ private void onRoomUpdated() => Scheduler.AddOnce(updateState);
+
+ private void updateState()
{
- base.UserJoined(user);
+ if (EqualityComparer.Default.Equals(host, client.Room?.Host))
+ return;
- Scheduler.AddOnce(() => userJoinedSample?.Play());
- }
-
- protected override void UserLeft(MultiplayerRoomUser user)
- {
- base.UserLeft(user);
-
- Scheduler.AddOnce(() => userLeftSample?.Play());
- }
-
- protected override void UserKicked(MultiplayerRoomUser user)
- {
- base.UserKicked(user);
-
- Scheduler.AddOnce(() => userKickedSample?.Play());
- }
-
- private void hostChanged(ValueChangedEvent value)
- {
// only play sound when the host changes from an already-existing host.
- if (value.OldValue == null) return;
+ if (host != null)
+ Scheduler.AddOnce(() => hostChangedSample?.Play());
- Scheduler.AddOnce(() => hostChangedSample?.Play());
+ host = client.Room?.Host;
+ }
+
+ private void onUserJoined(MultiplayerRoomUser user)
+ => Scheduler.AddOnce(() => userJoinedSample?.Play());
+
+ private void onUserLeft(MultiplayerRoomUser user)
+ => Scheduler.AddOnce(() => userLeftSample?.Play());
+
+ private void onUserKicked(MultiplayerRoomUser user)
+ => Scheduler.AddOnce(() => userKickedSample?.Play());
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (client.IsNotNull())
+ {
+ client.RoomUpdated -= onRoomUpdated;
+ client.UserJoined -= onUserJoined;
+ client.UserLeft -= onUserLeft;
+ client.UserKicked -= onUserKicked;
+ }
}
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
index c79c210e30..7e42b18240 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -30,7 +31,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
- public partial class ParticipantPanel : MultiplayerRoomComposite, IHasContextMenu
+ public partial class ParticipantPanel : CompositeDrawable, IHasContextMenu
{
public readonly MultiplayerRoomUser User;
@@ -40,6 +41,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
[Resolved]
private IRulesetStore rulesets { get; set; } = null!;
+ [Resolved]
+ private MultiplayerClient client { get; set; } = null!;
+
private SpriteIcon crown = null!;
private OsuSpriteText userRankText = null!;
@@ -171,23 +175,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Origin = Anchor.Centre,
Alpha = 0,
Margin = new MarginPadding(4),
- Action = () => Client.KickUser(User.UserID).FireAndForget(),
+ Action = () => client.KickUser(User.UserID).FireAndForget(),
},
},
}
};
}
- protected override void OnRoomUpdated()
+ protected override void LoadComplete()
{
- base.OnRoomUpdated();
+ base.LoadComplete();
- if (Room == null || Client.LocalUser == null)
+ client.RoomUpdated += onRoomUpdated;
+ updateState();
+ }
+
+ private void onRoomUpdated() => Scheduler.AddOnce(updateState);
+
+ private void updateState()
+ {
+ if (client.Room == null || client.LocalUser == null)
return;
const double fade_time = 50;
- var currentItem = Playlist.GetCurrentItem();
+ MultiplayerPlaylistItem? currentItem = client.Room.GetCurrentItem();
Ruleset? ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
@@ -200,8 +212,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
else
userModsDisplay.FadeOut(fade_time);
- kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0;
- crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0;
+ kickButton.Alpha = client.IsHost && !User.Equals(client.LocalUser) ? 1 : 0;
+ crown.Alpha = client.Room.Host?.Equals(User) == true ? 1 : 0;
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
@@ -215,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
get
{
- if (Room == null)
+ if (client.Room == null)
return null;
// If the local user is targetted.
@@ -223,7 +235,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
return null;
// If the local user is not the host of the room.
- if (Room.Host?.UserID != api.LocalUser.Value.Id)
+ if (client.Room.Host?.UserID != api.LocalUser.Value.Id)
return null;
int targetUser = User.UserID;
@@ -233,23 +245,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
{
// Ensure the local user is still host.
- if (!Client.IsHost)
+ if (!client.IsHost)
return;
- Client.TransferHost(targetUser).FireAndForget();
+ client.TransferHost(targetUser).FireAndForget();
}),
new OsuMenuItem("Kick", MenuItemType.Destructive, () =>
{
// Ensure the local user is still host.
- if (!Client.IsHost)
+ if (!client.IsHost)
return;
- Client.KickUser(targetUser).FireAndForget();
+ client.KickUser(targetUser).FireAndForget();
})
};
}
}
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (client.IsNotNull())
+ client.RoomUpdated -= onRoomUpdated;
+ }
+
public partial class KickButton : IconButton
{
public KickButton()
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
index 6a7a3758c3..a9d7f4ab52 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs
@@ -1,24 +1,24 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
+using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
- public partial class ParticipantsList : MultiplayerRoomComposite
+ public partial class ParticipantsList : CompositeDrawable
{
- private FillFlowContainer panels;
+ private FillFlowContainer panels = null!;
+ private ParticipantPanel? currentHostPanel;
- [CanBeNull]
- private ParticipantPanel currentHostPanel;
+ [Resolved]
+ private MultiplayerClient client { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
@@ -37,11 +37,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
};
}
- protected override void OnRoomUpdated()
+ protected override void LoadComplete()
{
- base.OnRoomUpdated();
+ base.LoadComplete();
- if (Room == null)
+ client.RoomUpdated += onRoomUpdated;
+ updateState();
+ }
+
+ private void onRoomUpdated() => Scheduler.AddOnce(updateState);
+
+ private void updateState()
+ {
+ if (client.Room == null)
panels.Clear();
else
{
@@ -49,15 +57,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
foreach (var p in panels)
{
// Note that we *must* use reference equality here, as this call is scheduled and a user may have left and joined since it was last run.
- if (Room.Users.All(u => !ReferenceEquals(p.User, u)))
+ if (client.Room.Users.All(u => !ReferenceEquals(p.User, u)))
p.Expire();
}
// Add panels for all users new to the room.
- foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
+ foreach (var user in client.Room.Users.Except(panels.Select(p => p.User)))
panels.Add(new ParticipantPanel(user));
- if (currentHostPanel == null || !currentHostPanel.User.Equals(Room.Host))
+ if (currentHostPanel == null || !currentHostPanel.User.Equals(client.Room.Host))
{
// Reset position of previous host back to normal, if one existing.
if (currentHostPanel != null && panels.Contains(currentHostPanel))
@@ -66,9 +74,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
currentHostPanel = null;
// Change position of new host to display above all participants.
- if (Room.Host != null)
+ if (client.Room.Host != null)
{
- currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host));
+ currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(client.Room.Host));
if (currentHostPanel != null)
panels.SetLayoutPosition(currentHostPanel, -1);
@@ -76,5 +84,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
}
}
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (client.IsNotNull())
+ client.RoomUpdated -= onRoomUpdated;
+ }
}
}
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs
index fe57ad26a5..bd9511d50d 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/TeamDisplay.cs
@@ -1,12 +1,11 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -20,27 +19,26 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
- internal partial class TeamDisplay : MultiplayerRoomComposite
+ internal partial class TeamDisplay : CompositeDrawable
{
private readonly MultiplayerRoomUser user;
- private Drawable box;
-
- private Sample sampleTeamSwap;
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
[Resolved]
- private OsuColour colours { get; set; }
+ private MultiplayerClient client { get; set; } = null!;
- private OsuClickableContainer clickableContent;
+ private OsuClickableContainer clickableContent = null!;
+ private Drawable box = null!;
+ private Sample? sampleTeamSwap;
public TeamDisplay(MultiplayerRoomUser user)
{
this.user = user;
RelativeSizeAxes = Axes.Y;
-
AutoSizeAxes = Axes.X;
-
Margin = new MarginPadding { Horizontal = 3 };
}
@@ -71,7 +69,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
}
};
- if (Client.LocalUser?.Equals(user) == true)
+ if (client.LocalUser?.Equals(user) == true)
{
clickableContent.Action = changeTeam;
clickableContent.TooltipText = "Change team";
@@ -80,23 +78,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap");
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ client.RoomUpdated += onRoomUpdated;
+ updateState();
+ }
+
private void changeTeam()
{
- Client.SendMatchRequest(new ChangeTeamRequest
+ client.SendMatchRequest(new ChangeTeamRequest
{
- TeamID = ((Client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
+ TeamID = ((client.LocalUser?.MatchState as TeamVersusUserState)?.TeamID + 1) % 2 ?? 0,
}).FireAndForget();
}
public int? DisplayedTeam { get; private set; }
- protected override void OnRoomUpdated()
- {
- base.OnRoomUpdated();
+ private void onRoomUpdated() => Scheduler.AddOnce(updateState);
+ private void updateState()
+ {
// we don't have a way of knowing when an individual user's state has updated, so just handle on RoomUpdated for now.
- var userRoomState = Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState;
+ var userRoomState = client.Room?.Users.FirstOrDefault(u => u.Equals(user))?.MatchState;
const double duration = 400;
@@ -138,5 +144,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
return colours.Blue;
}
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+
+ if (client.IsNotNull())
+ client.RoomUpdated -= onRoomUpdated;
+ }
}
}
diff --git a/osu.Game/Screens/Play/Break/BreakInfoLine.cs b/osu.Game/Screens/Play/Break/BreakInfoLine.cs
index df71767f82..79b417732a 100644
--- a/osu.Game/Screens/Play/Break/BreakInfoLine.cs
+++ b/osu.Game/Screens/Play/Break/BreakInfoLine.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Screens.Play.Break
protected virtual LocalisableString Format(T count)
{
if (count is Enum countEnum)
- return countEnum.GetDescription();
+ return countEnum.GetLocalisableDescription();
return count.ToString() ?? string.Empty;
}
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 292f554483..a6c2405eb6 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -300,7 +300,9 @@ namespace osu.Game.Screens.Play
if (element is LegacyHealthDisplay)
return;
- float bottom = drawable.ScreenSpaceDrawQuad.BottomRight.Y;
+ // AABB is used here because the drawable can be flipped/rotated arbitrarily,
+ // so the "bottom right" corner of the raw SSDQ might not necessarily be where one expects it to be.
+ float bottom = drawable.ScreenSpaceDrawQuad.AABBFloat.BottomRight.Y;
bool isRelativeX = drawable.RelativeSizeAxes == Axes.X;
@@ -319,7 +321,7 @@ namespace osu.Game.Screens.Play
// and align bottom-right components with the top-edge of the highest bottom-anchored hud element.
else if (drawable.Anchor.HasFlag(Anchor.BottomRight) || (drawable.Anchor.HasFlag(Anchor.y2) && drawable.RelativeSizeAxes == Axes.X))
{
- var topLeft = element.ScreenSpaceDrawQuad.TopLeft;
+ var topLeft = element.ScreenSpaceDrawQuad.AABBFloat.TopLeft;
if (highestBottomScreenSpace == null || topLeft.Y < highestBottomScreenSpace.Value.Y)
highestBottomScreenSpace = topLeft;
}
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 44f91b4df1..95268c35da 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -216,18 +216,12 @@ namespace osu.Game.Screens.Select
private int visibleSetsCount;
- public BeatmapCarousel(FilterCriteria initialCriterial)
+ public BeatmapCarousel(FilterCriteria initialCriteria)
{
root = new CarouselRoot(this);
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding
- {
- // Avoid clash between scrollbar and osu! logo.
- Top = 10,
- Bottom = 100,
- },
Children = new Drawable[]
{
setPool,
@@ -239,7 +233,7 @@ namespace osu.Game.Screens.Select
}
};
- activeCriteria = initialCriterial;
+ activeCriteria = initialCriteria;
}
[BackgroundDependencyLoader]
@@ -1116,11 +1110,6 @@ namespace osu.Game.Screens.Select
// adjusting the item's overall X position can cause it to become masked away when
// child items (difficulties) are still visible.
item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0);
-
- // We are applying a multiplicative alpha (which is internally done by nesting an
- // additional container and setting that container's alpha) such that we can
- // layer alpha transformations on top.
- item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1));
}
private enum PendingScrollOperation
@@ -1271,6 +1260,38 @@ namespace osu.Game.Screens.Select
return base.OnDragStart(e);
}
+
+ protected override ScrollbarContainer CreateScrollbar(Direction direction)
+ {
+ return new PaddedScrollbar();
+ }
+
+ protected partial class PaddedScrollbar : OsuScrollbar
+ {
+ public PaddedScrollbar()
+ : base(Direction.Vertical)
+ {
+ }
+ }
+
+ private const float top_padding = 10;
+ private const float bottom_padding = 70;
+
+ protected override float ToScrollbarPosition(float scrollPosition)
+ {
+ if (Precision.AlmostEquals(0, ScrollableExtent))
+ return 0;
+
+ return top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent);
+ }
+
+ protected override float FromScrollbarPosition(float scrollbarPosition)
+ {
+ if (Precision.AlmostEquals(0, ScrollbarMovementExtent))
+ return 0;
+
+ return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding)));
+ }
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs
index 755008d370..10921c331e 100644
--- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs
+++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs
@@ -86,8 +86,6 @@ namespace osu.Game.Screens.Select.Carousel
};
}
- public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha;
-
protected override void LoadComplete()
{
base.LoadComplete();
diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs
index 877db75317..b221296ba8 100644
--- a/osu.Game/Screens/Select/FilterControl.cs
+++ b/osu.Game/Screens/Select/FilterControl.cs
@@ -28,7 +28,6 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select.Filter;
using osuTK;
-using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.Select
@@ -97,8 +96,8 @@ namespace osu.Game.Screens.Select
{
new Box
{
- Colour = Color4.Black,
- Alpha = 0.8f,
+ Colour = OsuColour.Gray(0.05f),
+ Alpha = 0.96f,
Width = 2,
RelativeSizeAxes = Axes.Both,
},
diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs
index 933df2464a..1d05f644b7 100644
--- a/osu.Game/Screens/Select/Footer.cs
+++ b/osu.Game/Screens/Select/Footer.cs
@@ -6,12 +6,11 @@
using System.Collections.Generic;
using System.Linq;
using osuTK;
-using osuTK.Graphics;
-using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Screens.Select
@@ -82,14 +81,15 @@ namespace osu.Game.Screens.Select
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
- Colour = Color4.Black.Opacity(0.5f),
+ Colour = OsuColour.Gray(0.1f),
+ Alpha = 0.96f,
},
modeLight = new Box
{
RelativeSizeAxes = Axes.X,
Height = 3,
Position = new Vector2(0, -3),
- Colour = Color4.Black,
+ Colour = OsuColour.Gray(0.1f),
},
new FillFlowContainer
{
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index ea5048ca49..9f7a2c02ff 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -711,12 +711,6 @@ namespace osu.Game.Screens.Select
Carousel.AllowSelection = true;
- if (pendingFilterApplication)
- {
- Carousel.Filter(FilterControl.CreateCriteria());
- pendingFilterApplication = false;
- }
-
BeatmapDetails.Refresh();
beginLooping();
@@ -749,6 +743,17 @@ namespace osu.Game.Screens.Select
FilterControl.Activate();
}
+ protected override void Update()
+ {
+ base.Update();
+
+ if (Carousel.AllowSelection && pendingFilterApplication)
+ {
+ Carousel.Filter(FilterControl.CreateCriteria());
+ pendingFilterApplication = false;
+ }
+ }
+
public override void OnSuspending(ScreenTransitionEvent e)
{
// Handle the case where FinaliseSelection is never called (ie. when a screen is pushed externally).
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 7405a7c587..855ba3374b 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -20,7 +20,7 @@
-
+
@@ -35,9 +35,9 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
-
+